//************************************************************************************** // Filename: InternetLocationCM.cp // Part of Contextual Menu Workshop by Abracode Inc. // http://free.abracode.com/cmworkshop/ // Copyright © 2002-2003 Abracode, Inc. All rights reserved. // // Description: Internet Location Contextual Menu plugin // // History: // 1.4 - Ported to OS X, code cleanup, some fixes // 1.3 - Added info-mac pseudo URL support (like "/info-mac/text/some-cool-app.hqx") // 1.2 - Stripping of "<" and ">" at the beginning and end of an URL respectively, as often used in e-mail // - Support for hard-wrapped URLs (with line breaks) sometimes happening in e-mail // 1.1 - Filename options + about box. // - Duplicate filename problem fix and redesign // 1.0.1 - URL Manager Pro crash fix // 1.0 - Initial release //************************************************************************************** #include "InternetLocationCM.h" #include "CMUtils.h" #include "MoreFilesExtras.h" #include #include #include "CFObjDel.h" #include "StAllocThrowDisable.h" #include "DebugSettings.h" // Strings enum { kCommandStrings = 130, // STR# id kCopyURLToClipboardStr = 20, kCopyMultipleURLsToClipboardStr, kSetupIL_CMStr, kAboutIL_CMStr, kNewInfoMacLocation }; enum ILSaveOptions { kNameKeepBegin = 1, kNameKeepEnd, kNameKeepBeginAndEnd, kNameAsk }; #define kSaveOptionsMask 0x000F #define kDeafultSaveOptions (kNameKeepBegin) //'drag' resource reverse engineering typedef struct FlavorSpec { FlavorType resType; unsigned long resID; long unknown1; //keep zeroed long unknown2; //keep zeroed } FlavorSpec; typedef struct DragResource { long unknown1;//1 - is it a version 1.4 or what? long unknown2;//4 long unknown3;//0 long numberOfFlavors; //1 - 2 FlavorSpec flavors[1]; } DragResource; //we check syntax for these protocols: UrlWordAndCode sBeginKeyWords[] = { {"\phttp://", kInternetLocationHTTP}, {"\phttps://", kInternetLocationHTTP}, {"\pftp://", kInternetLocationFTP}, {"\pnntp:", kInternetLocationNNTP}, {"\pmailto:", kInternetLocationMail} }; enum { kIndexHTTP = 0, kIndexHTTPS, kIndexFTP, kIndexNNTP, kIndexMailto, kFirstTableCount }; //are the protocol prefixes OK? - maybe some of them need "://" instead of ":" UrlWordAndCode sBeginKeyWordsNoCheck[] = { {"\pafp://", kInternetLocationAFP}, {"\pat://", kInternetLocationAppleTalk}, {"\panarchie:", kInternetLocationGeneric}, {"\pbolo:", kInternetLocationGeneric}, {"\pfile://", kInternetLocationGeneric}, {"\pfinger:", kInternetLocationGeneric}, {"\pgopher://", kInternetLocationGeneric}, {"\pnetphone:", kInternetLocationGeneric}, {"\pnetsearch:",kInternetLocationGeneric}, {"\pnews:", kInternetLocationGeneric}, {"\ptelnet://", kInternetLocationGeneric}, {"\ptn3270:", kInternetLocationGeneric}, {"\pwais://", kInternetLocationGeneric}, {"\pwhois:", kInternetLocationGeneric} }; //if there is no protocol name we try to find out which one to use: UrlWordAndCode sBeginServerWords[] = { {"\pwww.", kIndexHTTP},//these are indexes in sBeginKeyWords table to properly resolve protocol prefix {"\pftp.", kIndexFTP}, {"\pnntp.", kIndexNNTP} }; UrlWordAndCode sEndKeyWords[] = { {"\p.html", kIndexHTTP},//these are indexes in sBeginKeyWords table to properly resolve protocol prefix {"\p.htm", kIndexHTTP}, {"\p.shtml", kIndexHTTP}, {"\p.shtm", kIndexHTTP} }; // Function declarations OSStatus AddCommandToAEDescList( ConstStr255Param inCommandString, SInt32 inCommandID, AEDescList* ioCommandList); Handle GetTextFromClipboard(); OSErr Create1Res(OSType inResType, short inResID, void *inDataPtr, Size inDataSize, ConstStr255Param inDescription); Boolean IsURL( Ptr inTextPtr, Size inTextSize, Boolean *outIsNormalized, UrlWordAndCode **tablePtr, short *useIndex); Boolean CanBeServerAddress(char *inTextPtr, Size inLen, Boolean &hasSlashAtEnd); Boolean CanBeMailAddress( char *inTextPtr, Size inTextSize, Boolean &looksLikeFTP ); Boolean BeginsWithServerWord(char *inTextPtr, Size inLen, short &outIndex); Boolean BeginsWithProtocolSpec(char *inTextPtr, Size inLen, short &outIndex); Boolean BeginsWithOtherProtocol(char *inTextPtr, Size inLen, short &outIndex); Boolean EndsWithHTML(char *inTextPtr, Size inLen); Boolean IsContinuous( char* inTextPtr, Size inTextSize); OSErr NormalizeURL(Handle inTextH, UrlWordAndCode tableToUse[], short indexToUse); Boolean StripBeginningAndTrailingWhiteChars(Handle clipTextH); void RemoveLineBreaks(Handle clipTextH); void NormalizeFileName(Str255 inStr, UniChar *outName, UInt16 &outCount); Boolean FindUniqueFileName(const FSRef *parentRef, HFSUniStr255 *ioName); SInt32 BlockCompare( const void *inPtr1, const void *inPtr2, UInt32 inLen1, UInt32 inLen2); Boolean DoPrefsMovableModalDialog(short &flags); //--- OSErr AddURLToText( const FSRef *inRef, Handle *inTextHPtr); //-- info-mac from internet config Boolean BeginsWithInfoMac(char *inTextPtr, Size inLen); Handle GetDefaultInfoMacFromIC(); OSErr MergeInfoMacFragments( Handle ioInfoMacURLBeginH, Ptr infoMacEndPtr, Size infoMacEndPtrSize ); OSStatus FSRefCheckFileOrFolder(const FSRef *inRef, void *ioData); OSStatus FSRefProcessFileOrFolder(const FSRef *inRef, void *ioData); // --------------------------------------------------------------------------- // CMPluginExamineContext // --------------------------------------------------------------------------- // Have a look at the selection and decides whether to display any commands OSStatus CMPluginExamineContext( void *thisInstance, const AEDesc *inContext, AEDescList *ioCommands ) { StAllocThrowDisable disableNewThrows;//for MSL operator new TRACE_STR( "\pInternetLocationCM->CMPluginExamineContext" ); AbstractCMPluginType *theThis = (AbstractCMPluginType *)thisInstance; CMPluginImplData &theData = theThis->implData; OSStatus err = noErr; short strIndex = 0; if(inContext == NULL) return errAENotAEDesc; if(inContext->descriptorType == typeNull) return errAENotAEDesc; //check if any files were selected by user and if at least one is of url type Boolean isIL_CMClicked = false; UInt32 theFlags = kProcBreakOnFirst; if( CMUtils::ProcessObjectList( inContext, theFlags, FSRefCheckFileOrFolder, (void*)&isIL_CMClicked ) ) { TRACE_STR( "\pIL File Clicked" ); if( (theFlags & kListOutMultipleObjects) != 0 ) strIndex = kCopyMultipleURLsToClipboardStr; else strIndex = kCopyURLToClipboardStr; StBundleResOpen resOpen( theThis->bundleRef ); if( resOpen.IsValid() ) { CMUtils::AddResCommand( ioCommands, kCommandStrings, strIndex, kCopyURLToClipboardCommand ); return noErr; } else return fnOpnErr; } else if( isIL_CMClicked && ((theFlags & kListOutMultipleObjects) == 0) ) {//IL CM itself clicked but must be the only clicked TRACE_STR( "\pIL_CM Clicked" ); StBundleResOpen resOpen( theThis->bundleRef ); if( resOpen.IsValid() ) { CMUtils::AddResCommand( ioCommands, kCommandStrings, kAboutIL_CMStr, kAboutIL_CMCommand ); CMUtils::AddResCommand( ioCommands, kCommandStrings, kSetupIL_CMStr, kSetupIL_CMCommand ); return noErr; } else return fnOpnErr; } //check if we have anything in clipboard theData.mClipTextH = GetTextFromClipboard(); if(theData.mClipTextH == NULL) return noTypeErr; TRACE_STR( "\pSome text in clipboard" ); //check what was clicked err = CMUtils::GetFSRef(*inContext, theData.mFolderSpec); if(err == noErr) { //check if folder Boolean isFolder = false; err = CMUtils::IsFolder( &theData.mFolderSpec, isFolder ); if(err != noErr) return err; if( !isFolder ) { return fnfErr; } TRACE_STR( "\pFolder Clicked" ); // EventRecord theEvent; // ::EventAvail(mDownMask, &theEvent); // mWhereClicked = theEvent.where; if( StripBeginningAndTrailingWhiteChars(theData.mClipTextH) )//returns true if there is something more then just white chars { RemoveLineBreaks(theData.mClipTextH); } else { ::DisposeHandle(theData.mClipTextH); theData.mClipTextH = NULL; return errEmptyScrap; } Size textSize = ::GetHandleSize(theData.mClipTextH); ::HLock(theData.mClipTextH); Boolean isUrl = IsURL( *theData.mClipTextH, textSize, &theData.mIsNormalized, &theData.mUsedTable, &theData.mUsedIndex); //short info-mac pseudo URL support if( !isUrl && BeginsWithInfoMac( *theData.mClipTextH, textSize ) && IsContinuous( *theData.mClipTextH, textSize ) ) { Handle infoMacUrl = GetDefaultInfoMacFromIC(); if(infoMacUrl != NULL) { if( MergeInfoMacFragments( infoMacUrl, *theData.mClipTextH, textSize ) == noErr ) { //exchange short pseudo URL with full URL ::HUnlock(theData.mClipTextH); ::DisposeHandle(theData.mClipTextH); theData.mClipTextH = infoMacUrl; StBundleResOpen resOpen( theThis->bundleRef ); if( resOpen.IsValid() ) { CMUtils::AddResCommand( ioCommands, kCommandStrings, kNewInfoMacLocation, kAddURLCommand ); theData.mIsNormalized = true; theData.mUsedTable = sBeginKeyWords; theData.mUsedIndex = kIndexFTP; return noErr; } // else isUrl = false; - handled below } else { ::DisposeHandle( infoMacUrl ); infoMacUrl = NULL; //isUrl = false; - handled below } } // else isUrl = false; - handled below } ::HUnlock(theData.mClipTextH); if( !isUrl) { ::DisposeHandle(theData.mClipTextH); theData.mClipTextH = NULL; return errEmptyScrap; } TRACE_STR( "\pText in clipboard is URL" ); if( theData.mUsedTable == sBeginKeyWords ) strIndex = theData.mUsedIndex + 1;//1-5 else if( theData.mUsedTable == sBeginKeyWordsNoCheck ) strIndex = theData.mUsedIndex + kFirstTableCount + 1; //6 and up StBundleResOpen resOpen( theThis->bundleRef ); if( resOpen.IsValid() ) { CMUtils::AddResCommand( ioCommands, kCommandStrings, strIndex, kAddURLCommand ); } else err = fnOpnErr; } return err; } // --------------------------------------------------------------------------- // CMPluginHandleSelection // --------------------------------------------------------------------------- // Carry out the command that the user selected. The commandID indicates // which command was selected OSStatus CMPluginHandleSelection( void *thisInstance, AEDesc *inContext, SInt32 inCommandID ) { StAllocThrowDisable disableNewThrows;//for MSL operator new TRACE_STR( "\pInternetLocationCM->CMPluginHandleSelection" ); AbstractCMPluginType *theThis = (AbstractCMPluginType *)thisInstance; CMPluginImplData &theData = theThis->implData; OSStatus iErr = noErr; //when files are selected we try to copy URLs to clipboard if(inCommandID == kCopyURLToClipboardCommand) { Handle textH = NULL; UInt32 theFlags = kListClear; CMUtils::ProcessObjectList( inContext, theFlags, FSRefProcessFileOrFolder, (void*)&textH ); if(textH != NULL) { Size handleSize = ::GetHandleSize(textH); if(handleSize > 0) { ::HLock(textH); iErr = ::ClearCurrentScrap(); if(iErr == noErr) { ScrapRef scrap; if( ::GetCurrentScrap(&scrap) == noErr ) { iErr = ::PutScrapFlavor( scrap, 'TEXT', kScrapFlavorMaskNone, handleSize, *textH ); } } } ::DisposeHandle(textH); } return iErr; } if(inCommandID == kAboutIL_CMCommand) { AboutIL_CM(theThis); return iErr; } if(inCommandID == kSetupIL_CMCommand) { SetupIL_CM(theThis); return iErr; } //when no files are selected but clipboard contains URL - we try to make a new Internet Location if( inCommandID != kAddURLCommand) return noErr; if(theData.mClipTextH == NULL) return noTypeErr; FSRef newFileRef; HFSUniStr255 fileName; fileName.length = 0; if( iErr == noErr) { if( !theData.mIsNormalized) { iErr = NormalizeURL(theData.mClipTextH, theData.mUsedTable, theData.mUsedIndex); if(iErr != noErr) { ::DisposeHandle(theData.mClipTextH); theData.mClipTextH = NULL; return iErr; } } Size textSize = ::GetHandleSize(theData.mClipTextH); Size prefixSize = theData.mUsedTable[theData.mUsedIndex].keyWord[0]; ::HLock(theData.mClipTextH); if( ! ChooseFileName( theThis, *theData.mClipTextH, textSize, fileName.unicode, fileName.length, prefixSize ) ) { ::DisposeHandle(theData.mClipTextH); theData.mClipTextH = NULL; return -1;//user canceled } //check if file present iErr = ::FSMakeFSRefUnicode( &theData.mFolderSpec, fileName.length, fileName.unicode, kTextEncodingUnknown, &newFileRef ); if(iErr == noErr) { if( ! FindUniqueFileName( &theData.mFolderSpec, &fileName ) ) iErr = dupFNErr; } else if(iErr == fnfErr) {//good, file does not exist iErr = noErr; } if( iErr == noErr ) { FourCharCode urlCode = theData.mUsedTable[theData.mUsedIndex].urlCode; FSCatalogInfoBitmap whichInfo = kFSCatInfoTextEncoding; FSCatalogInfo theFileInfo; theFileInfo.textEncodingHint = kTextEncodingUnknown;//deafult value will be used ::FSCreateResFile( &theData.mFolderSpec, fileName.length, fileName.unicode, whichInfo, &theFileInfo, &newFileRef, NULL ); iErr = ::ResError(); if( iErr == noErr ) { whichInfo = kFSCatInfoFinderInfo; iErr = ::FSGetCatalogInfo(&newFileRef, whichInfo, &theFileInfo, NULL, NULL, NULL); if(iErr == noErr) { FileInfo *theInfo = (FileInfo *)theFileInfo.finderInfo; theInfo->fileType = urlCode; theInfo->fileCreator = 'MACS'; iErr = ::FSSetCatalogInfo(&newFileRef, whichInfo, &theFileInfo); if(iErr == noErr) { StResOpen resOpen( &newFileRef, fsRdWrPerm ); iErr = ::Create1Res('TEXT', 256, *theData.mClipTextH, textSize, "\pText Clipping"); if(iErr == noErr) { iErr = ::Create1Res('url ', 256, *theData.mClipTextH, textSize, "\pInternet location"); if(iErr == noErr) { DragResource *dragPtr = (DragResource *)::NewPtrClear( sizeof(DragResource) + sizeof(FlavorSpec) );//two flavors if(dragPtr != NULL) { dragPtr->unknown1 = 1; dragPtr->unknown2 = 4; dragPtr->numberOfFlavors = 2; dragPtr->flavors[0].resType = 'TEXT'; dragPtr->flavors[0].resID = 256; dragPtr->flavors[1].resType = 'url '; dragPtr->flavors[1].resID = 256; iErr = ::Create1Res('drag', 128, dragPtr, sizeof(DragResource) + sizeof(FlavorSpec), "\pMade-up drag"); ::DisposePtr( (Ptr)dragPtr ); } } } } } } } ::HUnlock(theData.mClipTextH); } ::DisposeHandle(theData.mClipTextH); theData.mClipTextH = NULL; /* this does not work, why? FInfo fndrInfo; iErr = ::FSpGetFInfo( &macFileSpec, &fndrInfo ); if(iErr == noErr) { fndrInfo.fdLocation = theData.mWhereClicked; iErr = ::FSpSetFInfo( &theData.mMacFileSpec, &fndrInfo); } */ if(iErr == noErr) {//tell Finder to update the item look CMUtils::UpdateFinderObject( &theData.mFolderSpec, false, false ); } return iErr; } // --------------------------------------------------------------------------- // CMPluginPostMenuCleanup // --------------------------------------------------------------------------- void CMPluginPostMenuCleanup( void *thisInstance ) { StAllocThrowDisable disableNewThrows;//for MSL operator new TRACE_STR( "\pInternetLocationCM->CMPluginPostMenuCleanup" ); AbstractCMPluginType *theThis = (AbstractCMPluginType *)thisInstance; CMPluginImplData &theData = theThis->implData; if(theData.mClipTextH != NULL) ::DisposeHandle(theData.mClipTextH); theData.mClipTextH = NULL; } #pragma mark - OSStatus FSRefCheckFileOrFolder(const FSRef *inRef, void *ioData) { if( (inRef == NULL) || (ioData == NULL) ) return paramErr; TRACE_STR( "\pInternetLocationCM->FSRefCheckFileOrFolder" ); Boolean *outIsIL_CMClicked = (Boolean *)ioData; *outIsIL_CMClicked = false; FSCatalogInfo theInfo; HFSUniStr255 theName; OSStatus err = ::FSGetCatalogInfo(inRef, kFSCatInfoNodeFlags, &theInfo, &theName, NULL, NULL); if( err == noErr ) { if( (theInfo.nodeFlags & kFSNodeIsDirectoryMask) != 0) {//it is a directory //brute UTF16 to UTF-8 conversion //we base this on assumption that our plugin name contains ASCII chars only Str255 name; name[0] = theName.length; for(int i = 0; i < theName.length; i++) { name[i+1] = theName.unicode[i]; } if( ::EqualString(name, "\pInternetLocationCM.plugin", false, true) ) *outIsIL_CMClicked = true; //indicate an error, caller will check outIsIL_CMClicked flag to see that our plugin has been clicked return fnfErr; } LSItemInfoRecord itemInfo; LSRequestedInfo whichInfo = kLSRequestTypeCreator; err = ::LSCopyItemInfoForRef( inRef, whichInfo, &itemInfo ); if(err == noErr) { err = fnfErr;//start with error indicating that this is not the file we want if( (kInternetLocationCreator == itemInfo.creator) || ('MACS' == itemInfo.creator) )//'drag' or 'MACS' creator, but it is not enough { OSType fileType = itemInfo.filetype; if ( fileType == kInternetLocationHTTP || fileType == kInternetLocationFTP || fileType == kInternetLocationNNTP || fileType == kInternetLocationMail || fileType == kInternetLocationAFP || fileType == kInternetLocationAppleTalk || fileType == kInternetLocationGeneric ) { err = noErr; } } } } return err; } OSStatus FSRefProcessFileOrFolder(const FSRef *inRef, void *ioData) { TRACE_STR( "\pProcessFileOrFolder" ); if( (inRef == NULL) || (ioData == NULL) ) return paramErr; Handle * textHPtr = (Handle *)ioData; FSCatalogInfo theInfo; OSStatus err = ::FSGetCatalogInfo(inRef, kFSCatInfoNodeFlags, &theInfo, NULL, NULL, NULL); if( err == noErr ) { if( (theInfo.nodeFlags & kFSNodeIsDirectoryMask) != 0) {//it is a directory return fnfErr; } LSItemInfoRecord itemInfo; LSRequestedInfo whichInfo = kLSRequestTypeCreator; err = ::LSCopyItemInfoForRef( inRef, whichInfo, &itemInfo ); if(err == noErr) { err = fnfErr;//start with error indicating that this is not the file we want if( (kInternetLocationCreator == itemInfo.creator) || ('MACS' == itemInfo.creator) )//'drag' or 'MACS' creator, but it is not enough { OSType fileType = itemInfo.filetype; if ( fileType == kInternetLocationHTTP || fileType == kInternetLocationFTP || fileType == kInternetLocationNNTP || fileType == kInternetLocationMail || fileType == kInternetLocationAFP || fileType == kInternetLocationAppleTalk || fileType == kInternetLocationGeneric ) { err = noErr; if( (*textHPtr != NULL) ) { Size oldHSize = ::GetHandleSize( *textHPtr ); //if anything has been put into the handle already - we add CR before next URL (if not present already) if(oldHSize > 0 && (*(*textHPtr))[oldHSize-1] != 0x0D) { ::SetHandleSize( *textHPtr, oldHSize + 1); if( (err = ::MemError()) == noErr) (*(*textHPtr))[oldHSize] = 0x0D;//insert carriage return } } err = AddURLToText( inRef, textHPtr); } } } } return err; } #pragma mark - Handle GetTextFromClipboard() { TRACE_STR( "\pGetTextFromClipboard" ); Handle srcH = NULL; ScrapFlavorType dataType = 'TEXT'; ScrapRef scrap; if( ::GetCurrentScrap(&scrap) == noErr ) { SInt32 byteCount = 0; if( ::GetScrapFlavorSize(scrap, dataType, &byteCount) == noErr ) { srcH = ::NewHandle(byteCount); if(srcH != NULL) { ::HLock(srcH); if( ::GetScrapFlavorData(scrap, dataType, &byteCount, *srcH) != noErr) { ::DisposeHandle(srcH); srcH = NULL;//something is wrong } ::HUnlock(srcH); } } } return srcH; } //this function assumes the res file is open OSErr Create1Res(OSType inResType, short inResID, void *inDataPtr, Size inDataSize, ConstStr255Param inDescription) { Handle dataHandle; OSErr error = noErr; dataHandle = Get1Resource(inResType, inResID); if (dataHandle == NULL) //we create new resource { //create handle and copy data into it error = PtrToHand(inDataPtr, &dataHandle, inDataSize); if (error == noErr) { AddResource(dataHandle, inResType, inResID, inDescription); //tempHandle is now a resource handle error = ResError(); //resource error if(error != noErr) DisposeHandle(dataHandle); } } return error; } #pragma mark - Boolean IsURL( Ptr inTextPtr, Size inTextSize, Boolean *outIsNormalized, UrlWordAndCode **tablePtr, short *useIndex) { short index; Boolean hasSlashAtEnd = false; Boolean canBeFTP = false; if( BeginsWithProtocolSpec(inTextPtr, inTextSize, index) ) { *tablePtr = sBeginKeyWords; *useIndex = index; *outIsNormalized = true; unsigned char offset = sBeginKeyWords[index].keyWord[0]; if(index == kIndexMailto)//mailto is special { //don't try to be too smart - if "mailto:" is at the beginning, do not force ftp, so ignore "canBeFTP" value return CanBeMailAddress( inTextPtr + offset, inTextSize - offset, canBeFTP ); } else if(index == kIndexFTP)//ftp is special as well { if( CanBeServerAddress( inTextPtr + offset, inTextSize - offset, hasSlashAtEnd) ) return true; //something wrong with the syntax - it may be that there is a name and optional password provided //this function performs all the required syntax checking, same as e-mail return CanBeMailAddress( inTextPtr + offset, inTextSize - offset, canBeFTP); } else return CanBeServerAddress( inTextPtr + offset, inTextSize - offset, hasSlashAtEnd); return false; } if( BeginsWithOtherProtocol( inTextPtr, inTextSize, index) )//no syntax checking after that - should we do this? { *tablePtr = sBeginKeyWordsNoCheck; *useIndex = index; *outIsNormalized = true; return true; } if( BeginsWithServerWord(inTextPtr, inTextSize, index) ) // "www." or "ftp." - but we must add http:// or ftp:// { if( CanBeServerAddress(inTextPtr, inTextSize, hasSlashAtEnd) ) { *tablePtr = sBeginKeyWords; *useIndex = (short)(sBeginServerWords[index].urlCode);//url code is index in this table *outIsNormalized = false; return true; } return false; } if( EndsWithHTML(inTextPtr, inTextSize) ) { if( CanBeServerAddress( inTextPtr, inTextSize, hasSlashAtEnd) ) { *tablePtr = sBeginKeyWords; *outIsNormalized = false; *useIndex = kIndexHTTP; return true; //do not return unconditionally here because it may be an ftp with name that gets 'html' file } } //Most of the possibilties are checked - now check for mail and ftp beginning with user name if( CanBeMailAddress( inTextPtr, inTextSize, canBeFTP) ) { *tablePtr = sBeginKeyWords; *outIsNormalized = false; if(canBeFTP) *useIndex = kIndexFTP; else *useIndex = kIndexMailto; return true; } //this covers names like homepage.mac.com or name.com, assume http protocol if( CanBeServerAddress( inTextPtr, inTextSize, hasSlashAtEnd) ) { *tablePtr = sBeginKeyWords; *outIsNormalized = false; *useIndex = kIndexHTTP; return true; } return false; } //handle must be unlocked for this operation: //we add "http://" if we have "www." at the beginning and so on OSErr NormalizeURL(Handle inTextH, UrlWordAndCode tableToUse[], short indexToUse) { OSErr iErr = noErr; Size oldSize = GetHandleSize(inTextH); Size prefixSize = tableToUse[indexToUse].keyWord[0]; SetHandleSize(inTextH, oldSize + prefixSize ); if( (iErr = MemError()) == noErr) { HLock(inTextH); Ptr textPtr = *inTextH; BlockMove(textPtr, textPtr + prefixSize, oldSize); BlockMove( &(tableToUse[indexToUse].keyWord[1]), textPtr, prefixSize); HUnlock(inTextH); } return iErr; } Boolean CanBeServerAddress(char *inTextPtr, Size inLen, Boolean &hasSlashAtEnd) { Size lastDotPos = 0; Boolean hasForbiddenChar = false; hasSlashAtEnd = false; //check first char - it must be an alphanumeric sign or '-' or '_' ? is '_' allowed ? if( (isalnum(inTextPtr[0]) == 0) && (inTextPtr[0] != '_') && (inTextPtr[0] != '-') ) return false; Size i; //then check the rest of the chars for(i = 1; i < inLen; i++) { if(inTextPtr[i] == '.') lastDotPos = i; else if( (isalnum(inTextPtr[i]) == 0) && (inTextPtr[i] != '_') && (inTextPtr[i] != '-') ) { //we assume that this is the end of server address //the rest can be path spec '/' or gate spec and path ':' if( ( (inTextPtr[i] == '/') || (inTextPtr[i] == ':') ) && ((lastDotPos+1) != i)) hasSlashAtEnd = true; else hasForbiddenChar = true; break; } } if( (lastDotPos > 0) && !hasForbiddenChar) { if( hasSlashAtEnd && ( i < (inLen-1) ) )//not the very last char - check the rest { i++;//move to next char after '/' or ':' if( IsContinuous( inTextPtr +i, inLen -i) ) return true; } else return true; } return false; } //we pass the whole url string here and we check if we have '@' before some string, //then we check if the name string looks OK //then we check if the server name looks OK Boolean CanBeMailAddress( char* inTextPtr, Size inTextSize, Boolean &looksLikeFTP ) { Boolean canBeAddress = false; Size atSignPos = 0; looksLikeFTP = false; //look for '@' from the end for( Size i = inTextSize-1; i >= 0; i-- ) { if( inTextPtr[i] == '@' ) { atSignPos = i; break; } } if( (atSignPos > 0) && (atSignPos < (inTextSize-1) ) ) { Boolean looksOK = true; for(Size i = 0; i < atSignPos; i++) { if( isspace(inTextPtr[i]) != 0 )//if we meet a white char we break - it cannot be good { looksOK = false; break; } if(inTextPtr[i] == ':')//well - I would say it is a ftp account with password specified looksLikeFTP = true; } if( looksOK ) {//name looks ok, now take a look at the server address Boolean hasSlash = false; canBeAddress = CanBeServerAddress(inTextPtr+atSignPos+1, inTextSize-atSignPos-1, hasSlash); if(hasSlash)//well it looks like ftp - make sure by checkig integrity (no white spaces) looksLikeFTP = IsContinuous( inTextPtr+atSignPos+1, inTextSize-atSignPos-1); } } return canBeAddress; } Boolean BeginsWithProtocolSpec(char *inTextPtr, Size inLen, short &outIndex) { Boolean doesBegin = false; short tableLen = sizeof(sBeginKeyWords) / sizeof(UrlWordAndCode); for( short i = 0; i < tableLen; i++ ) { unsigned char keyWordLen = sBeginKeyWords[i].keyWord[0]; if( inLen > keyWordLen ) { if( 0 == BlockCompare( inTextPtr, &(sBeginKeyWords[i].keyWord[1]) , keyWordLen, keyWordLen ) ) { doesBegin = true; outIndex = i; break; } } else { ;//skip this one because the string is too short } } return doesBegin; } Boolean BeginsWithOtherProtocol(char *inTextPtr, Size inLen, short &outIndex) { Boolean doesBegin = false; short tableLen = sizeof(sBeginKeyWordsNoCheck) / sizeof(UrlWordAndCode); for( short i = 0; i < tableLen; i++ ) { unsigned char keyWordLen = sBeginKeyWordsNoCheck[i].keyWord[0]; if( inLen > keyWordLen ) { if( 0 == BlockCompare( inTextPtr, &(sBeginKeyWordsNoCheck[i].keyWord[1]) , keyWordLen, keyWordLen ) ) { doesBegin = true; outIndex = i; break; } } else { ;//skip this one because the string is too short } } return doesBegin; } Boolean BeginsWithServerWord(char *inTextPtr, Size inLen, short &outIndex) { Boolean doesBegin = false; short tableLen = sizeof(sBeginServerWords) / sizeof(UrlWordAndCode); for( short i = 0; i < tableLen; i++ ) { unsigned char keyWordLen = sBeginServerWords[i].keyWord[0]; if( inLen > keyWordLen ) { if( 0 == BlockCompare( inTextPtr, &(sBeginServerWords[i].keyWord[1]) , keyWordLen, keyWordLen ) ) { doesBegin = true; outIndex = i; break; } } else { ;//skip this one because the string is too short } } return doesBegin; } Boolean EndsWithHTML(char *inTextPtr, Size inLen) { Boolean doesEnd = false; short tableLen = sizeof(sEndKeyWords) / sizeof(UrlWordAndCode); for( short i = 0; i < tableLen; i++ ) { unsigned char endingLen = sEndKeyWords[i].keyWord[0]; if( inLen > endingLen ) { if( 0 == BlockCompare( inTextPtr + inLen - endingLen , &(sEndKeyWords[i].keyWord[1]) , endingLen, endingLen ) ) { doesEnd = true; break; } } else { ;//skip this one because the string is too short } } return doesEnd; } Boolean IsContinuous( char* inTextPtr, Size inTextSize) { Boolean isContinuous = true; for( Size i = 0; i < inTextSize; i++) { if( isspace(inTextPtr[i]) != 0)//we break when we find the first white char { isContinuous = false; break; } } return isContinuous; } //stolen from PowerPlant :-), but made case insensitive SInt32 BlockCompare( const void *inPtr1, const void *inPtr2, UInt32 inLen1, UInt32 inLen2) { const unsigned char *ucp1 = (const unsigned char *) inPtr1; const unsigned char *ucp2 = (const unsigned char *) inPtr2; unsigned char one, two; UInt32 len = inLen1; // len is the shorter block length if (inLen2 < inLen1) { len = inLen2; } while (len > 0) { // Compare byte by byte one = tolower(*ucp1); two = tolower(*ucp2); if ( one != two ) { return ((SInt32)(one - two)); } ucp1++; ucp2++; len--; } // All bytes the same so far return (SInt32)(inLen1 - inLen2); // Longer block is "bigger" } //16-01-2000: //added stripping of "<" at the beginning and ">" at the end //as it is sometimes entered in e-mail Boolean StripBeginningAndTrailingWhiteChars(Handle clipTextH) { Boolean isAnythingGood = true; Size oldSize = GetHandleSize(clipTextH); HLock(clipTextH); Ptr textPtr = *clipTextH; Size from = 0, to = oldSize-1; for( from; from <= to; from++)//from the beginning first { if( (isspace(textPtr[from]) == 0) && (textPtr[from] != '<') )//we break when we find the first good char break; } for(to; to >= from; to--)//from the end then { if( (isspace(textPtr[to]) == 0) && (textPtr[to] != '>') )//we break when we find the first good char break; } if(from > to) { isAnythingGood = false; HUnlock(clipTextH); } else { Size newSize = to - from + 1; if(newSize < oldSize) { if(from > 0)//move data left BlockMove(textPtr + from, textPtr, newSize); HUnlock(clipTextH); SetHandleSize(clipTextH, newSize); } else HUnlock(clipTextH); } return isAnythingGood; } //23-01-2000: //support for hard-wrapped URLs (this happens in most e-mail clients) - remove CRs and LFs enum { lineBreakLF = 0x0A, lineBreakCR = 0x0D }; void RemoveLineBreaks(Handle clipTextH) { Size oldSize = GetHandleSize(clipTextH); Size newSize = oldSize; HLock(clipTextH); Ptr thePtr = *clipTextH; Size source = 0, dest = 0; while(source < oldSize) { while( ((thePtr[source] == lineBreakLF) || (thePtr[source] == lineBreakCR)) && (source < oldSize) )//eat one or more line breaks { source++; newSize--; } if( (source != dest) && (source < oldSize) ) thePtr[dest] = thePtr[source]; source++; dest++; } HUnlock(clipTextH); if(newSize < oldSize) { SetHandleSize(clipTextH, newSize); } } #pragma mark - Boolean ChooseFileName(AbstractCMPluginType *theThis, Ptr inNamePtr, Size inTextSize, UniChar *outName, UInt16 &outCount, Size inPrefixSize) { //ask for filename CMPluginImplData &theData = theThis->implData; ReadPreferences(theData); Str255 theName; if( (theData.mPrefs & kSaveOptionsMask) == kNameAsk ) { StBundleResOpen resOpen( theThis->bundleRef ); if(! resOpen.IsValid() ) return false; DialogPtr dialogPtr; ControlHandle controlHdl; SInt16 itemHit; Size nameLen = inTextSize - inPrefixSize; if(!(dialogPtr = ::GetNewDialog(129, NULL, (WindowPtr) -1))) return false; ::SetDialogDefaultItem(dialogPtr,kStdOkItemIndex); ::SetDialogCancelItem(dialogPtr,kStdCancelItemIndex); ::SetDialogTracksCursor(dialogPtr,true); ::GetDialogItemAsControl(dialogPtr, 4, &controlHdl); if( nameLen > 255 ) nameLen = 255; theName[0] = nameLen; ::BlockMove( inNamePtr + inPrefixSize, &(theName[1]), nameLen); ::SetDialogItemText( (Handle) controlHdl, theName); ::SelectDialogItemText(dialogPtr,4,0,32767); do { ::ModalDialog(NULL, &itemHit); } while((itemHit != kStdOkItemIndex) && (itemHit != kStdCancelItemIndex)); if(itemHit != kStdCancelItemIndex) { ::GetDialogItemText( (Handle) controlHdl, theName); ::DisposeDialog(dialogPtr); if(theName[0] > 255) theName[0] = 255; NormalizeFileName(theName, outName, outCount); return true; } ::DisposeDialog(dialogPtr); return false; } //automatic filename creation if( (inTextSize-inPrefixSize) > 255 ) {//URL is longer then maximum fileame theName[0] = 255; switch( (theData.mPrefs & kSaveOptionsMask) ) { case kNameKeepBegin: { ::BlockMove( inNamePtr + inPrefixSize, &(theName[1]), 255); } break; case kNameKeepEnd: { Size beginCut = inTextSize-255; ::BlockMove( inNamePtr + beginCut, &(theName[1]), 255); } break; case kNameKeepBeginAndEnd: default: { //find slash to determine how much to take from the end int i = 128; for(i = 64; i < 192; i++) { if( inNamePtr[inTextSize - i -1] == '/') break; } ::BlockMove( inNamePtr + inPrefixSize, &(theName[1]), 255-i);//copy some of the beginning Size endStart = inTextSize-i; ::BlockMove( inNamePtr + endStart, &(theName[255-i+1]), i);//copy end theName[255-i] = 'É';//mark connection } break; } } else {//URL is short, so there is no need to perform any special shortening theName[0] = (unsigned char)(inTextSize-inPrefixSize); ::BlockMove( inNamePtr + inPrefixSize, &(theName[1]), theName[0]); } NormalizeFileName(theName, outName, outCount); return true; } void NormalizeFileName(Str255 inStr, UniChar *outName, UInt16 &outCount) { short i = 0; for( i = 1; i <= inStr[0]; i++ ) { if( inStr[i] == ':') outName[i-1] = (UniChar)'-'; else outName[i-1] = (UniChar)inStr[i]; } outName[i] = (UniChar)0; outCount = (UniCharCount)inStr[0]; } Boolean FindUniqueFileName(const FSRef *parentRef, HFSUniStr255 *ioName) { OSErr err = noErr; short j = ioName->length-1; short i = ioName->length-1; FSRef tempRef; do {// we may have 255 chars: [0...254] if(i < 254) { i++; ioName->unicode[i] = '+';//add to the end ioName->length = i+1; } else if( j >= 0 ) { ioName->unicode[j] = '*'; //replace chars beginning from the end j--; } // err = ::FSMakeFSSpec( tempSpec.vRefNum, tempSpec.parID, tempSpec.name, &tempSpec); err = ::FSMakeFSRefUnicode( parentRef, ioName->length, ioName->unicode, kTextEncodingUnknown, &tempRef ); } while( (err == noErr) && ((i<254) || (j>=0) ) );//we are waiting for fnfErr ! return (err != noErr); } #pragma mark - OSErr AddURLToText( const FSRef *inRef, Handle *inTextHPtr) { OSErr err = noErr; if(inTextHPtr == NULL) return paramErr; if( *inTextHPtr == NULL) { *inTextHPtr = NewHandle(0); if(*inTextHPtr == NULL) return memFullErr; } StResOpen resOpen( inRef, fsRdPerm ); if( resOpen.IsValid() ) { short iCnt = Count1Resources('url '); if(iCnt > 0) { Handle hRsrc = Get1IndResource( 'url ', 1);//get first url - should we check for more? err = ResError(); if(err == noErr && GetHandleSize(hRsrc) > 0 ) { Size oldHSize = GetHandleSize( *inTextHPtr ); err = HandAndHand(hRsrc, *inTextHPtr); } } } else err = fnOpnErr; return err; } #pragma mark - void SetupIL_CM(AbstractCMPluginType *theThis) { CMPluginImplData &theData = theThis->implData; CFURLRef urlRef = ::CFBundleCopyResourceURL( theThis->bundleRef, CFSTR("Preferences.app"), NULL, NULL ); if(urlRef != NULL) { CFObjDel urlDel(urlRef); #if _DEBUG_ CFStringRef strRef = ::CFURLGetString( urlRef ); if(strRef != NULL) { Str255 theString; if( ::CFStringGetPascalString(strRef, theString, sizeof(Str255), kCFStringEncodingMacRoman) ) { DEBUG_STR( theString ); } else { DEBUG_STR( "\pSetupIL_CM. CFStringGetPascalString failed" ); } } else { DEBUG_STR( "\pSetupIL_CM. CFURLGetString failed" ); } #endif //_DEBUG_ ::LSOpenCFURLRef(urlRef, NULL); } else { DEBUG_STR( "\pSetupIL_CM. CFURLCreateFromFSRef failed" ); } } void ReadPreferences(CMPluginImplData &ioData) { TRACE_STR( "\pInternetLocationCM->ReadPreferences" ); //set defaults; ioData.mPrefs = kDeafultSaveOptions; ::CFPreferencesAppSynchronize( CFSTR(CM_IMPL_PLUGIN_PREFS_INDENTIFIER) ); CFPropertyListRef resultRef = ::CFPreferencesCopyAppValue( CFSTR("FILENAME"), CFSTR(CM_IMPL_PLUGIN_PREFS_INDENTIFIER) ); if(resultRef == NULL)//unable to read or not created yet return; CFObjDel resDel(resultRef); if(::CFGetTypeID(resultRef) != ::CFNumberGetTypeID() ) return; CFNumberRef saveOption = (CFNumberRef)resultRef; SInt32 theNumber = 3; ::CFNumberGetValue(saveOption, kCFNumberSInt32Type, &theNumber); ioData.mPrefs &= ~kSaveOptionsMask; ioData.mPrefs |= theNumber; } //this function is stolen (and modified) from: //"MACINTOSH C: A Hobbyist's Guide to Programming the Mac OS in C" //by Ambrosia Software //res file must be open for this function Boolean DoPrefsMovableModalDialog(short &flags) { DialogPtr dialogPtr; ControlHandle controlHdl; SInt16 itemHit=0, a; if(!(dialogPtr = GetNewDialog(130, NULL, (WindowPtr) -1))) return(false); SetDialogDefaultItem(dialogPtr,kStdOkItemIndex); SetDialogCancelItem(dialogPtr,kStdCancelItemIndex); SetDialogTracksCursor(dialogPtr,true); short saveOption = (flags & kSaveOptionsMask); itemHit = saveOption + 2; GetDialogItemAsControl(dialogPtr,itemHit,&controlHdl); SetControlValue(controlHdl,1); ShowWindow( (WindowPtr)dialogPtr ); do { ModalDialog(NULL, &itemHit); if(itemHit >= 3 && itemHit <= 6) { for(a=3;a<=6;a++) { GetDialogItemAsControl(dialogPtr,a,&controlHdl); SetControlValue(controlHdl,0); } GetDialogItemAsControl(dialogPtr,itemHit,&controlHdl); SetControlValue(controlHdl,1); saveOption = itemHit - 2; } } while((itemHit != kStdOkItemIndex) && (itemHit != kStdCancelItemIndex)); if(itemHit != kStdCancelItemIndex) flags = (flags & ~kSaveOptionsMask) | saveOption; DisposeDialog(dialogPtr); return(true); } void AboutIL_CM(AbstractCMPluginType *theThis) { DialogPtr dialogPtr; short itemHit; StBundleResOpen resOpen( theThis->bundleRef ); if(! resOpen.IsValid() ) return; if(NULL != (dialogPtr = ::GetNewDialog(128, NULL, (WindowPtr) -1))) { ::ShowWindow( (WindowPtr)dialogPtr); do { ::ModalDialog(NULL, &itemHit); } while(itemHit != 1); ::DisposeDialog(dialogPtr); } return; } #pragma mark - Str31 sInfoMacStr[] = { "\p/info-mac/", //all mirrors use this catalog name except: "\p/infomac/", // ftp://grind.isca.uiowa.edu/mac/infomac/ "\p/Info-Mac.Archive/" //ftp://mirror.apple.com/mirrors/Info-Mac.Archive/ }; //check for pseudo URL like "/info-mac/text/some-cool-app.hqx" Boolean BeginsWithInfoMac(char *inTextPtr, Size inLen) { Boolean doesBegin = false; unsigned char keyWordLen = sInfoMacStr[0][0]; if( (inLen >= keyWordLen) && (0 == BlockCompare( inTextPtr, &(sInfoMacStr[0][1]) , keyWordLen, keyWordLen )) ) doesBegin = true; return doesBegin; } #define RETURN_IF_ERROR() if(err != noErr) return NULL; Handle GetDefaultInfoMacFromIC() { OSStatus err; ICInstance inst; Str255 infoMacStr = "\p"; ICAttr attr; long size; err = ICStart( &inst, 'ILCM' ); RETURN_IF_ERROR(); err = ICBegin( inst, icReadOnlyPerm ); RETURN_IF_ERROR(); size = sizeof( infoMacStr ); err = ICGetPref( inst, kICInfoMacPreferred, &attr, (Ptr) infoMacStr, &size ); RETURN_IF_ERROR(); err = ICEnd( inst ); RETURN_IF_ERROR(); err = ICStop( inst ); RETURN_IF_ERROR(); if( infoMacStr[0] != 0 ) { Size urlStart = 0; //it is formatted like this: "\pAustralia, ANU:sunsite.anu.edu.au:pub/mac/info-mac/" for( Size i = 1; i < size; i++) { if( infoMacStr[i] == ':') { if(urlStart == 0) urlStart = i+1; else {//the second ':' should be replaced with '/' infoMacStr[i] = '/'; break; } } } Size newSize = size - urlStart; if( newSize > 0) { //allocate handle for our new info-mac URL, add space for "ftp://" (6 chars) Handle outURL = NewHandle( newSize + 6 ); if( outURL != NULL ) { HLock(outURL); BlockMove( "ftp://", *outURL, 6 ); BlockMove( &(infoMacStr[urlStart]), *outURL + 6, newSize ); HUnlock(outURL); return outURL; } } } return NULL; } //the begin handle is expanded and the end is added OSErr MergeInfoMacFragments( Handle ioInfoMacURLBeginH, Ptr infoMacEndPtr, Size infoMacEndPtrSize ) { if(ioInfoMacURLBeginH == NULL || infoMacEndPtr == NULL) return nilHandleErr; OSErr outErr = noErr; Size beginSize = GetHandleSize(ioInfoMacURLBeginH); Size infoMacEndStart = sInfoMacStr[0][0];//we take the string after "/info-mac/" SetHandleSize( ioInfoMacURLBeginH, beginSize + infoMacEndPtrSize - infoMacEndStart ); outErr = MemError(); if(outErr == noErr) { HLock( ioInfoMacURLBeginH ); BlockMove( &(infoMacEndPtr[infoMacEndStart]), *ioInfoMacURLBeginH + beginSize, infoMacEndPtrSize - infoMacEndStart); HUnlock( ioInfoMacURLBeginH ); } return outErr; }