| /* | |
| * Copyright 2008 Google Inc. | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not | |
| * use this file except in compliance with the License. You may obtain a copy of | |
| * the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
| * License for the specific language governing permissions and limitations under | |
| * the License. | |
| */ | |
| #include <map> | |
| #import <JavaScriptCore/JavaScriptCore.h> | |
| #import <WebKit/WebKit.h> | |
| #import "BrowserChannel.h" | |
| #import "Debug.h" | |
| #import "GTMSystemVersion.h" | |
| #import "NSMutableString+HtmlReplacement.h" | |
| #import "LoadModuleMessage.h" | |
| #import "OophmWebScriptObject.h" | |
| #import "SessionHandler.h" | |
| #import "AllowedConnections.h" | |
| /* | |
| * This is a helper shim to bridge crash events from the core cpp code to the | |
| * objc plugin and UI layer. | |
| */ | |
| class PluginCrashHandler : public CrashHandler { | |
| public: | |
| PluginCrashHandler(OophmWebScriptObject* obj) : obj(obj) { | |
| } | |
| virtual void crash(const char* functionName, const char* message) { | |
| Debug::log(Debug::Error) << "Crashing with message: "<< message << Debug::flush; | |
| NSString* str = [NSString stringWithFormat:@"%s\n\n%s", message, functionName]; | |
| [obj crashWithMessage:str]; | |
| } | |
| virtual bool hasCrashed(); | |
| private: | |
| OophmWebScriptObject* const obj; | |
| }; | |
| @interface OophmWebScriptObject (Private) | |
| + (void)logAndThrowString: (NSString*)message; | |
| - (void)addAllowedHost: (NSString*)host; | |
| - (BOOL)hasCrashed; | |
| - (void)connectAlertDidEnd: (NSAlert*)alert | |
| returnCode: (int)returnCode | |
| contextInfo: (void*)contextInfo; | |
| - (BOOL)doConnectWithUrl: (NSString*) url | |
| withSessionKey: (NSString*) sessionKey | |
| withHost: (NSString*) host | |
| withModule: (NSString*) moduleName | |
| withHostedHtmlVersion: (NSString*) hostedHtmlVersion; | |
| @end | |
| // This is declared here so that we can access the category method | |
| bool PluginCrashHandler::hasCrashed() { | |
| return [obj hasCrashed] ? true : false; | |
| } | |
| @implementation OophmWebScriptObject | |
| + (void)initialize { | |
| // Add the plugin's bundle name to the user defaults search path | |
| NSBundle* pluginBundle = [NSBundle bundleForClass:[OophmWebScriptObject class]]; | |
| NSString* bundleIdentifier = [pluginBundle bundleIdentifier]; | |
| NSUserDefaults* shared = [NSUserDefaults standardUserDefaults]; | |
| [shared addSuiteNamed:bundleIdentifier]; | |
| } | |
| + (BOOL)isSelectorExcludedFromWebScript:(SEL)selector { | |
| if (selector == @selector(initForWebScriptWithJsniContext:)) { | |
| return NO; | |
| } else if (selector == @selector(connectWithUrl:withSessionKey:withHost:withModuleName:withHostedHtmlVersion:)) { | |
| return NO; | |
| } else if (selector == @selector(crashWithMessage:)) { | |
| return NO; | |
| } | |
| return YES; | |
| } | |
| + (OophmWebScriptObject*)scriptObjectWithContext: (JSGlobalContextRef) context | |
| withWebView: (WebView*) webView { | |
| JSGlobalContextRetain(context); | |
| OophmWebScriptObject* obj = [[[OophmWebScriptObject alloc] init] autorelease]; | |
| obj->_contextRef = context; | |
| obj->_webView = [webView retain]; | |
| return obj; | |
| } | |
| + (NSString*)webScriptNameForSelector: (SEL)selector { | |
| if (selector == @selector(initForWebScriptWithJsniContext:)) { | |
| return @"init"; | |
| } else if (selector == @selector(connectWithUrl:withSessionKey:withHost:withModuleName:withHostedHtmlVersion:)) { | |
| return @"connect"; | |
| } else if (selector == @selector(crashWithMessage:)) { | |
| return @"crash"; | |
| } | |
| return nil; | |
| } | |
| // Simply return true to indicate the plugin was successfully loaded and | |
| // reachable. | |
| - (BOOL)initForWebScriptWithJsniContext: (WebScriptObject*) jsniContext { | |
| return YES; | |
| } | |
| - (BOOL)connectWithUrl: (NSString*) url | |
| withSessionKey: (NSString*) sessionKey | |
| withHost: (NSString*) host | |
| withModuleName: (NSString*) moduleName | |
| withHostedHtmlVersion: (NSString*) hostedHtmlVersion { | |
| NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; | |
| // See if authentication has been bypassed | |
| if ([defaults boolForKey:@"allowAll"]) { | |
| return [self doConnectWithUrl:url withSessionKey:sessionKey withHost:host | |
| withModule:moduleName withHostedHtmlVersion:hostedHtmlVersion]; | |
| } | |
| // TODO(jat): do this only once, refactor to a separate method | |
| NSArray* allowedHosts = [defaults arrayForKey:@"allowedHosts"]; | |
| if (allowedHosts != nil) { | |
| AllowedConnections::clearRules(); | |
| int n = [allowedHosts count]; | |
| for (int i = 0; i < n; ++i) { | |
| NSString* entry = [allowedHosts objectAtIndex:i]; | |
| std::string hostName = [entry UTF8String]; | |
| int len = hostName.length(); | |
| bool exclude = false; | |
| if (len > 0) { | |
| if (len > 1 && hostName[0] == '!') { | |
| exclude = true; | |
| hostName = hostName.substr(1); | |
| } | |
| AllowedConnections::addRule(hostName, exclude); | |
| } | |
| } | |
| } | |
| const std::string urlStr = [url UTF8String]; | |
| bool allowed = false; | |
| if (AllowedConnections::matchesRule(urlStr, &allowed)) { | |
| if (allowed) { | |
| return [self doConnectWithUrl:url withSessionKey:sessionKey withHost:host | |
| withModule:moduleName withHostedHtmlVersion:hostedHtmlVersion]; | |
| } else { | |
| return YES; | |
| } | |
| } | |
| // Otherwise, bring up an alert dialog | |
| // TODO(jat): add an include/exclude option, currently treat as only include | |
| NSAlert* alert = [NSAlert alertWithMessageText:@"Initiate development mode session" | |
| defaultButton:@"Deny" | |
| alternateButton:nil | |
| otherButton:@"Allow" | |
| informativeTextWithFormat:@"The current web-page would like to initiate a development-mode connection to %@", host]; | |
| if ([alert respondsToSelector:@selector(setShowsSuppressionButton:)]) { | |
| [alert setShowsSuppressionButton:YES]; | |
| [[alert suppressionButton] setTitle:@"Remember this decision for this server"]; | |
| } else { | |
| [[alert addButtonWithTitle:@"Always allow"] setTag:NSAlertAlternateReturn]; | |
| } | |
| NSBundle* bundle = [NSBundle bundleForClass:[OophmWebScriptObject class]]; | |
| NSArray* contextArray = [[NSArray arrayWithObjects:[url retain], | |
| [sessionKey retain], [host retain], [moduleName retain], | |
| [hostedHtmlVersion retain], nil] retain]; | |
| NSString* imagePath = [bundle pathForImageResource:@"gwtlogo"]; | |
| if (imagePath != nil) { | |
| NSImage* img = [[[NSImage alloc] initByReferencingFile:imagePath] autorelease]; | |
| [alert setIcon:img]; | |
| } | |
| [alert beginSheetModalForWindow:[_webView hostWindow] | |
| modalDelegate:self | |
| didEndSelector:@selector(connectAlertDidEnd:returnCode:contextInfo:) | |
| contextInfo:contextArray]; | |
| return YES; | |
| } | |
| - (void)crashWithMessage: (NSString*)message { | |
| if (self->_hasCrashed) { | |
| return; | |
| } | |
| self->_hasCrashed = YES; | |
| #ifdef GWT_DEBUGDISABLE | |
| // We'll call out to the JS support function | |
| JSGlobalContextRef contextRef = self->_contextRef; | |
| JSStringRef disconnectedName = JSStringCreateWithUTF8CString("__gwt_disconnected"); | |
| JSValueRef disconnected = JSObjectGetProperty(contextRef, JSContextGetGlobalObject(contextRef), disconnectedName, NULL); | |
| JSStringRelease(disconnectedName); | |
| if (JSValueIsObject(contextRef, disconnected)) { | |
| // Found hosted.html's crash support | |
| JSObjectRef disconnectedFunction = JSValueToObject(contextRef, disconnected, NULL); | |
| JSValueRef exception = NULL; | |
| JSObjectCallAsFunction(contextRef, disconnectedFunction, JSContextGetGlobalObject(contextRef), 0, NULL, &exception); | |
| if (!exception) { | |
| // Couldn't invoke the crash handler. | |
| return; | |
| } | |
| } | |
| #endif //GWT_DEBUGDISABLE | |
| // Use a simple crash page built into the bundle | |
| NSBundle* oophmBundle = [NSBundle bundleForClass:[self class]]; | |
| NSString* path = [oophmBundle pathForResource:@"crash" ofType:@"html"]; | |
| NSMutableString* crashPage = [NSMutableString stringWithContentsOfFile:path]; | |
| [crashPage replacePattern:@"__MESSAGE__" withStringLiteral:message]; | |
| long major, minor, bugFix; | |
| [GTMSystemVersion getMajor:&major minor:&minor bugFix:&bugFix]; | |
| NSString* systemVersion = [NSString stringWithFormat:@"%i.%i.%i", major, minor, bugFix]; | |
| [crashPage replacePattern:@"__SYSTEM_VERSION__" withStringLiteral:systemVersion]; | |
| NSString* ua = [_webView userAgentForURL:[NSURL URLWithString:@"about:blank"]]; | |
| [crashPage replacePattern:@"__USER_AGENT__" withStringLiteral:ua]; | |
| [crashPage replacePattern:@"__DATE__" | |
| withStringLiteral:[NSString stringWithUTF8String:__DATE__]]; | |
| [crashPage replacePattern:@"__TIME__" | |
| withStringLiteral:[NSString stringWithUTF8String:__TIME__]]; | |
| NSURL* currentUrl = [[[[_webView mainFrame] dataSource] response] URL]; | |
| [[_webView mainFrame] loadAlternateHTMLString:crashPage | |
| baseURL:[NSURL fileURLWithPath:path] | |
| forUnreachableURL:currentUrl]; | |
| } | |
| - (void)dealloc { | |
| [_webView release]; | |
| delete _crashHandler; | |
| [super dealloc]; | |
| } | |
| - (void)finalizeForWebScript { | |
| Debug::log(Debug::Info) << "Finalizing OophmWebScriptObject" << Debug::flush; | |
| // Free memory | |
| delete _sessionHandler; | |
| if (_hostChannel) { | |
| _hostChannel->disconnectFromHost(); | |
| delete _hostChannel; | |
| _hostChannel = NULL; | |
| } | |
| if (_contextRef) { | |
| JSGlobalContextRelease(_contextRef); | |
| _contextRef = NULL; | |
| } | |
| } | |
| @end | |
| @implementation OophmWebScriptObject (Private) | |
| + (void)logAndThrowString:(NSString*)message { | |
| Debug::log(Debug::Info) << "Throwing exception from WSO: " << message << Debug::flush; | |
| [WebScriptObject throwException:message]; | |
| } | |
| - (void)addAllowedHost:(NSString*)host { | |
| /* | |
| * This is more complicated than usual because we're not using the | |
| * application's default persestent domain. Instead, we use a plugin-specific | |
| * domain. | |
| */ | |
| NSBundle* pluginBundle = [NSBundle bundleForClass:[OophmWebScriptObject class]]; | |
| NSString* bundleIdentifier = [pluginBundle bundleIdentifier]; | |
| NSUserDefaults* shared = [NSUserDefaults standardUserDefaults]; | |
| NSDictionary* pluginDict = [shared persistentDomainForName:bundleIdentifier]; | |
| NSArray* allowedHosts = [pluginDict objectForKey:@"allowedHosts"]; | |
| NSMutableArray* mutableHosts = [NSMutableArray arrayWithArray:allowedHosts]; | |
| NSMutableDictionary* mutableDict = [NSMutableDictionary dictionaryWithDictionary:pluginDict]; | |
| [mutableHosts addObject:host]; | |
| [mutableDict setObject:mutableHosts forKey:@"allowedHosts"]; | |
| [shared setPersistentDomain:mutableDict forName:bundleIdentifier]; | |
| [shared synchronize]; | |
| } | |
| - (BOOL)hasCrashed{ | |
| return self->_hasCrashed; | |
| } | |
| - (void)connectAlertDidEnd:(NSAlert *)alert | |
| returnCode:(int)returnCode | |
| contextInfo:(void *)contextInfo { | |
| NSArray* contextArray = (NSArray*) contextInfo; | |
| NSString* url = [[contextArray objectAtIndex:0] autorelease]; | |
| NSString* sessionKey = [[contextArray objectAtIndex:1] autorelease]; | |
| NSString* host = [[contextArray objectAtIndex:2] autorelease]; | |
| NSString* moduleName = [[contextArray objectAtIndex:3] autorelease]; | |
| NSString* hostedHtmlVersion = [[contextArray objectAtIndex:4] autorelease]; | |
| [contextArray release]; | |
| if (returnCode == NSAlertDefaultReturn) { | |
| return; | |
| } else if (returnCode == NSAlertAlternateReturn || | |
| [alert respondsToSelector:@selector(suppressionButton)] && | |
| [[alert suppressionButton] state] == NSOnState) { | |
| // TODO(jat): simplify, handle errors | |
| // Get the host part of the URL and store that | |
| NSString* server = [[[[[[url componentsSeparatedByString:@"://"] | |
| objectAtIndex:1] componentsSeparatedByString:@"/"] objectAtIndex:0] | |
| componentsSeparatedByString:@":"] objectAtIndex:0]; | |
| [self addAllowedHost:server]; | |
| } | |
| [self doConnectWithUrl:url withSessionKey:sessionKey withHost:host | |
| withModule:moduleName withHostedHtmlVersion:hostedHtmlVersion]; | |
| } | |
| - (BOOL)doConnectWithUrl: (NSString*) url | |
| withSessionKey: (NSString*) sessionKey | |
| withHost: (NSString*) host | |
| withModule: (NSString*) moduleName | |
| withHostedHtmlVersion: (NSString*) hostedHtmlVersion { | |
| Debug::log(Debug::Debugging) << "connect : " << [host UTF8String] << " " << | |
| [moduleName UTF8String] << Debug::flush; | |
| if (_hostChannel != NULL) { | |
| [OophmWebScriptObject logAndThrowString:@"Already connected"]; | |
| return NO; | |
| } | |
| NSArray *parts = [host componentsSeparatedByString:@":"]; | |
| if ([parts count] != 2) { | |
| [OophmWebScriptObject logAndThrowString: | |
| [NSString stringWithFormat:@"Incorrect format for host string %i", | |
| [parts count]]]; | |
| return NO; | |
| } | |
| NSString *hostPart = [parts objectAtIndex:0]; | |
| NSString *portPart = [parts objectAtIndex:1]; | |
| Debug::log(Debug::Debugging) << "Extracted host: " << [hostPart UTF8String] << | |
| " and port: " << [portPart UTF8String] << Debug::flush; | |
| char *hostAsChars = const_cast<char*>([hostPart UTF8String]); | |
| unsigned portAsInt = [portPart intValue]; | |
| _hostChannel = new HostChannel(); | |
| if (!_hostChannel->connectToHost(hostAsChars, portAsInt)) { | |
| [OophmWebScriptObject logAndThrowString:@"HostChannel failed to connect"]; | |
| delete _hostChannel; | |
| _hostChannel = NULL; | |
| return NO; | |
| } | |
| _crashHandler = new PluginCrashHandler(self); | |
| _sessionHandler = new WebScriptSessionHandler(_hostChannel, _contextRef, _crashHandler); | |
| std::string hostedHtmlVersionStr([hostedHtmlVersion UTF8String]); | |
| // TODO: add support for a range of protocol versions when more are added. | |
| if (!_hostChannel->init(_sessionHandler, BROWSERCHANNEL_PROTOCOL_VERSION, | |
| BROWSERCHANNEL_PROTOCOL_VERSION, hostedHtmlVersionStr)) { | |
| [OophmWebScriptObject logAndThrowString:@"HostChannel failed to initialize"]; | |
| _hostChannel->disconnectFromHost(); | |
| delete _hostChannel; | |
| _hostChannel = NULL; | |
| return NO; | |
| } | |
| const std::string urlStr = [url UTF8String]; | |
| // TODO(jat): add support for tab identity | |
| const std::string tabKeyStr = ""; | |
| const std::string sessionKeyStr = [sessionKey UTF8String]; | |
| const std::string moduleNameStr = [moduleName UTF8String]; | |
| if (!LoadModuleMessage::send(*_hostChannel, urlStr, tabKeyStr, | |
| sessionKeyStr, moduleNameStr, | |
| "Safari DMP", _sessionHandler)) { | |
| _hostChannel->disconnectFromHost(); | |
| delete _hostChannel; | |
| _hostChannel = NULL; | |
| [OophmWebScriptObject logAndThrowString:@"Unable to load module"]; | |
| return NO; | |
| } | |
| return YES; | |
| } | |
| @end |