/core/externals/update-engine/externals/google-toolbox-for-mac/AppKit/GTMGetURLHandler.m
Objective C | 308 lines | 211 code | 19 blank | 78 comment | 29 complexity | 2b5f09eb320ffada04bc91ae45b66e30 MD5 | raw file
1// 2// GTMGetURLHandler.m 3// 4// Copyright 2008 Google Inc. 5// 6// Licensed under the Apache License, Version 2.0 (the "License"); you may not 7// use this file except in compliance with the License. You may obtain a copy 8// of the License at 9// 10// http://www.apache.org/licenses/LICENSE-2.0 11// 12// Unless required by applicable law or agreed to in writing, software 13// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15// License for the specific language governing permissions and limitations under 16// the License. 17// 18 19// Add this class to your app to have get URL handled almost automatically for 20// you. For each entry in your CFBundleURLTypes dictionaries, add a new 21// key/object pair of GTMBundleURLClass/the name of the class you want 22// to have handle the scheme(s). 23// Then have that class respond to the class method: 24// + (BOOL)gtm_openURL:(NSURL*)url 25// and voila, it will just work. 26// Note that in Debug mode we will do extensive testing to make sure that this 27// is all hooked up correctly, and will spew out to the console if we 28// find anything amiss. 29// 30// Example plist entry 31// ... 32// 33// <key>CFBundleURLTypes</key> 34// <array> 35// <dict> 36// <key>CFBundleURLName</key> 37// <string>Google Suggestion URL</string> 38// <key>GTMBundleURLClass</key> 39// <string>GoogleSuggestURLHandler</string> 40// <key>CFBundleURLSchemes</key> 41// <array> 42// <string>googlesuggest</string> 43// <string>googlesuggestextreme</string> 44// </array> 45// </dict> 46// </array> 47// 48// 49// Example implementation 50// @interface GoogleSuggestURLHandler 51// @end 52// @implementation GoogleSuggestURLHandler 53// + (BOOL)gtm_openURL:(NSURL*)url { 54// NSLog(@"%@", url); 55// } 56// @end 57 58#import <AppKit/AppKit.h> 59#import "GTMNSAppleEventDescriptor+Foundation.h" 60#import "GTMMethodCheck.h" 61 62static NSString *const kGTMBundleURLClassKey = @"GTMBundleURLClass"; 63// A variety of constants Apple really should have defined somewhere to 64// allow the compiler to find your typos. 65static NSString *const kGTMCFBundleURLSchemesKey = @"CFBundleURLSchemes"; 66static NSString *const kGTMCFBundleURLNameKey = @"CFBundleURLName"; 67static NSString *const kGTMCFBundleTypeRoleKey = @"CFBundleTypeRole"; 68static NSString *const kGTMCFBundleURLTypesKey = @"CFBundleURLTypes"; 69static NSString *const kGTMCFBundleViewerRole = @"Viewer"; 70static NSString *const kGTMCFBundleEditorRole = @"Editor"; 71 72// Set this macro elsewhere is you want to force the 73// bundle checks on/off. They are nice for debugging 74// problems, but shouldn't be required in a release version 75// unless you are paranoid about your users messing with your 76// Info.plist 77#ifndef GTM_CHECK_BUNDLE_URL_CLASSES 78#define GTM_CHECK_BUNDLE_URL_CLASSES DEBUG 79#endif // GTM_CHECK_BUNDLE_URL_CLASSES 80 81@protocol GTMGetURLHandlerProtocol 82+ (BOOL)gtm_openURL:(NSURL*)url; 83@end 84 85@interface GTMGetURLHandler : NSObject { 86 NSArray *urlTypes_; 87} 88- (id)initWithTypes:(NSArray*)urlTypes; 89- (void)getUrl:(NSAppleEventDescriptor *)event 90withReplyEvent:(NSAppleEventDescriptor *)replyEvent; 91- (void)addError:(OSStatus)error 92 withDescription:(NSString*)string 93 toDescriptor:(NSAppleEventDescriptor *)desc; 94+ (id)handlerForBundle:(NSBundle *)bundle; 95+ (void)getUrl:(NSAppleEventDescriptor *)event 96withReplyEvent:(NSAppleEventDescriptor *)replyEvent; 97@end 98 99@implementation GTMGetURLHandler 100GTM_METHOD_CHECK(NSNumber, gtm_appleEventDescriptor); 101GTM_METHOD_CHECK(NSString, gtm_appleEventDescriptor); 102 103+ (void)load { 104 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 105 NSAppleEventManager *man = [NSAppleEventManager sharedAppleEventManager]; 106 [man setEventHandler:self 107 andSelector:@selector(getUrl:withReplyEvent:) 108 forEventClass:kInternetEventClass 109 andEventID:kAEGetURL]; 110 [pool drain]; 111} 112 113+ (void)getUrl:(NSAppleEventDescriptor *)event 114withReplyEvent:(NSAppleEventDescriptor *)replyEvent { 115 static GTMGetURLHandler *sHandler = nil; 116 if (!sHandler) { 117 NSBundle *bundle = [NSBundle mainBundle]; 118 sHandler = [GTMGetURLHandler handlerForBundle:bundle]; 119 if (sHandler) { 120 [sHandler retain]; 121 } 122 } 123 [sHandler getUrl:event withReplyEvent:replyEvent]; 124} 125 126+ (id)handlerForBundle:(NSBundle *)bundle { 127 GTMGetURLHandler *handler = nil; 128 NSArray *urlTypes 129 = [bundle objectForInfoDictionaryKey:kGTMCFBundleURLTypesKey]; 130 if (urlTypes) { 131 handler = [[[GTMGetURLHandler alloc] initWithTypes:urlTypes] autorelease]; 132 } else { 133 // COV_NF_START 134 // Hard to test it if we don't have it. 135 _GTMDevLog(@"If you don't have CFBundleURLTypes in your plist, you may want" 136 @" to remove GTMGetURLHandler.m from your project"); 137 // COV_NF_END 138 } 139 return handler; 140} 141 142- (id)initWithTypes:(NSArray*)urlTypes { 143 if ((self = [super init])) { 144 urlTypes_ = [urlTypes retain]; 145#if GTM_CHECK_BUNDLE_URL_CLASSES 146 // Some debug handling to check to make sure we can handle the 147 // classes properly. We check here instead of at init in case some of the 148 // handlers are being handled by plugins or other imported code that are 149 // loaded after we have been initialized. 150 NSDictionary *urlType; 151 GTM_FOREACH_OBJECT(urlType, urlTypes_) { 152 NSString *className = [urlType objectForKey:kGTMBundleURLClassKey]; 153 if ([className length]) { 154 Class cls = NSClassFromString(className); 155 if (cls) { 156 if (![cls respondsToSelector:@selector(gtm_openURL:)]) { 157 _GTMDevLog(@"Class %@ for URL handler %@ " 158 @"(URL schemes: %@) doesn't respond to openURL:", 159 className, 160 [urlType objectForKey:kGTMCFBundleURLNameKey], 161 [urlType objectForKey:kGTMCFBundleURLSchemesKey]); 162 } 163 } else { 164 _GTMDevLog(@"Unable to get class %@ for URL handler %@ " 165 @"(URL schemes: %@)", 166 className, 167 [urlType objectForKey:kGTMCFBundleURLNameKey], 168 [urlType objectForKey:kGTMCFBundleURLSchemesKey]); 169 } 170 } else { 171 NSString *role = [urlType objectForKey:kGTMCFBundleTypeRoleKey]; 172 if ([role caseInsensitiveCompare:kGTMCFBundleViewerRole] == NSOrderedSame || 173 [role caseInsensitiveCompare:kGTMCFBundleEditorRole] == NSOrderedSame) { 174 _GTMDevLog(@"Missing %@ for URL handler %@ " 175 @"(URL schemes: %@)", 176 kGTMBundleURLClassKey, 177 [urlType objectForKey:kGTMCFBundleURLNameKey], 178 [urlType objectForKey:kGTMCFBundleURLSchemesKey]); 179 } 180 } 181 } 182#endif // GTM_CHECK_BUNDLE_URL_CLASSES 183 } 184 return self; 185} 186 187// COV_NF_START 188// Singleton is never dealloc'd 189- (void)dealloc { 190 [urlTypes_ release]; 191 [super dealloc]; 192} 193// COV_NF_END 194 195 196- (NSURL*)extractURLFromEvent:(NSAppleEventDescriptor*)event 197 withReplyEvent:(NSAppleEventDescriptor *)replyEvent { 198 NSAppleEventDescriptor *desc 199 = [event paramDescriptorForKeyword:keyDirectObject]; 200 NSString *urlstring = [desc stringValue]; 201 NSURL *url = [NSURL URLWithString:urlstring]; 202 if (!url) { 203 // COV_NF_START 204 // Can't convince the OS to give me a bad URL 205 [self addError:errAECoercionFail 206 withDescription:@"Unable to extract url from key direct object." 207 toDescriptor:replyEvent]; 208 // COV_NF_END 209 } 210 return url; 211} 212 213- (Class)getClassForScheme:(NSString *)scheme 214 withReplyEvent:(NSAppleEventDescriptor*)replyEvent { 215 NSDictionary *urlType; 216 Class cls = nil; 217 NSString *typeScheme = nil; 218 GTM_FOREACH_OBJECT(urlType, urlTypes_) { 219 NSArray *schemes = [urlType objectForKey:kGTMCFBundleURLSchemesKey]; 220 NSString *aScheme; 221 GTM_FOREACH_OBJECT(aScheme, schemes) { 222 if ([aScheme caseInsensitiveCompare:scheme] == NSOrderedSame) { 223 typeScheme = aScheme; 224 break; 225 } 226 } 227 if (typeScheme) { 228 break; 229 } 230 } 231 if (typeScheme) { 232 NSString *class = [urlType objectForKey:kGTMBundleURLClassKey]; 233 if (class) { 234 cls = NSClassFromString(class); 235 } 236 if (!cls) { 237 NSString *errorString 238 = [NSString stringWithFormat:@"Unable to instantiate class for " 239 @"%@:%@ for scheme:%@.", 240 kGTMBundleURLClassKey, class, typeScheme]; 241 [self addError:errAECorruptData 242 withDescription:errorString 243 toDescriptor:replyEvent]; 244 } else { 245 if (![cls respondsToSelector:@selector(gtm_openURL:)]) { 246 NSString *errorString 247 = [NSString stringWithFormat:@"Class %@:%@ for scheme:%@ does not" 248 @"respond to gtm_openURL:", 249 kGTMBundleURLClassKey, class, typeScheme]; 250 [self addError:errAECorruptData 251 withDescription:errorString 252 toDescriptor:replyEvent]; 253 cls = Nil; 254 } 255 } 256 } else { 257 // COV_NF_START 258 // Don't know how to force an URL that we don't respond to upon ourselves. 259 NSString *errorString 260 = [NSString stringWithFormat:@"Unable to find handler for scheme %@.", 261 scheme]; 262 [self addError:errAECorruptData 263 withDescription:errorString 264 toDescriptor:replyEvent]; 265 // COV_NF_END 266 267 } 268 return cls; 269} 270 271- (void)getUrl:(NSAppleEventDescriptor *)event 272withReplyEvent:(NSAppleEventDescriptor *)replyEvent { 273 NSURL *url = [self extractURLFromEvent:event withReplyEvent:replyEvent]; 274 if (!url) { 275 return; 276 } 277 NSString *scheme = [url scheme]; 278 Class cls = [self getClassForScheme:scheme withReplyEvent:replyEvent]; 279 if (!cls) { 280 return; 281 } 282 BOOL wasGood = [cls gtm_openURL:url]; 283 if (!wasGood) { 284 NSString *errorString 285 = [NSString stringWithFormat:@"[%@ gtm_openURL:] failed to handle %@", 286 NSStringFromClass(cls), url]; 287 [self addError:errAEEventNotHandled 288 withDescription:errorString 289 toDescriptor:replyEvent]; 290 } 291} 292 293- (void)addError:(OSStatus)error 294 withDescription:(NSString*)string 295 toDescriptor:(NSAppleEventDescriptor *)desc { 296 NSAppleEventDescriptor *errorDesc = nil; 297 if (error != noErr) { 298 NSNumber *errNum = [NSNumber numberWithLong:error]; 299 errorDesc = [errNum gtm_appleEventDescriptor]; 300 [desc setParamDescriptor:errorDesc forKeyword:keyErrorNumber]; 301 } 302 if (string) { 303 errorDesc = [string gtm_appleEventDescriptor]; 304 [desc setParamDescriptor:errorDesc forKeyword:keyErrorString]; 305 } 306} 307@end 308