| /* |
| * 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; |
| |
| // Disable any lingering JS proxy objects |
| _hasCrashed = true; |
| |
| // 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 |