| /* | 
 |  * 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 |