diff options
author | yoz@chromium.org <yoz@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-04 23:46:16 +0000 |
---|---|---|
committer | yoz@chromium.org <yoz@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-06-04 23:46:16 +0000 |
commit | 8f857ef8ff619a02e2318c120dcbb2263beaae30 (patch) | |
tree | d5c12ee644e1f0e80e32fdfdb3ebcd5f767a8357 /extensions | |
parent | ef198a101f10c40b44c0397bd4f8a37c6ec51344 (diff) | |
download | chromium_src-8f857ef8ff619a02e2318c120dcbb2263beaae30.zip chromium_src-8f857ef8ff619a02e2318c120dcbb2263beaae30.tar.gz chromium_src-8f857ef8ff619a02e2318c120dcbb2263beaae30.tar.bz2 |
Reland 274764 - Reland 274558 - Move some resources to extensions_renderer_resources.grd.
This patch fixes references in the .gyp to .js resource files (again).
This breaks the remaining dependency from src/extensions to chrome resources files.
BUG=368334
TBR=rockot@chromium.org,jam@chromium.org
Review URL: https://codereview.chromium.org/312213002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@274954 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'extensions')
31 files changed, 4245 insertions, 5 deletions
diff --git a/extensions/DEPS b/extensions/DEPS index f919975..d762dc4 100644 --- a/extensions/DEPS +++ b/extensions/DEPS @@ -2,6 +2,7 @@ include_rules = [ "+components/url_matcher", "+content/public/common", "+crypto", + "+grit/extensions_renderer_resources.h", "+grit/extensions_resources.h", "+testing", "+ui", @@ -13,9 +14,6 @@ include_rules = [ # # TODO(jamescook): Remove these. http://crbug.com/162530 "!chrome/browser/chrome_notification_types.h", - # This is needed for renderer JS sources which should eventually move to - # the extensions_resources target. - "!grit/renderer_resources.h", ] specific_include_rules = { diff --git a/extensions/extensions.gyp b/extensions/extensions.gyp index e00b987..2d6c3ca 100644 --- a/extensions/extensions.gyp +++ b/extensions/extensions.gyp @@ -526,6 +526,31 @@ 'renderer/request_sender.h', 'renderer/resource_bundle_source_map.cc', 'renderer/resource_bundle_source_map.h', + 'renderer/resources/app_runtime_custom_bindings.js', + 'renderer/resources/binding.js', + 'renderer/resources/context_menus_custom_bindings.js', + 'renderer/resources/entry_id_manager.js', + 'renderer/resources/event.js', + 'renderer/resources/extension_custom_bindings.js', + 'renderer/resources/greasemonkey_api.js', + 'renderer/resources/i18n_custom_bindings.js', + 'renderer/resources/image_util.js', + 'renderer/resources/json_schema.js', + 'renderer/resources/last_error.js', + 'renderer/resources/messaging.js', + 'renderer/resources/messaging_utils.js', + 'renderer/resources/permissions_custom_bindings.js', + 'renderer/resources/platform_app.css', + 'renderer/resources/platform_app.js', + 'renderer/resources/runtime_custom_bindings.js', + 'renderer/resources/schema_utils.js', + 'renderer/resources/send_request.js', + 'renderer/resources/set_icon.js', + 'renderer/resources/storage_area.js', + 'renderer/resources/test_custom_bindings.js', + 'renderer/resources/uncaught_exception_handler.js', + 'renderer/resources/unload_event.js', + 'renderer/resources/utils.js', 'renderer/runtime_custom_bindings.cc', 'renderer/runtime_custom_bindings.h', 'renderer/safe_builtins.cc', diff --git a/extensions/extensions_resources.gyp b/extensions/extensions_resources.gyp index 856def8..dd0e1cd 100644 --- a/extensions/extensions_resources.gyp +++ b/extensions/extensions_resources.gyp @@ -18,8 +18,20 @@ }, 'includes': [ '../build/grit_action.gypi' ], }, + { + 'action_name': 'extensions_renderer_resources', + 'variables': { + 'grit_grd_file': 'renderer/resources/extensions_renderer_resources.grd', + }, + 'includes': [ '../build/grit_action.gypi' ], + }, ], 'includes': [ '../build/grit_target.gypi' ], + 'direct_dependent_settings': { + 'include_dirs': [ + '<(SHARED_INTERMEDIATE_DIR)/extensions', + ] + }, 'hard_dependency': 1, } ] diff --git a/extensions/renderer/dispatcher.cc b/extensions/renderer/dispatcher.cc index 0d11726..dde54db 100644 --- a/extensions/renderer/dispatcher.cc +++ b/extensions/renderer/dispatcher.cc @@ -73,7 +73,7 @@ #include "extensions/renderer/user_script_slave.h" #include "extensions/renderer/utils_native_handler.h" #include "extensions/renderer/v8_context_native_handler.h" -#include "grit/renderer_resources.h" +#include "grit/extensions_renderer_resources.h" #include "third_party/WebKit/public/platform/WebString.h" #include "third_party/WebKit/public/platform/WebURLRequest.h" #include "third_party/WebKit/public/web/WebCustomElement.h" diff --git a/extensions/renderer/resources/app_runtime_custom_bindings.js b/extensions/renderer/resources/app_runtime_custom_bindings.js new file mode 100644 index 0000000..f4fe24d --- /dev/null +++ b/extensions/renderer/resources/app_runtime_custom_bindings.js @@ -0,0 +1,56 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Custom binding for the chrome.app.runtime API. + +var binding = require('binding').Binding.create('app.runtime'); + +var eventBindings = require('event_bindings'); +var fileSystemHelpers = requireNative('file_system_natives'); +var GetIsolatedFileSystem = fileSystemHelpers.GetIsolatedFileSystem; +var appNatives = requireNative('app_runtime'); +var DeserializeString = appNatives.DeserializeString; +var SerializeToString = appNatives.SerializeToString; +var CreateBlob = appNatives.CreateBlob; +var entryIdManager = require('entryIdManager'); + +eventBindings.registerArgumentMassager('app.runtime.onLaunched', + function(args, dispatch) { + var launchData = args[0]; + if (launchData.items) { + // An onLaunched corresponding to file_handlers in the app's manifest. + var items = []; + var numItems = launchData.items.length; + var itemLoaded = function(err, item) { + if (err) { + console.error('Error getting fileEntry, code: ' + err.code); + } else { + $Array.push(items, item); + } + if (--numItems === 0) { + var data = { isKioskSession: launchData.isKioskSession }; + if (items.length !== 0) { + data.id = launchData.id; + data.items = items; + } + dispatch([data]); + } + }; + $Array.forEach(launchData.items, function(item) { + var fs = GetIsolatedFileSystem(item.fileSystemId); + fs.root.getFile(item.baseName, {}, function(fileEntry) { + entryIdManager.registerEntry(item.entryId, fileEntry); + itemLoaded(null, { entry: fileEntry, type: item.mimeType }); + }, function(fileError) { + itemLoaded(fileError); + }); + }); + } else { + // Default case. This currently covers an onLaunched corresponding to + // url_handlers in the app's manifest. + dispatch([launchData]); + } +}); + +exports.binding = binding.generate(); diff --git a/extensions/renderer/resources/binding.js b/extensions/renderer/resources/binding.js new file mode 100644 index 0000000..b0c6afa --- /dev/null +++ b/extensions/renderer/resources/binding.js @@ -0,0 +1,434 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +var Event = require('event_bindings').Event; +var forEach = require('utils').forEach; +var GetAvailability = requireNative('v8_context').GetAvailability; +var logActivity = requireNative('activityLogger'); +var logging = requireNative('logging'); +var process = requireNative('process'); +var schemaRegistry = requireNative('schema_registry'); +var schemaUtils = require('schemaUtils'); +var utils = require('utils'); +var sendRequestHandler = require('sendRequest'); + +var contextType = process.GetContextType(); +var extensionId = process.GetExtensionId(); +var manifestVersion = process.GetManifestVersion(); +var sendRequest = sendRequestHandler.sendRequest; + +// Stores the name and definition of each API function, with methods to +// modify their behaviour (such as a custom way to handle requests to the +// API, a custom callback, etc). +function APIFunctions(namespace) { + this.apiFunctions_ = {}; + this.unavailableApiFunctions_ = {}; + this.namespace = namespace; +} + +APIFunctions.prototype.register = function(apiName, apiFunction) { + this.apiFunctions_[apiName] = apiFunction; +}; + +// Registers a function as existing but not available, meaning that calls to +// the set* methods that reference this function should be ignored rather +// than throwing Errors. +APIFunctions.prototype.registerUnavailable = function(apiName) { + this.unavailableApiFunctions_[apiName] = apiName; +}; + +APIFunctions.prototype.setHook_ = + function(apiName, propertyName, customizedFunction) { + if ($Object.hasOwnProperty(this.unavailableApiFunctions_, apiName)) + return; + if (!$Object.hasOwnProperty(this.apiFunctions_, apiName)) + throw new Error('Tried to set hook for unknown API "' + apiName + '"'); + this.apiFunctions_[apiName][propertyName] = customizedFunction; +}; + +APIFunctions.prototype.setHandleRequest = + function(apiName, customizedFunction) { + var prefix = this.namespace; + return this.setHook_(apiName, 'handleRequest', + function() { + var ret = $Function.apply(customizedFunction, this, arguments); + // Logs API calls to the Activity Log if it doesn't go through an + // ExtensionFunction. + if (!sendRequestHandler.getCalledSendRequest()) + logActivity.LogAPICall(extensionId, prefix + "." + apiName, + $Array.slice(arguments)); + return ret; + }); +}; + +APIFunctions.prototype.setUpdateArgumentsPostValidate = + function(apiName, customizedFunction) { + return this.setHook_( + apiName, 'updateArgumentsPostValidate', customizedFunction); +}; + +APIFunctions.prototype.setUpdateArgumentsPreValidate = + function(apiName, customizedFunction) { + return this.setHook_( + apiName, 'updateArgumentsPreValidate', customizedFunction); +}; + +APIFunctions.prototype.setCustomCallback = + function(apiName, customizedFunction) { + return this.setHook_(apiName, 'customCallback', customizedFunction); +}; + +function CustomBindingsObject() { +} + +CustomBindingsObject.prototype.setSchema = function(schema) { + // The functions in the schema are in list form, so we move them into a + // dictionary for easier access. + var self = this; + self.functionSchemas = {}; + $Array.forEach(schema.functions, function(f) { + self.functionSchemas[f.name] = { + name: f.name, + definition: f + } + }); +}; + +// Get the platform from navigator.appVersion. +function getPlatform() { + var platforms = [ + [/CrOS Touch/, "chromeos touch"], + [/CrOS/, "chromeos"], + [/Linux/, "linux"], + [/Mac/, "mac"], + [/Win/, "win"], + ]; + + for (var i = 0; i < platforms.length; i++) { + if ($RegExp.test(platforms[i][0], navigator.appVersion)) { + return platforms[i][1]; + } + } + return "unknown"; +} + +function isPlatformSupported(schemaNode, platform) { + return !schemaNode.platforms || + schemaNode.platforms.indexOf(platform) > -1; +} + +function isManifestVersionSupported(schemaNode, manifestVersion) { + return !schemaNode.maximumManifestVersion || + manifestVersion <= schemaNode.maximumManifestVersion; +} + +function isSchemaNodeSupported(schemaNode, platform, manifestVersion) { + return isPlatformSupported(schemaNode, platform) && + isManifestVersionSupported(schemaNode, manifestVersion); +} + +function createCustomType(type) { + var jsModuleName = type.js_module; + logging.CHECK(jsModuleName, 'Custom type ' + type.id + + ' has no "js_module" property.'); + var jsModule = require(jsModuleName); + logging.CHECK(jsModule, 'No module ' + jsModuleName + ' found for ' + + type.id + '.'); + var customType = jsModule[jsModuleName]; + logging.CHECK(customType, jsModuleName + ' must export itself.'); + customType.prototype = new CustomBindingsObject(); + customType.prototype.setSchema(type); + return customType; +} + +var platform = getPlatform(); + +function Binding(schema) { + this.schema_ = schema; + this.apiFunctions_ = new APIFunctions(schema.namespace); + this.customEvent_ = null; + this.customHooks_ = []; +}; + +Binding.create = function(apiName) { + return new Binding(schemaRegistry.GetSchema(apiName)); +}; + +Binding.prototype = { + // The API through which the ${api_name}_custom_bindings.js files customize + // their API bindings beyond what can be generated. + // + // There are 2 types of customizations available: those which are required in + // order to do the schema generation (registerCustomEvent and + // registerCustomType), and those which can only run after the bindings have + // been generated (registerCustomHook). + + // Registers a custom event type for the API identified by |namespace|. + // |event| is the event's constructor. + registerCustomEvent: function(event) { + this.customEvent_ = event; + }, + + // Registers a function |hook| to run after the schema for all APIs has been + // generated. The hook is passed as its first argument an "API" object to + // interact with, and second the current extension ID. See where + // |customHooks| is used. + registerCustomHook: function(fn) { + $Array.push(this.customHooks_, fn); + }, + + // TODO(kalman/cduvall): Refactor this so |runHooks_| is not needed. + runHooks_: function(api) { + $Array.forEach(this.customHooks_, function(hook) { + if (!isSchemaNodeSupported(this.schema_, platform, manifestVersion)) + return; + + if (!hook) + return; + + hook({ + apiFunctions: this.apiFunctions_, + schema: this.schema_, + compiledApi: api + }, extensionId, contextType); + }, this); + }, + + // Generates the bindings from |this.schema_| and integrates any custom + // bindings that might be present. + generate: function() { + var schema = this.schema_; + + function shouldCheckUnprivileged() { + var shouldCheck = 'unprivileged' in schema; + if (shouldCheck) + return shouldCheck; + + $Array.forEach(['functions', 'events'], function(type) { + if ($Object.hasOwnProperty(schema, type)) { + $Array.forEach(schema[type], function(node) { + if ('unprivileged' in node) + shouldCheck = true; + }); + } + }); + if (shouldCheck) + return shouldCheck; + + for (var property in schema.properties) { + if ($Object.hasOwnProperty(schema, property) && + 'unprivileged' in schema.properties[property]) { + shouldCheck = true; + break; + } + } + return shouldCheck; + } + var checkUnprivileged = shouldCheckUnprivileged(); + + // TODO(kalman/cduvall): Make GetAvailability handle this, then delete the + // supporting code. + if (!isSchemaNodeSupported(schema, platform, manifestVersion)) { + console.error('chrome.' + schema.namespace + ' is not supported on ' + + 'this platform or manifest version'); + return undefined; + } + + var mod = {}; + + var namespaces = $String.split(schema.namespace, '.'); + for (var index = 0, name; name = namespaces[index]; index++) { + mod[name] = mod[name] || {}; + mod = mod[name]; + } + + // Add types to global schemaValidator, the types we depend on from other + // namespaces will be added as needed. + if (schema.types) { + $Array.forEach(schema.types, function(t) { + if (!isSchemaNodeSupported(t, platform, manifestVersion)) + return; + schemaUtils.schemaValidator.addTypes(t); + }, this); + } + + // TODO(cduvall): Take out when all APIs have been converted to features. + // Returns whether access to the content of a schema should be denied, + // based on the presence of "unprivileged" and whether this is an + // extension process (versus e.g. a content script). + function isSchemaAccessAllowed(itemSchema) { + return (contextType == 'BLESSED_EXTENSION') || + schema.unprivileged || + itemSchema.unprivileged; + }; + + // Setup Functions. + if (schema.functions) { + $Array.forEach(schema.functions, function(functionDef) { + if (functionDef.name in mod) { + throw new Error('Function ' + functionDef.name + + ' already defined in ' + schema.namespace); + } + + if (!isSchemaNodeSupported(functionDef, platform, manifestVersion)) { + this.apiFunctions_.registerUnavailable(functionDef.name); + return; + } + + var apiFunction = {}; + apiFunction.definition = functionDef; + apiFunction.name = schema.namespace + '.' + functionDef.name; + + if (!GetAvailability(apiFunction.name).is_available || + (checkUnprivileged && !isSchemaAccessAllowed(functionDef))) { + this.apiFunctions_.registerUnavailable(functionDef.name); + return; + } + + // TODO(aa): It would be best to run this in a unit test, but in order + // to do that we would need to better factor this code so that it + // doesn't depend on so much v8::Extension machinery. + if (logging.DCHECK_IS_ON() && + schemaUtils.isFunctionSignatureAmbiguous(apiFunction.definition)) { + throw new Error( + apiFunction.name + ' has ambiguous optional arguments. ' + + 'To implement custom disambiguation logic, add ' + + '"allowAmbiguousOptionalArguments" to the function\'s schema.'); + } + + this.apiFunctions_.register(functionDef.name, apiFunction); + + mod[functionDef.name] = $Function.bind(function() { + var args = $Array.slice(arguments); + if (this.updateArgumentsPreValidate) + args = $Function.apply(this.updateArgumentsPreValidate, this, args); + + args = schemaUtils.normalizeArgumentsAndValidate(args, this); + if (this.updateArgumentsPostValidate) { + args = $Function.apply(this.updateArgumentsPostValidate, + this, + args); + } + + sendRequestHandler.clearCalledSendRequest(); + + var retval; + if (this.handleRequest) { + retval = $Function.apply(this.handleRequest, this, args); + } else { + var optArgs = { + customCallback: this.customCallback + }; + retval = sendRequest(this.name, args, + this.definition.parameters, + optArgs); + } + sendRequestHandler.clearCalledSendRequest(); + + // Validate return value if in sanity check mode. + if (logging.DCHECK_IS_ON() && this.definition.returns) + schemaUtils.validate([retval], [this.definition.returns]); + return retval; + }, apiFunction); + }, this); + } + + // Setup Events + if (schema.events) { + $Array.forEach(schema.events, function(eventDef) { + if (eventDef.name in mod) { + throw new Error('Event ' + eventDef.name + + ' already defined in ' + schema.namespace); + } + if (!isSchemaNodeSupported(eventDef, platform, manifestVersion)) + return; + + var eventName = schema.namespace + "." + eventDef.name; + if (!GetAvailability(eventName).is_available || + (checkUnprivileged && !isSchemaAccessAllowed(eventDef))) { + return; + } + + var options = eventDef.options || {}; + if (eventDef.filters && eventDef.filters.length > 0) + options.supportsFilters = true; + + var parameters = eventDef.parameters; + if (this.customEvent_) { + mod[eventDef.name] = new this.customEvent_( + eventName, parameters, eventDef.extraParameters, options); + } else { + mod[eventDef.name] = new Event(eventName, parameters, options); + } + }, this); + } + + function addProperties(m, parentDef) { + var properties = parentDef.properties; + if (!properties) + return; + + forEach(properties, function(propertyName, propertyDef) { + if (propertyName in m) + return; // TODO(kalman): be strict like functions/events somehow. + if (!isSchemaNodeSupported(propertyDef, platform, manifestVersion)) + return; + if (!GetAvailability(schema.namespace + "." + + propertyName).is_available || + (checkUnprivileged && !isSchemaAccessAllowed(propertyDef))) { + return; + } + + var value = propertyDef.value; + if (value) { + // Values may just have raw types as defined in the JSON, such + // as "WINDOW_ID_NONE": { "value": -1 }. We handle this here. + // TODO(kalman): enforce that things with a "value" property can't + // define their own types. + var type = propertyDef.type || typeof(value); + if (type === 'integer' || type === 'number') { + value = parseInt(value); + } else if (type === 'boolean') { + value = value === 'true'; + } else if (propertyDef['$ref']) { + var ref = propertyDef['$ref']; + var type = utils.loadTypeSchema(propertyDef['$ref'], schema); + logging.CHECK(type, 'Schema for $ref type ' + ref + ' not found'); + var constructor = createCustomType(type); + var args = value; + // For an object propertyDef, |value| is an array of constructor + // arguments, but we want to pass the arguments directly (i.e. + // not as an array), so we have to fake calling |new| on the + // constructor. + value = { __proto__: constructor.prototype }; + $Function.apply(constructor, value, args); + // Recursively add properties. + addProperties(value, propertyDef); + } else if (type === 'object') { + // Recursively add properties. + addProperties(value, propertyDef); + } else if (type !== 'string') { + throw new Error('NOT IMPLEMENTED (extension_api.json error): ' + + 'Cannot parse values for type "' + type + '"'); + } + m[propertyName] = value; + } + }); + }; + + addProperties(mod, schema); + + var availability = GetAvailability(schema.namespace); + if (!availability.is_available && $Object.keys(mod).length == 0) { + console.error('chrome.' + schema.namespace + ' is not available: ' + + availability.message); + return; + } + + this.runHooks_(mod); + return mod; + } +}; + +exports.Binding = Binding; diff --git a/extensions/renderer/resources/context_menus_custom_bindings.js b/extensions/renderer/resources/context_menus_custom_bindings.js new file mode 100644 index 0000000..71a97a4 --- /dev/null +++ b/extensions/renderer/resources/context_menus_custom_bindings.js @@ -0,0 +1,101 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Custom binding for the contextMenus API. + +var binding = require('binding').Binding.create('contextMenus'); + +var contextMenuNatives = requireNative('context_menus'); +var sendRequest = require('sendRequest').sendRequest; +var Event = require('event_bindings').Event; + +binding.registerCustomHook(function(bindingsAPI) { + var apiFunctions = bindingsAPI.apiFunctions; + + var contextMenus = {}; + contextMenus.generatedIdHandlers = {}; + contextMenus.stringIdHandlers = {}; + var eventName = 'contextMenus'; + contextMenus.event = new Event(eventName); + contextMenus.getIdFromCreateProperties = function(prop) { + if (typeof(prop.id) !== 'undefined') + return prop.id; + return prop.generatedId; + }; + contextMenus.handlersForId = function(id) { + if (typeof(id) === 'number') + return contextMenus.generatedIdHandlers; + return contextMenus.stringIdHandlers; + }; + contextMenus.ensureListenerSetup = function() { + if (contextMenus.listening) { + return; + } + contextMenus.listening = true; + contextMenus.event.addListener(function() { + // An extension context menu item has been clicked on - fire the onclick + // if there is one. + var id = arguments[0].menuItemId; + var onclick = contextMenus.handlersForId(id)[id]; + if (onclick) { + $Function.apply(onclick, null, arguments); + } + }); + }; + + apiFunctions.setHandleRequest('create', function() { + var args = arguments; + var id = contextMenuNatives.GetNextContextMenuId(); + args[0].generatedId = id; + var optArgs = { + customCallback: this.customCallback, + }; + sendRequest(this.name, args, this.definition.parameters, optArgs); + return contextMenus.getIdFromCreateProperties(args[0]); + }); + + apiFunctions.setCustomCallback('create', function(name, request, response) { + if (chrome.runtime.lastError) { + return; + } + + var id = contextMenus.getIdFromCreateProperties(request.args[0]); + + // Set up the onclick handler if we were passed one in the request. + var onclick = request.args.length ? request.args[0].onclick : null; + if (onclick) { + contextMenus.ensureListenerSetup(); + contextMenus.handlersForId(id)[id] = onclick; + } + }); + + apiFunctions.setCustomCallback('remove', function(name, request, response) { + if (chrome.runtime.lastError) { + return; + } + var id = request.args[0]; + delete contextMenus.handlersForId(id)[id]; + }); + + apiFunctions.setCustomCallback('update', function(name, request, response) { + if (chrome.runtime.lastError) { + return; + } + var id = request.args[0]; + if (request.args[1].onclick) { + contextMenus.handlersForId(id)[id] = request.args[1].onclick; + } + }); + + apiFunctions.setCustomCallback('removeAll', + function(name, request, response) { + if (chrome.runtime.lastError) { + return; + } + contextMenus.generatedIdHandlers = {}; + contextMenus.stringIdHandlers = {}; + }); +}); + +exports.binding = binding.generate(); diff --git a/extensions/renderer/resources/entry_id_manager.js b/extensions/renderer/resources/entry_id_manager.js new file mode 100644 index 0000000..9fc2c14 --- /dev/null +++ b/extensions/renderer/resources/entry_id_manager.js @@ -0,0 +1,52 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +var fileSystemNatives = requireNative('file_system_natives'); + +var nameToIds = {}; +var idsToEntries = {}; + +function computeName(entry) { + return entry.filesystem.name + ':' + entry.fullPath; +} + +function computeId(entry) { + var fileSystemId = fileSystemNatives.CrackIsolatedFileSystemName( + entry.filesystem.name); + if (!fileSystemId) + return null; + // Strip the leading '/' from the path. + return fileSystemId + ':' + $String.slice(entry.fullPath, 1); +} + +function registerEntry(id, entry) { + var name = computeName(entry); + nameToIds[name] = id; + idsToEntries[id] = entry; +} + +function getEntryId(entry) { + var name = null; + try { + name = computeName(entry); + } catch(e) { + return null; + } + var id = nameToIds[name]; + if (id != null) + return id; + + // If an entry has not been registered, compute its id and register it. + id = computeId(entry); + registerEntry(id, entry); + return id; +} + +function getEntryById(id) { + return idsToEntries[id]; +} + +exports.registerEntry = registerEntry; +exports.getEntryId = getEntryId; +exports.getEntryById = getEntryById; diff --git a/extensions/renderer/resources/event.js b/extensions/renderer/resources/event.js new file mode 100644 index 0000000..d82b9fb --- /dev/null +++ b/extensions/renderer/resources/event.js @@ -0,0 +1,528 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + + var eventNatives = requireNative('event_natives'); + var handleUncaughtException = require('uncaught_exception_handler').handle; + var logging = requireNative('logging'); + var schemaRegistry = requireNative('schema_registry'); + var sendRequest = require('sendRequest').sendRequest; + var utils = require('utils'); + var validate = require('schemaUtils').validate; + var unloadEvent = require('unload_event'); + + // Schemas for the rule-style functions on the events API that + // only need to be generated occasionally, so populate them lazily. + var ruleFunctionSchemas = { + // These values are set lazily: + // addRules: {}, + // getRules: {}, + // removeRules: {} + }; + + // This function ensures that |ruleFunctionSchemas| is populated. + function ensureRuleSchemasLoaded() { + if (ruleFunctionSchemas.addRules) + return; + var eventsSchema = schemaRegistry.GetSchema("events"); + var eventType = utils.lookup(eventsSchema.types, 'id', 'events.Event'); + + ruleFunctionSchemas.addRules = + utils.lookup(eventType.functions, 'name', 'addRules'); + ruleFunctionSchemas.getRules = + utils.lookup(eventType.functions, 'name', 'getRules'); + ruleFunctionSchemas.removeRules = + utils.lookup(eventType.functions, 'name', 'removeRules'); + } + + // A map of event names to the event object that is registered to that name. + var attachedNamedEvents = {}; + + // An array of all attached event objects, used for detaching on unload. + var allAttachedEvents = []; + + // A map of functions that massage event arguments before they are dispatched. + // Key is event name, value is function. + var eventArgumentMassagers = {}; + + // An attachment strategy for events that aren't attached to the browser. + // This applies to events with the "unmanaged" option and events without + // names. + var NullAttachmentStrategy = function(event) { + this.event_ = event; + }; + NullAttachmentStrategy.prototype.onAddedListener = + function(listener) { + }; + NullAttachmentStrategy.prototype.onRemovedListener = + function(listener) { + }; + NullAttachmentStrategy.prototype.detach = function(manual) { + }; + NullAttachmentStrategy.prototype.getListenersByIDs = function(ids) { + // |ids| is for filtered events only. + return this.event_.listeners; + }; + + // Handles adding/removing/dispatching listeners for unfiltered events. + var UnfilteredAttachmentStrategy = function(event) { + this.event_ = event; + }; + + UnfilteredAttachmentStrategy.prototype.onAddedListener = + function(listener) { + // Only attach / detach on the first / last listener removed. + if (this.event_.listeners.length == 0) + eventNatives.AttachEvent(this.event_.eventName); + }; + + UnfilteredAttachmentStrategy.prototype.onRemovedListener = + function(listener) { + if (this.event_.listeners.length == 0) + this.detach(true); + }; + + UnfilteredAttachmentStrategy.prototype.detach = function(manual) { + eventNatives.DetachEvent(this.event_.eventName, manual); + }; + + UnfilteredAttachmentStrategy.prototype.getListenersByIDs = function(ids) { + // |ids| is for filtered events only. + return this.event_.listeners; + }; + + var FilteredAttachmentStrategy = function(event) { + this.event_ = event; + this.listenerMap_ = {}; + }; + + FilteredAttachmentStrategy.idToEventMap = {}; + + FilteredAttachmentStrategy.prototype.onAddedListener = function(listener) { + var id = eventNatives.AttachFilteredEvent(this.event_.eventName, + listener.filters || {}); + if (id == -1) + throw new Error("Can't add listener"); + listener.id = id; + this.listenerMap_[id] = listener; + FilteredAttachmentStrategy.idToEventMap[id] = this.event_; + }; + + FilteredAttachmentStrategy.prototype.onRemovedListener = function(listener) { + this.detachListener(listener, true); + }; + + FilteredAttachmentStrategy.prototype.detachListener = + function(listener, manual) { + if (listener.id == undefined) + throw new Error("listener.id undefined - '" + listener + "'"); + var id = listener.id; + delete this.listenerMap_[id]; + delete FilteredAttachmentStrategy.idToEventMap[id]; + eventNatives.DetachFilteredEvent(id, manual); + }; + + FilteredAttachmentStrategy.prototype.detach = function(manual) { + for (var i in this.listenerMap_) + this.detachListener(this.listenerMap_[i], manual); + }; + + FilteredAttachmentStrategy.prototype.getListenersByIDs = function(ids) { + var result = []; + for (var i = 0; i < ids.length; i++) + $Array.push(result, this.listenerMap_[ids[i]]); + return result; + }; + + function parseEventOptions(opt_eventOptions) { + function merge(dest, src) { + for (var k in src) { + if (!$Object.hasOwnProperty(dest, k)) { + dest[k] = src[k]; + } + } + } + + var options = opt_eventOptions || {}; + merge(options, { + // Event supports adding listeners with filters ("filtered events"), for + // example as used in the webNavigation API. + // + // event.addListener(listener, [filter1, filter2]); + supportsFilters: false, + + // Events supports vanilla events. Most APIs use these. + // + // event.addListener(listener); + supportsListeners: true, + + // Event supports adding rules ("declarative events") rather than + // listeners, for example as used in the declarativeWebRequest API. + // + // event.addRules([rule1, rule2]); + supportsRules: false, + + // Event is unmanaged in that the browser has no knowledge of its + // existence; it's never invoked, doesn't keep the renderer alive, and + // the bindings system has no knowledge of it. + // + // Both events created by user code (new chrome.Event()) and messaging + // events are unmanaged, though in the latter case the browser *does* + // interact indirectly with them via IPCs written by hand. + unmanaged: false, + }); + return options; + }; + + // Event object. If opt_eventName is provided, this object represents + // the unique instance of that named event, and dispatching an event + // with that name will route through this object's listeners. Note that + // opt_eventName is required for events that support rules. + // + // Example: + // var Event = require('event_bindings').Event; + // chrome.tabs.onChanged = new Event("tab-changed"); + // chrome.tabs.onChanged.addListener(function(data) { alert(data); }); + // Event.dispatch("tab-changed", "hi"); + // will result in an alert dialog that says 'hi'. + // + // If opt_eventOptions exists, it is a dictionary that contains the boolean + // entries "supportsListeners" and "supportsRules". + // If opt_webViewInstanceId exists, it is an integer uniquely identifying a + // <webview> tag within the embedder. If it does not exist, then this is an + // extension event rather than a <webview> event. + var EventImpl = function(opt_eventName, opt_argSchemas, opt_eventOptions, + opt_webViewInstanceId) { + this.eventName = opt_eventName; + this.argSchemas = opt_argSchemas; + this.listeners = []; + this.eventOptions = parseEventOptions(opt_eventOptions); + this.webViewInstanceId = opt_webViewInstanceId || 0; + + if (!this.eventName) { + if (this.eventOptions.supportsRules) + throw new Error("Events that support rules require an event name."); + // Events without names cannot be managed by the browser by definition + // (the browser has no way of identifying them). + this.eventOptions.unmanaged = true; + } + + // Track whether the event has been destroyed to help track down the cause + // of http://crbug.com/258526. + // This variable will eventually hold the stack trace of the destroy call. + // TODO(kalman): Delete this and replace with more sound logic that catches + // when events are used without being *attached*. + this.destroyed = null; + + if (this.eventOptions.unmanaged) + this.attachmentStrategy = new NullAttachmentStrategy(this); + else if (this.eventOptions.supportsFilters) + this.attachmentStrategy = new FilteredAttachmentStrategy(this); + else + this.attachmentStrategy = new UnfilteredAttachmentStrategy(this); + }; + + // callback is a function(args, dispatch). args are the args we receive from + // dispatchEvent(), and dispatch is a function(args) that dispatches args to + // its listeners. + function registerArgumentMassager(name, callback) { + if (eventArgumentMassagers[name]) + throw new Error("Massager already registered for event: " + name); + eventArgumentMassagers[name] = callback; + } + + // Dispatches a named event with the given argument array. The args array is + // the list of arguments that will be sent to the event callback. + function dispatchEvent(name, args, filteringInfo) { + var listenerIDs = []; + + if (filteringInfo) + listenerIDs = eventNatives.MatchAgainstEventFilter(name, filteringInfo); + + var event = attachedNamedEvents[name]; + if (!event) + return; + + var dispatchArgs = function(args) { + var result = event.dispatch_(args, listenerIDs); + if (result) + logging.DCHECK(!result.validationErrors, result.validationErrors); + return result; + }; + + if (eventArgumentMassagers[name]) + eventArgumentMassagers[name](args, dispatchArgs); + else + dispatchArgs(args); + } + + // Registers a callback to be called when this event is dispatched. + EventImpl.prototype.addListener = function(cb, filters) { + if (!this.eventOptions.supportsListeners) + throw new Error("This event does not support listeners."); + if (this.eventOptions.maxListeners && + this.getListenerCount_() >= this.eventOptions.maxListeners) { + throw new Error("Too many listeners for " + this.eventName); + } + if (filters) { + if (!this.eventOptions.supportsFilters) + throw new Error("This event does not support filters."); + if (filters.url && !(filters.url instanceof Array)) + throw new Error("filters.url should be an array."); + if (filters.serviceType && + !(typeof filters.serviceType === 'string')) { + throw new Error("filters.serviceType should be a string.") + } + } + var listener = {callback: cb, filters: filters}; + this.attach_(listener); + $Array.push(this.listeners, listener); + }; + + EventImpl.prototype.attach_ = function(listener) { + this.attachmentStrategy.onAddedListener(listener); + + if (this.listeners.length == 0) { + allAttachedEvents[allAttachedEvents.length] = this; + if (this.eventName) { + if (attachedNamedEvents[this.eventName]) { + throw new Error("Event '" + this.eventName + + "' is already attached."); + } + attachedNamedEvents[this.eventName] = this; + } + } + }; + + // Unregisters a callback. + EventImpl.prototype.removeListener = function(cb) { + if (!this.eventOptions.supportsListeners) + throw new Error("This event does not support listeners."); + + var idx = this.findListener_(cb); + if (idx == -1) + return; + + var removedListener = $Array.splice(this.listeners, idx, 1)[0]; + this.attachmentStrategy.onRemovedListener(removedListener); + + if (this.listeners.length == 0) { + var i = $Array.indexOf(allAttachedEvents, this); + if (i >= 0) + delete allAttachedEvents[i]; + if (this.eventName) { + if (!attachedNamedEvents[this.eventName]) { + throw new Error( + "Event '" + this.eventName + "' is not attached."); + } + delete attachedNamedEvents[this.eventName]; + } + } + }; + + // Test if the given callback is registered for this event. + EventImpl.prototype.hasListener = function(cb) { + if (!this.eventOptions.supportsListeners) + throw new Error("This event does not support listeners."); + return this.findListener_(cb) > -1; + }; + + // Test if any callbacks are registered for this event. + EventImpl.prototype.hasListeners = function() { + return this.getListenerCount_() > 0; + }; + + // Returns the number of listeners on this event. + EventImpl.prototype.getListenerCount_ = function() { + if (!this.eventOptions.supportsListeners) + throw new Error("This event does not support listeners."); + return this.listeners.length; + }; + + // Returns the index of the given callback if registered, or -1 if not + // found. + EventImpl.prototype.findListener_ = function(cb) { + for (var i = 0; i < this.listeners.length; i++) { + if (this.listeners[i].callback == cb) { + return i; + } + } + + return -1; + }; + + EventImpl.prototype.dispatch_ = function(args, listenerIDs) { + if (this.destroyed) { + throw new Error(this.eventName + ' was already destroyed at: ' + + this.destroyed); + } + if (!this.eventOptions.supportsListeners) + throw new Error("This event does not support listeners."); + + if (this.argSchemas && logging.DCHECK_IS_ON()) { + try { + validate(args, this.argSchemas); + } catch (e) { + e.message += ' in ' + this.eventName; + throw e; + } + } + + // Make a copy of the listeners in case the listener list is modified + // while dispatching the event. + var listeners = $Array.slice( + this.attachmentStrategy.getListenersByIDs(listenerIDs)); + + var results = []; + for (var i = 0; i < listeners.length; i++) { + try { + var result = this.wrapper.dispatchToListener(listeners[i].callback, + args); + if (result !== undefined) + $Array.push(results, result); + } catch (e) { + handleUncaughtException( + 'Error in event handler for ' + + (this.eventName ? this.eventName : '(unknown)') + + ': ' + e.message + '\nStack trace: ' + e.stack, + e); + } + } + if (results.length) + return {results: results}; + } + + // Can be overridden to support custom dispatching. + EventImpl.prototype.dispatchToListener = function(callback, args) { + return $Function.apply(callback, null, args); + } + + // Dispatches this event object to all listeners, passing all supplied + // arguments to this function each listener. + EventImpl.prototype.dispatch = function(varargs) { + return this.dispatch_($Array.slice(arguments), undefined); + }; + + // Detaches this event object from its name. + EventImpl.prototype.detach_ = function() { + this.attachmentStrategy.detach(false); + }; + + EventImpl.prototype.destroy_ = function() { + this.listeners.length = 0; + this.detach_(); + this.destroyed = new Error().stack; + }; + + EventImpl.prototype.addRules = function(rules, opt_cb) { + if (!this.eventOptions.supportsRules) + throw new Error("This event does not support rules."); + + // Takes a list of JSON datatype identifiers and returns a schema fragment + // that verifies that a JSON object corresponds to an array of only these + // data types. + function buildArrayOfChoicesSchema(typesList) { + return { + 'type': 'array', + 'items': { + 'choices': typesList.map(function(el) {return {'$ref': el};}) + } + }; + }; + + // Validate conditions and actions against specific schemas of this + // event object type. + // |rules| is an array of JSON objects that follow the Rule type of the + // declarative extension APIs. |conditions| is an array of JSON type + // identifiers that are allowed to occur in the conditions attribute of each + // rule. Likewise, |actions| is an array of JSON type identifiers that are + // allowed to occur in the actions attribute of each rule. + function validateRules(rules, conditions, actions) { + var conditionsSchema = buildArrayOfChoicesSchema(conditions); + var actionsSchema = buildArrayOfChoicesSchema(actions); + $Array.forEach(rules, function(rule) { + validate([rule.conditions], [conditionsSchema]); + validate([rule.actions], [actionsSchema]); + }); + }; + + if (!this.eventOptions.conditions || !this.eventOptions.actions) { + throw new Error('Event ' + this.eventName + ' misses ' + + 'conditions or actions in the API specification.'); + } + + validateRules(rules, + this.eventOptions.conditions, + this.eventOptions.actions); + + ensureRuleSchemasLoaded(); + // We remove the first parameter from the validation to give the user more + // meaningful error messages. + validate([this.webViewInstanceId, rules, opt_cb], + $Array.splice( + $Array.slice(ruleFunctionSchemas.addRules.parameters), 1)); + sendRequest( + "events.addRules", + [this.eventName, this.webViewInstanceId, rules, opt_cb], + ruleFunctionSchemas.addRules.parameters); + } + + EventImpl.prototype.removeRules = function(ruleIdentifiers, opt_cb) { + if (!this.eventOptions.supportsRules) + throw new Error("This event does not support rules."); + ensureRuleSchemasLoaded(); + // We remove the first parameter from the validation to give the user more + // meaningful error messages. + validate([this.webViewInstanceId, ruleIdentifiers, opt_cb], + $Array.splice( + $Array.slice(ruleFunctionSchemas.removeRules.parameters), 1)); + sendRequest("events.removeRules", + [this.eventName, + this.webViewInstanceId, + ruleIdentifiers, + opt_cb], + ruleFunctionSchemas.removeRules.parameters); + } + + EventImpl.prototype.getRules = function(ruleIdentifiers, cb) { + if (!this.eventOptions.supportsRules) + throw new Error("This event does not support rules."); + ensureRuleSchemasLoaded(); + // We remove the first parameter from the validation to give the user more + // meaningful error messages. + validate([this.webViewInstanceId, ruleIdentifiers, cb], + $Array.splice( + $Array.slice(ruleFunctionSchemas.getRules.parameters), 1)); + + sendRequest( + "events.getRules", + [this.eventName, this.webViewInstanceId, ruleIdentifiers, cb], + ruleFunctionSchemas.getRules.parameters); + } + + unloadEvent.addListener(function() { + for (var i = 0; i < allAttachedEvents.length; ++i) { + var event = allAttachedEvents[i]; + if (event) + event.detach_(); + } + }); + + var Event = utils.expose('Event', EventImpl, { functions: [ + 'addListener', + 'removeListener', + 'hasListener', + 'hasListeners', + 'dispatchToListener', + 'dispatch', + 'addRules', + 'removeRules', + 'getRules' + ] }); + + // NOTE: Event is (lazily) exposed as chrome.Event from dispatcher.cc. + exports.Event = Event; + + exports.dispatchEvent = dispatchEvent; + exports.parseEventOptions = parseEventOptions; + exports.registerArgumentMassager = registerArgumentMassager; diff --git a/extensions/renderer/resources/extension_custom_bindings.js b/extensions/renderer/resources/extension_custom_bindings.js new file mode 100644 index 0000000..d114f52 --- /dev/null +++ b/extensions/renderer/resources/extension_custom_bindings.js @@ -0,0 +1,113 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Custom binding for the extension API. + +var binding = require('binding').Binding.create('extension'); + +var messaging = require('messaging'); +var runtimeNatives = requireNative('runtime'); +var GetExtensionViews = runtimeNatives.GetExtensionViews; +var OpenChannelToExtension = runtimeNatives.OpenChannelToExtension; +var OpenChannelToNativeApp = runtimeNatives.OpenChannelToNativeApp; +var chrome = requireNative('chrome').GetChrome(); + +var inIncognitoContext = requireNative('process').InIncognitoContext(); +var sendRequestIsDisabled = requireNative('process').IsSendRequestDisabled(); +var contextType = requireNative('process').GetContextType(); +var manifestVersion = requireNative('process').GetManifestVersion(); + +// This should match chrome.windows.WINDOW_ID_NONE. +// +// We can't use chrome.windows.WINDOW_ID_NONE directly because the +// chrome.windows API won't exist unless this extension has permission for it; +// which may not be the case. +var WINDOW_ID_NONE = -1; + +binding.registerCustomHook(function(bindingsAPI, extensionId) { + var extension = bindingsAPI.compiledApi; + if (manifestVersion < 2) { + chrome.self = extension; + extension.inIncognitoTab = inIncognitoContext; + } + extension.inIncognitoContext = inIncognitoContext; + + var apiFunctions = bindingsAPI.apiFunctions; + + apiFunctions.setHandleRequest('getViews', function(properties) { + var windowId = WINDOW_ID_NONE; + var type = 'ALL'; + if (properties) { + if (properties.type != null) { + type = properties.type; + } + if (properties.windowId != null) { + windowId = properties.windowId; + } + } + return GetExtensionViews(windowId, type); + }); + + apiFunctions.setHandleRequest('getBackgroundPage', function() { + return GetExtensionViews(-1, 'BACKGROUND')[0] || null; + }); + + apiFunctions.setHandleRequest('getExtensionTabs', function(windowId) { + if (windowId == null) + windowId = WINDOW_ID_NONE; + return GetExtensionViews(windowId, 'TAB'); + }); + + apiFunctions.setHandleRequest('getURL', function(path) { + path = String(path); + if (!path.length || path[0] != '/') + path = '/' + path; + return 'chrome-extension://' + extensionId + path; + }); + + // Alias several messaging deprecated APIs to their runtime counterparts. + var mayNeedAlias = [ + // Types + 'Port', + // Functions + 'connect', 'sendMessage', 'connectNative', 'sendNativeMessage', + // Events + 'onConnect', 'onConnectExternal', 'onMessage', 'onMessageExternal' + ]; + $Array.forEach(mayNeedAlias, function(alias) { + // Checking existence isn't enough since some functions are disabled via + // getters that throw exceptions. Assume that any getter is such a function. + if (chrome.runtime && + $Object.hasOwnProperty(chrome.runtime, alias) && + chrome.runtime.__lookupGetter__(alias) === undefined) { + extension[alias] = chrome.runtime[alias]; + } + }); + + apiFunctions.setUpdateArgumentsPreValidate('sendRequest', + $Function.bind(messaging.sendMessageUpdateArguments, + null, 'sendRequest', false /* hasOptionsArgument */)); + + apiFunctions.setHandleRequest('sendRequest', + function(targetId, request, responseCallback) { + if (sendRequestIsDisabled) + throw new Error(sendRequestIsDisabled); + var port = chrome.runtime.connect(targetId || extensionId, + {name: messaging.kRequestChannel}); + messaging.sendMessageImpl(port, request, responseCallback); + }); + + if (sendRequestIsDisabled) { + extension.onRequest.addListener = function() { + throw new Error(sendRequestIsDisabled); + }; + if (contextType == 'BLESSED_EXTENSION') { + extension.onRequestExternal.addListener = function() { + throw new Error(sendRequestIsDisabled); + }; + } + } +}); + +exports.binding = binding.generate(); diff --git a/extensions/renderer/resources/extensions_renderer_resources.grd b/extensions/renderer/resources/extensions_renderer_resources.grd new file mode 100644 index 0000000..4cf03e4b --- /dev/null +++ b/extensions/renderer/resources/extensions_renderer_resources.grd @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<grit latest_public_release="0" current_release="1"> + <outputs> + <output filename="grit/extensions_renderer_resources.h" type="rc_header"> + <emit emit_type='prepend'></emit> + </output> + <output filename="extensions_renderer_resources.pak" type="data_package" /> + </outputs> + <release seq="1"> + <includes> + <!-- Extension libraries. --> + <include name="IDR_ENTRY_ID_MANAGER" file="entry_id_manager.js" type="BINDATA" /> + <include name="IDR_EVENT_BINDINGS_JS" file="event.js" type="BINDATA" /> + <include name="IDR_IMAGE_UTIL_JS" file="image_util.js" type="BINDATA" /> + <include name="IDR_JSON_SCHEMA_JS" file="json_schema.js" type="BINDATA" /> + <include name="IDR_LAST_ERROR_JS" file="last_error.js" type="BINDATA" /> + <include name="IDR_MESSAGING_JS" file="messaging.js" type="BINDATA" /> + <include name="IDR_MESSAGING_UTILS_JS" file="messaging_utils.js" type="BINDATA" /> + <include name="IDR_SCHEMA_UTILS_JS" file="schema_utils.js" type="BINDATA" /> + <include name="IDR_SEND_REQUEST_JS" file="send_request.js" type="BINDATA" /> + <include name="IDR_SET_ICON_JS" file="set_icon.js" type="BINDATA" /> + <include name="IDR_TEST_CUSTOM_BINDINGS_JS" file="test_custom_bindings.js" type="BINDATA" /> + <include name="IDR_UNCAUGHT_EXCEPTION_HANDLER_JS" file="uncaught_exception_handler.js" type="BINDATA" /> + <include name="IDR_UNLOAD_EVENT_JS" file="unload_event.js" type="BINDATA" /> + <include name="IDR_UTILS_JS" file="utils.js" type="BINDATA" /> + + <!-- Custom bindings for APIs. --> + <include name="IDR_APP_RUNTIME_CUSTOM_BINDINGS_JS" file="app_runtime_custom_bindings.js" type="BINDATA" /> + <include name="IDR_CONTEXT_MENUS_CUSTOM_BINDINGS_JS" file="context_menus_custom_bindings.js" type="BINDATA" /> + <include name="IDR_EXTENSION_CUSTOM_BINDINGS_JS" file="extension_custom_bindings.js" type="BINDATA" /> + <include name="IDR_GREASEMONKEY_API_JS" file="greasemonkey_api.js" type="BINDATA" /> + <include name="IDR_I18N_CUSTOM_BINDINGS_JS" file="i18n_custom_bindings.js" type="BINDATA" /> + <include name="IDR_PERMISSIONS_CUSTOM_BINDINGS_JS" file="permissions_custom_bindings.js" type="BINDATA" /> + <include name="IDR_RUNTIME_CUSTOM_BINDINGS_JS" file="runtime_custom_bindings.js" type="BINDATA" /> + <include name="IDR_BINDING_JS" file="binding.js" type="BINDATA" /> + + <!-- Custom types for APIs. --> + <include name="IDR_STORAGE_AREA_JS" file="storage_area.js" type="BINDATA" /> + + <!-- Platform app support. --> + <include name="IDR_PLATFORM_APP_CSS" file="platform_app.css" type="BINDATA" /> + <include name="IDR_PLATFORM_APP_JS" file="platform_app.js" type="BINDATA" /> + </includes> + </release> +</grit> diff --git a/extensions/renderer/resources/greasemonkey_api.js b/extensions/renderer/resources/greasemonkey_api.js new file mode 100644 index 0000000..bc09911 --- /dev/null +++ b/extensions/renderer/resources/greasemonkey_api.js @@ -0,0 +1,82 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ----------------------------------------------------------------------------- +// NOTE: If you change this file you need to touch renderer_resources.grd to +// have your change take effect. +// ----------------------------------------------------------------------------- + +// Partial implementation of the Greasemonkey API, see: +// http://wiki.greasespot.net/Greasemonkey_Manual:APIs + +function GM_addStyle(css) { + var parent = document.getElementsByTagName("head")[0]; + if (!parent) { + parent = document.documentElement; + } + var style = document.createElement("style"); + style.type = "text/css"; + var textNode = document.createTextNode(css); + style.appendChild(textNode); + parent.appendChild(style); +} + +function GM_xmlhttpRequest(details) { + function setupEvent(xhr, url, eventName, callback) { + xhr[eventName] = function () { + var isComplete = xhr.readyState == 4; + var responseState = { + responseText: xhr.responseText, + readyState: xhr.readyState, + responseHeaders: isComplete ? xhr.getAllResponseHeaders() : "", + status: isComplete ? xhr.status : 0, + statusText: isComplete ? xhr.statusText : "", + finalUrl: isComplete ? url : "" + }; + callback(responseState); + }; + } + + var xhr = new XMLHttpRequest(); + var eventNames = ["onload", "onerror", "onreadystatechange"]; + for (var i = 0; i < eventNames.length; i++ ) { + var eventName = eventNames[i]; + if (eventName in details) { + setupEvent(xhr, details.url, eventName, details[eventName]); + } + } + + xhr.open(details.method, details.url); + + if (details.overrideMimeType) { + xhr.overrideMimeType(details.overrideMimeType); + } + if (details.headers) { + for (var header in details.headers) { + xhr.setRequestHeader(header, details.headers[header]); + } + } + xhr.send(details.data ? details.data : null); +} + +function GM_openInTab(url) { + window.open(url, ""); +} + +function GM_log(message) { + window.console.log(message); +} + +(function() { + function generateGreasemonkeyStub(name) { + return function() { + console.log("%s is not supported.", name); + }; + } + + var apis = ["GM_getValue", "GM_setValue", "GM_registerMenuCommand"]; + for (var i = 0, api; api = apis[i]; i++) { + window[api] = generateGreasemonkeyStub(api); + } +})(); diff --git a/extensions/renderer/resources/i18n_custom_bindings.js b/extensions/renderer/resources/i18n_custom_bindings.js new file mode 100644 index 0000000..38570f3 --- /dev/null +++ b/extensions/renderer/resources/i18n_custom_bindings.js @@ -0,0 +1,41 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Custom binding for the i18n API. + +var binding = require('binding').Binding.create('i18n'); + +var i18nNatives = requireNative('i18n'); +var GetL10nMessage = i18nNatives.GetL10nMessage; +var GetL10nUILanguage = i18nNatives.GetL10nUILanguage; + +binding.registerCustomHook(function(bindingsAPI, extensionId) { + var apiFunctions = bindingsAPI.apiFunctions; + + apiFunctions.setUpdateArgumentsPreValidate('getMessage', function() { + var args = $Array.slice(arguments); + + // The first argument is the message, and should be a string. + var message = args[0]; + if (typeof(message) !== 'string') { + console.warn(extensionId + ': the first argument to getMessage should ' + + 'be type "string", was ' + message + + ' (type "' + typeof(message) + '")'); + args[0] = String(message); + } + + return args; + }); + + apiFunctions.setHandleRequest('getMessage', + function(messageName, substitutions) { + return GetL10nMessage(messageName, substitutions, extensionId); + }); + + apiFunctions.setHandleRequest('getUILanguage', function() { + return GetL10nUILanguage(); + }); +}); + +exports.binding = binding.generate(); diff --git a/extensions/renderer/resources/image_util.js b/extensions/renderer/resources/image_util.js new file mode 100644 index 0000000..b6cd1b1 --- /dev/null +++ b/extensions/renderer/resources/image_util.js @@ -0,0 +1,82 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This function takes an object |imageSpec| with the key |path| - +// corresponding to the internet URL to be translated - and optionally +// |width| and |height| which are the maximum dimensions to be used when +// converting the image. +function loadImageData(imageSpec, callbacks) { + var path = imageSpec.path; + var img = new Image(); + if (typeof callbacks.onerror === 'function') { + img.onerror = function() { + callbacks.onerror({ problem: 'could_not_load', path: path }); + }; + } + img.onload = function() { + var canvas = document.createElement('canvas'); + + if (img.width <= 0 || img.height <= 0) { + callbacks.onerror({ problem: 'image_size_invalid', path: path}); + return; + } + + var scaleFactor = 1; + if (imageSpec.width && imageSpec.width < img.width) + scaleFactor = imageSpec.width / img.width; + + if (imageSpec.height && imageSpec.height < img.height) { + var heightScale = imageSpec.height / img.height; + if (heightScale < scaleFactor) + scaleFactor = heightScale; + } + + canvas.width = img.width * scaleFactor; + canvas.height = img.height * scaleFactor; + + var canvas_context = canvas.getContext('2d'); + canvas_context.clearRect(0, 0, canvas.width, canvas.height); + canvas_context.drawImage(img, 0, 0, canvas.width, canvas.height); + try { + var imageData = canvas_context.getImageData( + 0, 0, canvas.width, canvas.height); + if (typeof callbacks.oncomplete === 'function') { + callbacks.oncomplete( + imageData.width, imageData.height, imageData.data.buffer); + } + } catch (e) { + if (typeof callbacks.onerror === 'function') { + callbacks.onerror({ problem: 'data_url_unavailable', path: path }); + } + } + } + img.src = path; +} + +function on_complete_index(index, err, loading, finished, callbacks) { + return function(width, height, imageData) { + delete loading[index]; + finished[index] = { width: width, height: height, data: imageData }; + if (err) + callbacks.onerror(index); + if ($Object.keys(loading).length == 0) + callbacks.oncomplete(finished); + } +} + +function loadAllImages(imageSpecs, callbacks) { + var loading = {}, finished = [], + index, pathname; + + for (var index = 0; index < imageSpecs.length; index++) { + loading[index] = imageSpecs[index]; + loadImageData(imageSpecs[index], { + oncomplete: on_complete_index(index, false, loading, finished, callbacks), + onerror: on_complete_index(index, true, loading, finished, callbacks) + }); + } +} + +exports.loadImageData = loadImageData; +exports.loadAllImages = loadAllImages; diff --git a/extensions/renderer/resources/json_schema.js b/extensions/renderer/resources/json_schema.js new file mode 100644 index 0000000..67c30c9 --- /dev/null +++ b/extensions/renderer/resources/json_schema.js @@ -0,0 +1,525 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ----------------------------------------------------------------------------- +// NOTE: If you change this file you need to touch +// extension_renderer_resources.grd to have your change take effect. +// ----------------------------------------------------------------------------- + +//============================================================================== +// This file contains a class that implements a subset of JSON Schema. +// See: http://www.json.com/json-schema-proposal/ for more details. +// +// The following features of JSON Schema are not implemented: +// - requires +// - unique +// - disallow +// - union types (but replaced with 'choices') +// +// The following properties are not applicable to the interface exposed by +// this class: +// - options +// - readonly +// - title +// - description +// - format +// - default +// - transient +// - hidden +// +// There are also these departures from the JSON Schema proposal: +// - function and undefined types are supported +// - null counts as 'unspecified' for optional values +// - added the 'choices' property, to allow specifying a list of possible types +// for a value +// - by default an "object" typed schema does not allow additional properties. +// if present, "additionalProperties" is to be a schema against which all +// additional properties will be validated. +//============================================================================== + +var loadTypeSchema = require('utils').loadTypeSchema; +var CHECK = requireNative('logging').CHECK; + +function isInstanceOfClass(instance, className) { + while ((instance = instance.__proto__)) { + if (instance.constructor.name == className) + return true; + } + return false; +} + +function isOptionalValue(value) { + return typeof(value) === 'undefined' || value === null; +} + +function enumToString(enumValue) { + if (enumValue.name === undefined) + return enumValue; + + return enumValue.name; +} + +/** + * Validates an instance against a schema and accumulates errors. Usage: + * + * var validator = new JSONSchemaValidator(); + * validator.validate(inst, schema); + * if (validator.errors.length == 0) + * console.log("Valid!"); + * else + * console.log(validator.errors); + * + * The errors property contains a list of objects. Each object has two + * properties: "path" and "message". The "path" property contains the path to + * the key that had the problem, and the "message" property contains a sentence + * describing the error. + */ +function JSONSchemaValidator() { + this.errors = []; + this.types = []; +} + +JSONSchemaValidator.messages = { + invalidEnum: "Value must be one of: [*].", + propertyRequired: "Property is required.", + unexpectedProperty: "Unexpected property.", + arrayMinItems: "Array must have at least * items.", + arrayMaxItems: "Array must not have more than * items.", + itemRequired: "Item is required.", + stringMinLength: "String must be at least * characters long.", + stringMaxLength: "String must not be more than * characters long.", + stringPattern: "String must match the pattern: *.", + numberFiniteNotNan: "Value must not be *.", + numberMinValue: "Value must not be less than *.", + numberMaxValue: "Value must not be greater than *.", + numberIntValue: "Value must fit in a 32-bit signed integer.", + numberMaxDecimal: "Value must not have more than * decimal places.", + invalidType: "Expected '*' but got '*'.", + invalidTypeIntegerNumber: + "Expected 'integer' but got 'number', consider using Math.round().", + invalidChoice: "Value does not match any valid type choices.", + invalidPropertyType: "Missing property type.", + schemaRequired: "Schema value required.", + unknownSchemaReference: "Unknown schema reference: *.", + notInstance: "Object must be an instance of *." +}; + +/** + * Builds an error message. Key is the property in the |errors| object, and + * |opt_replacements| is an array of values to replace "*" characters with. + */ +JSONSchemaValidator.formatError = function(key, opt_replacements) { + var message = this.messages[key]; + if (opt_replacements) { + for (var i = 0; i < opt_replacements.length; i++) { + message = message.replace("*", opt_replacements[i]); + } + } + return message; +}; + +/** + * Classifies a value as one of the JSON schema primitive types. Note that we + * don't explicitly disallow 'function', because we want to allow functions in + * the input values. + */ +JSONSchemaValidator.getType = function(value) { + var s = typeof value; + + if (s == "object") { + if (value === null) { + return "null"; + } else if (Object.prototype.toString.call(value) == "[object Array]") { + return "array"; + } else if (typeof(ArrayBuffer) != "undefined" && + value.constructor == ArrayBuffer) { + return "binary"; + } + } else if (s == "number") { + if (value % 1 == 0) { + return "integer"; + } + } + + return s; +}; + +/** + * Add types that may be referenced by validated schemas that reference them + * with "$ref": <typeId>. Each type must be a valid schema and define an + * "id" property. + */ +JSONSchemaValidator.prototype.addTypes = function(typeOrTypeList) { + function addType(validator, type) { + if (!type.id) + throw new Error("Attempt to addType with missing 'id' property"); + validator.types[type.id] = type; + } + + if (typeOrTypeList instanceof Array) { + for (var i = 0; i < typeOrTypeList.length; i++) { + addType(this, typeOrTypeList[i]); + } + } else { + addType(this, typeOrTypeList); + } +} + +/** + * Returns a list of strings of the types that this schema accepts. + */ +JSONSchemaValidator.prototype.getAllTypesForSchema = function(schema) { + var schemaTypes = []; + if (schema.type) + $Array.push(schemaTypes, schema.type); + if (schema.choices) { + for (var i = 0; i < schema.choices.length; i++) { + var choiceTypes = this.getAllTypesForSchema(schema.choices[i]); + schemaTypes = $Array.concat(schemaTypes, choiceTypes); + } + } + var ref = schema['$ref']; + if (ref) { + var type = this.getOrAddType(ref); + CHECK(type, 'Could not find type ' + ref); + schemaTypes = $Array.concat(schemaTypes, this.getAllTypesForSchema(type)); + } + return schemaTypes; +}; + +JSONSchemaValidator.prototype.getOrAddType = function(typeName) { + if (!this.types[typeName]) + this.types[typeName] = loadTypeSchema(typeName); + return this.types[typeName]; +}; + +/** + * Returns true if |schema| would accept an argument of type |type|. + */ +JSONSchemaValidator.prototype.isValidSchemaType = function(type, schema) { + if (type == 'any') + return true; + + // TODO(kalman): I don't understand this code. How can type be "null"? + if (schema.optional && (type == "null" || type == "undefined")) + return true; + + var schemaTypes = this.getAllTypesForSchema(schema); + for (var i = 0; i < schemaTypes.length; i++) { + if (schemaTypes[i] == "any" || type == schemaTypes[i] || + (type == "integer" && schemaTypes[i] == "number")) + return true; + } + + return false; +}; + +/** + * Returns true if there is a non-null argument that both |schema1| and + * |schema2| would accept. + */ +JSONSchemaValidator.prototype.checkSchemaOverlap = function(schema1, schema2) { + var schema1Types = this.getAllTypesForSchema(schema1); + for (var i = 0; i < schema1Types.length; i++) { + if (this.isValidSchemaType(schema1Types[i], schema2)) + return true; + } + return false; +}; + +/** + * Validates an instance against a schema. The instance can be any JavaScript + * value and will be validated recursively. When this method returns, the + * |errors| property will contain a list of errors, if any. + */ +JSONSchemaValidator.prototype.validate = function(instance, schema, opt_path) { + var path = opt_path || ""; + + if (!schema) { + this.addError(path, "schemaRequired"); + return; + } + + // If this schema defines itself as reference type, save it in this.types. + if (schema.id) + this.types[schema.id] = schema; + + // If the schema has an extends property, the instance must validate against + // that schema too. + if (schema.extends) + this.validate(instance, schema.extends, path); + + // If the schema has a $ref property, the instance must validate against + // that schema too. It must be present in this.types to be referenced. + var ref = schema["$ref"]; + if (ref) { + if (!this.getOrAddType(ref)) + this.addError(path, "unknownSchemaReference", [ ref ]); + else + this.validate(instance, this.getOrAddType(ref), path) + } + + // If the schema has a choices property, the instance must validate against at + // least one of the items in that array. + if (schema.choices) { + this.validateChoices(instance, schema, path); + return; + } + + // If the schema has an enum property, the instance must be one of those + // values. + if (schema.enum) { + if (!this.validateEnum(instance, schema, path)) + return; + } + + if (schema.type && schema.type != "any") { + if (!this.validateType(instance, schema, path)) + return; + + // Type-specific validation. + switch (schema.type) { + case "object": + this.validateObject(instance, schema, path); + break; + case "array": + this.validateArray(instance, schema, path); + break; + case "string": + this.validateString(instance, schema, path); + break; + case "number": + case "integer": + this.validateNumber(instance, schema, path); + break; + } + } +}; + +/** + * Validates an instance against a choices schema. The instance must match at + * least one of the provided choices. + */ +JSONSchemaValidator.prototype.validateChoices = + function(instance, schema, path) { + var originalErrors = this.errors; + + for (var i = 0; i < schema.choices.length; i++) { + this.errors = []; + this.validate(instance, schema.choices[i], path); + if (this.errors.length == 0) { + this.errors = originalErrors; + return; + } + } + + this.errors = originalErrors; + this.addError(path, "invalidChoice"); +}; + +/** + * Validates an instance against a schema with an enum type. Populates the + * |errors| property, and returns a boolean indicating whether the instance + * validates. + */ +JSONSchemaValidator.prototype.validateEnum = function(instance, schema, path) { + for (var i = 0; i < schema.enum.length; i++) { + if (instance === enumToString(schema.enum[i])) + return true; + } + + this.addError(path, "invalidEnum", + [schema.enum.map(enumToString).join(", ")]); + return false; +}; + +/** + * Validates an instance against an object schema and populates the errors + * property. + */ +JSONSchemaValidator.prototype.validateObject = + function(instance, schema, path) { + if (schema.properties) { + for (var prop in schema.properties) { + // It is common in JavaScript to add properties to Object.prototype. This + // check prevents such additions from being interpreted as required + // schema properties. + // TODO(aa): If it ever turns out that we actually want this to work, + // there are other checks we could put here, like requiring that schema + // properties be objects that have a 'type' property. + if (!$Object.hasOwnProperty(schema.properties, prop)) + continue; + + var propPath = path ? path + "." + prop : prop; + if (schema.properties[prop] == undefined) { + this.addError(propPath, "invalidPropertyType"); + } else if (prop in instance && !isOptionalValue(instance[prop])) { + this.validate(instance[prop], schema.properties[prop], propPath); + } else if (!schema.properties[prop].optional) { + this.addError(propPath, "propertyRequired"); + } + } + } + + // If "instanceof" property is set, check that this object inherits from + // the specified constructor (function). + if (schema.isInstanceOf) { + if (!isInstanceOfClass(instance, schema.isInstanceOf)) + this.addError(propPath, "notInstance", [schema.isInstanceOf]); + } + + // Exit early from additional property check if "type":"any" is defined. + if (schema.additionalProperties && + schema.additionalProperties.type && + schema.additionalProperties.type == "any") { + return; + } + + // By default, additional properties are not allowed on instance objects. This + // can be overridden by setting the additionalProperties property to a schema + // which any additional properties must validate against. + for (var prop in instance) { + if (schema.properties && prop in schema.properties) + continue; + + // Any properties inherited through the prototype are ignored. + if (!$Object.hasOwnProperty(instance, prop)) + continue; + + var propPath = path ? path + "." + prop : prop; + if (schema.additionalProperties) + this.validate(instance[prop], schema.additionalProperties, propPath); + else + this.addError(propPath, "unexpectedProperty"); + } +}; + +/** + * Validates an instance against an array schema and populates the errors + * property. + */ +JSONSchemaValidator.prototype.validateArray = function(instance, schema, path) { + var typeOfItems = JSONSchemaValidator.getType(schema.items); + + if (typeOfItems == 'object') { + if (schema.minItems && instance.length < schema.minItems) { + this.addError(path, "arrayMinItems", [schema.minItems]); + } + + if (typeof schema.maxItems != "undefined" && + instance.length > schema.maxItems) { + this.addError(path, "arrayMaxItems", [schema.maxItems]); + } + + // If the items property is a single schema, each item in the array must + // have that schema. + for (var i = 0; i < instance.length; i++) { + this.validate(instance[i], schema.items, path + "." + i); + } + } else if (typeOfItems == 'array') { + // If the items property is an array of schemas, each item in the array must + // validate against the corresponding schema. + for (var i = 0; i < schema.items.length; i++) { + var itemPath = path ? path + "." + i : String(i); + if (i in instance && !isOptionalValue(instance[i])) { + this.validate(instance[i], schema.items[i], itemPath); + } else if (!schema.items[i].optional) { + this.addError(itemPath, "itemRequired"); + } + } + + if (schema.additionalProperties) { + for (var i = schema.items.length; i < instance.length; i++) { + var itemPath = path ? path + "." + i : String(i); + this.validate(instance[i], schema.additionalProperties, itemPath); + } + } else { + if (instance.length > schema.items.length) { + this.addError(path, "arrayMaxItems", [schema.items.length]); + } + } + } +}; + +/** + * Validates a string and populates the errors property. + */ +JSONSchemaValidator.prototype.validateString = + function(instance, schema, path) { + if (schema.minLength && instance.length < schema.minLength) + this.addError(path, "stringMinLength", [schema.minLength]); + + if (schema.maxLength && instance.length > schema.maxLength) + this.addError(path, "stringMaxLength", [schema.maxLength]); + + if (schema.pattern && !schema.pattern.test(instance)) + this.addError(path, "stringPattern", [schema.pattern]); +}; + +/** + * Validates a number and populates the errors property. The instance is + * assumed to be a number. + */ +JSONSchemaValidator.prototype.validateNumber = + function(instance, schema, path) { + // Forbid NaN, +Infinity, and -Infinity. Our APIs don't use them, and + // JSON serialization encodes them as 'null'. Re-evaluate supporting + // them if we add an API that could reasonably take them as a parameter. + if (isNaN(instance) || + instance == Number.POSITIVE_INFINITY || + instance == Number.NEGATIVE_INFINITY ) + this.addError(path, "numberFiniteNotNan", [instance]); + + if (schema.minimum !== undefined && instance < schema.minimum) + this.addError(path, "numberMinValue", [schema.minimum]); + + if (schema.maximum !== undefined && instance > schema.maximum) + this.addError(path, "numberMaxValue", [schema.maximum]); + + // Check for integer values outside of -2^31..2^31-1. + if (schema.type === "integer" && (instance | 0) !== instance) + this.addError(path, "numberIntValue", []); + + if (schema.maxDecimal && instance * Math.pow(10, schema.maxDecimal) % 1) + this.addError(path, "numberMaxDecimal", [schema.maxDecimal]); +}; + +/** + * Validates the primitive type of an instance and populates the errors + * property. Returns true if the instance validates, false otherwise. + */ +JSONSchemaValidator.prototype.validateType = function(instance, schema, path) { + var actualType = JSONSchemaValidator.getType(instance); + if (schema.type == actualType || + (schema.type == "number" && actualType == "integer")) { + return true; + } else if (schema.type == "integer" && actualType == "number") { + this.addError(path, "invalidTypeIntegerNumber"); + return false; + } else { + this.addError(path, "invalidType", [schema.type, actualType]); + return false; + } +}; + +/** + * Adds an error message. |key| is an index into the |messages| object. + * |replacements| is an array of values to replace '*' characters in the + * message. + */ +JSONSchemaValidator.prototype.addError = function(path, key, replacements) { + $Array.push(this.errors, { + path: path, + message: JSONSchemaValidator.formatError(key, replacements) + }); +}; + +/** + * Resets errors to an empty list so you can call 'validate' again. + */ +JSONSchemaValidator.prototype.resetErrors = function() { + this.errors = []; +}; + +exports.JSONSchemaValidator = JSONSchemaValidator; diff --git a/extensions/renderer/resources/last_error.js b/extensions/renderer/resources/last_error.js new file mode 100644 index 0000000..8d53371 --- /dev/null +++ b/extensions/renderer/resources/last_error.js @@ -0,0 +1,124 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +var GetAvailability = requireNative('v8_context').GetAvailability; +var GetGlobal = requireNative('sendRequest').GetGlobal; + +// Utility for setting chrome.*.lastError. +// +// A utility here is useful for two reasons: +// 1. For backwards compatibility we need to set chrome.extension.lastError, +// but not all contexts actually have access to the extension namespace. +// 2. When calling across contexts, the global object that gets lastError set +// needs to be that of the caller. We force callers to explicitly specify +// the chrome object to try to prevent bugs here. + +/** + * Sets the last error for |name| on |targetChrome| to |message| with an + * optional |stack|. + */ +function set(name, message, stack, targetChrome) { + var errorMessage = name + ': ' + message; + if (stack != null && stack != '') + errorMessage += '\n' + stack; + + if (!targetChrome) + throw new Error('No chrome object to set error: ' + errorMessage); + clear(targetChrome); // in case somebody has set a sneaky getter/setter + + var errorObject = { message: message }; + if (GetAvailability('extension.lastError').is_available) + targetChrome.extension.lastError = errorObject; + + assertRuntimeIsAvailable(); + + // We check to see if developers access runtime.lastError in order to decide + // whether or not to log it in the (error) console. + privates(targetChrome.runtime).accessedLastError = false; + $Object.defineProperty(targetChrome.runtime, 'lastError', { + configurable: true, + get: function() { + privates(targetChrome.runtime).accessedLastError = true; + return errorObject; + }, + set: function(error) { + errorObject = errorObject; + }}); +}; + +/** + * Check if anyone has checked chrome.runtime.lastError since it was set. + * @param {Object} targetChrome the Chrome object to check. + * @return boolean True if the lastError property was set. + */ +function hasAccessed(targetChrome) { + assertRuntimeIsAvailable(); + return privates(targetChrome.runtime).accessedLastError === true; +} + +/** + * Check whether there is an error set on |targetChrome| without setting + * |accessedLastError|. + * @param {Object} targetChrome the Chrome object to check. + * @return boolean Whether lastError has been set. + */ +function hasError(targetChrome) { + if (!targetChrome) + throw new Error('No target chrome to check'); + + assertRuntimeIsAvailable(); + if ('lastError' in targetChrome.runtime) + return true; + + return false; +}; + +/** + * Clears the last error on |targetChrome|. + */ +function clear(targetChrome) { + if (!targetChrome) + throw new Error('No target chrome to clear error'); + + if (GetAvailability('extension.lastError').is_available) + delete targetChrome.extension.lastError; + + assertRuntimeIsAvailable(); + delete targetChrome.runtime.lastError; + delete privates(targetChrome.runtime).accessedLastError; +}; + +function assertRuntimeIsAvailable() { + // chrome.runtime should always be available, but maybe it's disappeared for + // some reason? Add debugging for http://crbug.com/258526. + var runtimeAvailability = GetAvailability('runtime.lastError'); + if (!runtimeAvailability.is_available) { + throw new Error('runtime.lastError is not available: ' + + runtimeAvailability.message); + } + if (!chrome.runtime) + throw new Error('runtime namespace is null or undefined'); +} + +/** + * Runs |callback(args)| with last error args as in set(). + * + * The target chrome object is the global object's of the callback, so this + * method won't work if the real callback has been wrapped (etc). + */ +function run(name, message, stack, callback, args) { + var targetChrome = GetGlobal(callback).chrome; + set(name, message, stack, targetChrome); + try { + $Function.apply(callback, undefined, args); + } finally { + clear(targetChrome); + } +} + +exports.clear = clear; +exports.hasAccessed = hasAccessed; +exports.hasError = hasError; +exports.set = set; +exports.run = run; diff --git a/extensions/renderer/resources/messaging.js b/extensions/renderer/resources/messaging.js new file mode 100644 index 0000000..c7a45de --- /dev/null +++ b/extensions/renderer/resources/messaging.js @@ -0,0 +1,386 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This contains unprivileged javascript APIs for extensions and apps. It +// can be loaded by any extension-related context, such as content scripts or +// background pages. See user_script_slave.cc for script that is loaded by +// content scripts only. + + // TODO(kalman): factor requiring chrome out of here. + var chrome = requireNative('chrome').GetChrome(); + var Event = require('event_bindings').Event; + var lastError = require('lastError'); + var logActivity = requireNative('activityLogger'); + var messagingNatives = requireNative('messaging_natives'); + var processNatives = requireNative('process'); + var unloadEvent = require('unload_event'); + var utils = require('utils'); + var messagingUtils = require('messaging_utils'); + + // The reserved channel name for the sendRequest/send(Native)Message APIs. + // Note: sendRequest is deprecated. + var kRequestChannel = "chrome.extension.sendRequest"; + var kMessageChannel = "chrome.runtime.sendMessage"; + var kNativeMessageChannel = "chrome.runtime.sendNativeMessage"; + + // Map of port IDs to port object. + var ports = {}; + + // Map of port IDs to unloadEvent listeners. Keep track of these to free the + // unloadEvent listeners when ports are closed. + var portReleasers = {}; + + // Change even to odd and vice versa, to get the other side of a given + // channel. + function getOppositePortId(portId) { return portId ^ 1; } + + // Port object. Represents a connection to another script context through + // which messages can be passed. + function PortImpl(portId, opt_name) { + this.portId_ = portId; + this.name = opt_name; + + var portSchema = {name: 'port', $ref: 'runtime.Port'}; + var options = {unmanaged: true}; + this.onDisconnect = new Event(null, [portSchema], options); + this.onMessage = new Event( + null, + [{name: 'message', type: 'any', optional: true}, portSchema], + options); + this.onDestroy_ = null; + } + + // Sends a message asynchronously to the context on the other end of this + // port. + PortImpl.prototype.postMessage = function(msg) { + // JSON.stringify doesn't support a root object which is undefined. + if (msg === undefined) + msg = null; + msg = $JSON.stringify(msg); + if (msg === undefined) { + // JSON.stringify can fail with unserializable objects. Log an error and + // drop the message. + // + // TODO(kalman/mpcomplete): it would be better to do the same validation + // here that we do for runtime.sendMessage (and variants), i.e. throw an + // schema validation Error, but just maintain the old behaviour until + // there's a good reason not to (http://crbug.com/263077). + console.error('Illegal argument to Port.postMessage'); + return; + } + messagingNatives.PostMessage(this.portId_, msg); + }; + + // Disconnects the port from the other end. + PortImpl.prototype.disconnect = function() { + messagingNatives.CloseChannel(this.portId_, true); + this.destroy_(); + }; + + PortImpl.prototype.destroy_ = function() { + var portId = this.portId_; + + if (this.onDestroy_) + this.onDestroy_(); + privates(this.onDisconnect).impl.destroy_(); + privates(this.onMessage).impl.destroy_(); + + messagingNatives.PortRelease(portId); + unloadEvent.removeListener(portReleasers[portId]); + + delete ports[portId]; + delete portReleasers[portId]; + }; + + // Returns true if the specified port id is in this context. This is used by + // the C++ to avoid creating the javascript message for all the contexts that + // don't care about a particular message. + function hasPort(portId) { + return portId in ports; + }; + + // Hidden port creation function. We don't want to expose an API that lets + // people add arbitrary port IDs to the port list. + function createPort(portId, opt_name) { + if (ports[portId]) + throw new Error("Port '" + portId + "' already exists."); + var port = new Port(portId, opt_name); + ports[portId] = port; + portReleasers[portId] = $Function.bind(messagingNatives.PortRelease, + this, + portId); + unloadEvent.addListener(portReleasers[portId]); + messagingNatives.PortAddRef(portId); + return port; + }; + + // Helper function for dispatchOnRequest. + function handleSendRequestError(isSendMessage, + responseCallbackPreserved, + sourceExtensionId, + targetExtensionId, + sourceUrl) { + var errorMsg = []; + var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest"; + if (isSendMessage && !responseCallbackPreserved) { + $Array.push(errorMsg, + "The chrome." + eventName + " listener must return true if you " + + "want to send a response after the listener returns"); + } else { + $Array.push(errorMsg, + "Cannot send a response more than once per chrome." + eventName + + " listener per document"); + } + $Array.push(errorMsg, "(message was sent by extension" + sourceExtensionId); + if (sourceExtensionId != "" && sourceExtensionId != targetExtensionId) + $Array.push(errorMsg, "for extension " + targetExtensionId); + if (sourceUrl != "") + $Array.push(errorMsg, "for URL " + sourceUrl); + lastError.set(eventName, errorMsg.join(" ") + ").", null, chrome); + } + + // Helper function for dispatchOnConnect + function dispatchOnRequest(portId, channelName, sender, + sourceExtensionId, targetExtensionId, sourceUrl, + isExternal) { + var isSendMessage = channelName == kMessageChannel; + var requestEvent = null; + if (isSendMessage) { + if (chrome.runtime) { + requestEvent = isExternal ? chrome.runtime.onMessageExternal + : chrome.runtime.onMessage; + } + } else { + if (chrome.extension) { + requestEvent = isExternal ? chrome.extension.onRequestExternal + : chrome.extension.onRequest; + } + } + if (!requestEvent) + return false; + if (!requestEvent.hasListeners()) + return false; + var port = createPort(portId, channelName); + + function messageListener(request) { + var responseCallbackPreserved = false; + var responseCallback = function(response) { + if (port) { + port.postMessage(response); + privates(port).impl.destroy_(); + port = null; + } else { + // We nulled out port when sending the response, and now the page + // is trying to send another response for the same request. + handleSendRequestError(isSendMessage, responseCallbackPreserved, + sourceExtensionId, targetExtensionId); + } + }; + // In case the extension never invokes the responseCallback, and also + // doesn't keep a reference to it, we need to clean up the port. Do + // so by attaching to the garbage collection of the responseCallback + // using some native hackery. + messagingNatives.BindToGC(responseCallback, function() { + if (port) { + privates(port).impl.destroy_(); + port = null; + } + }); + var rv = requestEvent.dispatch(request, sender, responseCallback); + if (isSendMessage) { + responseCallbackPreserved = + rv && rv.results && $Array.indexOf(rv.results, true) > -1; + if (!responseCallbackPreserved && port) { + // If they didn't access the response callback, they're not + // going to send a response, so clean up the port immediately. + privates(port).impl.destroy_(); + port = null; + } + } + } + + privates(port).impl.onDestroy_ = function() { + port.onMessage.removeListener(messageListener); + }; + port.onMessage.addListener(messageListener); + + var eventName = isSendMessage ? "runtime.onMessage" : "extension.onRequest"; + if (isExternal) + eventName += "External"; + logActivity.LogEvent(targetExtensionId, + eventName, + [sourceExtensionId, sourceUrl]); + return true; + } + + // Called by native code when a channel has been opened to this context. + function dispatchOnConnect(portId, + channelName, + sourceTab, + sourceExtensionId, + targetExtensionId, + sourceUrl, + tlsChannelId) { + // Only create a new Port if someone is actually listening for a connection. + // In addition to being an optimization, this also fixes a bug where if 2 + // channels were opened to and from the same process, closing one would + // close both. + var extensionId = processNatives.GetExtensionId(); + if (targetExtensionId != extensionId) + return false; // not for us + + if (ports[getOppositePortId(portId)]) + return false; // this channel was opened by us, so ignore it + + // Determine whether this is coming from another extension, so we can use + // the right event. + var isExternal = sourceExtensionId != extensionId; + + var sender = {}; + if (sourceExtensionId != '') + sender.id = sourceExtensionId; + if (sourceUrl) + sender.url = sourceUrl; + if (sourceTab) + sender.tab = sourceTab; + if (tlsChannelId !== undefined) + sender.tlsChannelId = tlsChannelId; + + // Special case for sendRequest/onRequest and sendMessage/onMessage. + if (channelName == kRequestChannel || channelName == kMessageChannel) { + return dispatchOnRequest(portId, channelName, sender, + sourceExtensionId, targetExtensionId, sourceUrl, + isExternal); + } + + var connectEvent = null; + if (chrome.runtime) { + connectEvent = isExternal ? chrome.runtime.onConnectExternal + : chrome.runtime.onConnect; + } + if (!connectEvent) + return false; + if (!connectEvent.hasListeners()) + return false; + + var port = createPort(portId, channelName); + port.sender = sender; + if (processNatives.manifestVersion < 2) + port.tab = port.sender.tab; + + var eventName = (isExternal ? + "runtime.onConnectExternal" : "runtime.onConnect"); + connectEvent.dispatch(port); + logActivity.LogEvent(targetExtensionId, + eventName, + [sourceExtensionId]); + return true; + }; + + // Called by native code when a channel has been closed. + function dispatchOnDisconnect(portId, errorMessage) { + var port = ports[portId]; + if (port) { + // Update the renderer's port bookkeeping, without notifying the browser. + messagingNatives.CloseChannel(portId, false); + if (errorMessage) + lastError.set('Port', errorMessage, null, chrome); + try { + port.onDisconnect.dispatch(port); + } finally { + privates(port).impl.destroy_(); + lastError.clear(chrome); + } + } + }; + + // Called by native code when a message has been sent to the given port. + function dispatchOnMessage(msg, portId) { + var port = ports[portId]; + if (port) { + if (msg) + msg = $JSON.parse(msg); + port.onMessage.dispatch(msg, port); + } + }; + + // Shared implementation used by tabs.sendMessage and runtime.sendMessage. + function sendMessageImpl(port, request, responseCallback) { + if (port.name != kNativeMessageChannel) + port.postMessage(request); + + if (port.name == kMessageChannel && !responseCallback) { + // TODO(mpcomplete): Do this for the old sendRequest API too, after + // verifying it doesn't break anything. + // Go ahead and disconnect immediately if the sender is not expecting + // a response. + port.disconnect(); + return; + } + + // Ensure the callback exists for the older sendRequest API. + if (!responseCallback) + responseCallback = function() {}; + + // Note: make sure to manually remove the onMessage/onDisconnect listeners + // that we added before destroying the Port, a workaround to a bug in Port + // where any onMessage/onDisconnect listeners added but not removed will + // be leaked when the Port is destroyed. + // http://crbug.com/320723 tracks a sustainable fix. + + function disconnectListener() { + // For onDisconnects, we only notify the callback if there was an error. + if (chrome.runtime && chrome.runtime.lastError) + responseCallback(); + } + + function messageListener(response) { + try { + responseCallback(response); + } finally { + port.disconnect(); + } + } + + privates(port).impl.onDestroy_ = function() { + port.onDisconnect.removeListener(disconnectListener); + port.onMessage.removeListener(messageListener); + }; + port.onDisconnect.addListener(disconnectListener); + port.onMessage.addListener(messageListener); + }; + + function sendMessageUpdateArguments(functionName, hasOptionsArgument) { + // skip functionName and hasOptionsArgument + var args = $Array.slice(arguments, 2); + var alignedArgs = messagingUtils.alignSendMessageArguments(args, + hasOptionsArgument); + if (!alignedArgs) + throw new Error('Invalid arguments to ' + functionName + '.'); + return alignedArgs; + } + +var Port = utils.expose('Port', PortImpl, { functions: [ + 'disconnect', + 'postMessage' + ], + properties: [ + 'name', + 'onDisconnect', + 'onMessage' + ] }); + +exports.kRequestChannel = kRequestChannel; +exports.kMessageChannel = kMessageChannel; +exports.kNativeMessageChannel = kNativeMessageChannel; +exports.Port = Port; +exports.createPort = createPort; +exports.sendMessageImpl = sendMessageImpl; +exports.sendMessageUpdateArguments = sendMessageUpdateArguments; + +// For C++ code to call. +exports.hasPort = hasPort; +exports.dispatchOnConnect = dispatchOnConnect; +exports.dispatchOnDisconnect = dispatchOnDisconnect; +exports.dispatchOnMessage = dispatchOnMessage; diff --git a/extensions/renderer/resources/messaging_utils.js b/extensions/renderer/resources/messaging_utils.js new file mode 100644 index 0000000..d1b563d --- /dev/null +++ b/extensions/renderer/resources/messaging_utils.js @@ -0,0 +1,53 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Routines used to normalize arguments to messaging functions. + +function alignSendMessageArguments(args, hasOptionsArgument) { + // Align missing (optional) function arguments with the arguments that + // schema validation is expecting, e.g. + // extension.sendRequest(req) -> extension.sendRequest(null, req) + // extension.sendRequest(req, cb) -> extension.sendRequest(null, req, cb) + if (!args || !args.length) + return null; + var lastArg = args.length - 1; + + // responseCallback (last argument) is optional. + var responseCallback = null; + if (typeof args[lastArg] == 'function') + responseCallback = args[lastArg--]; + + var options = null; + if (hasOptionsArgument && lastArg >= 1) { + // options (third argument) is optional. It can also be ambiguous which + // argument it should match. If there are more than two arguments remaining, + // options is definitely present: + if (lastArg > 1) { + options = args[lastArg--]; + } else { + // Exactly two arguments remaining. If the first argument is a string, + // it should bind to targetId, and the second argument should bind to + // request, which is required. In other words, when two arguments remain, + // only bind options when the first argument cannot bind to targetId. + if (!(args[0] === null || typeof args[0] == 'string')) + options = args[lastArg--]; + } + } + + // request (second argument) is required. + var request = args[lastArg--]; + + // targetId (first argument, extensionId in the manifest) is optional. + var targetId = null; + if (lastArg >= 0) + targetId = args[lastArg--]; + + if (lastArg != -1) + return null; + if (hasOptionsArgument) + return [targetId, request, options, responseCallback]; + return [targetId, request, responseCallback]; +} + +exports.alignSendMessageArguments = alignSendMessageArguments; diff --git a/extensions/renderer/resources/permissions_custom_bindings.js b/extensions/renderer/resources/permissions_custom_bindings.js new file mode 100644 index 0000000..60edfaa --- /dev/null +++ b/extensions/renderer/resources/permissions_custom_bindings.js @@ -0,0 +1,97 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Custom binding for the Permissions API. + +var binding = require('binding').Binding.create('permissions'); + +var Event = require('event_bindings').Event; + +// These custom binding are only necessary because it is not currently +// possible to have a union of types as the type of the items in an array. +// Once that is fixed, this entire file should go away. +// See, +// https://code.google.com/p/chromium/issues/detail?id=162044 +// https://code.google.com/p/chromium/issues/detail?id=162042 +// TODO(bryeung): delete this file. +binding.registerCustomHook(function(api) { + var apiFunctions = api.apiFunctions; + var permissions = api.compiledApi; + + function maybeConvertToObject(str) { + var parts = $String.split(str, '|'); + if (parts.length != 2) + return str; + + var ret = {}; + ret[parts[0]] = JSON.parse(parts[1]); + return ret; + } + + function convertObjectPermissionsToStrings() { + if (arguments.length < 1) + return arguments; + + var args = arguments[0].permissions; + if (!args) + return arguments; + + for (var i = 0; i < args.length; i += 1) { + if (typeof(args[i]) == 'object') { + var a = args[i]; + var keys = $Object.keys(a); + if (keys.length != 1) { + throw new Error("Too many keys in object-style permission."); + } + arguments[0].permissions[i] = keys[0] + '|' + + JSON.stringify(a[keys[0]]); + } + } + + return arguments; + } + + // Convert complex permissions to strings so they validate against the schema + apiFunctions.setUpdateArgumentsPreValidate( + 'contains', convertObjectPermissionsToStrings); + apiFunctions.setUpdateArgumentsPreValidate( + 'remove', convertObjectPermissionsToStrings); + apiFunctions.setUpdateArgumentsPreValidate( + 'request', convertObjectPermissionsToStrings); + + // Convert complex permissions back to objects + apiFunctions.setCustomCallback('getAll', + function(name, request, response) { + for (var i = 0; i < response.permissions.length; i += 1) { + response.permissions[i] = + maybeConvertToObject(response.permissions[i]); + } + + // Since the schema says Permissions.permissions contains strings and + // not objects, validation will fail after the for-loop above. This + // skips validation and calls the callback directly, then clears it so + // that handleResponse doesn't call it again. + try { + if (request.callback) + $Function.apply(request.callback, request, [response]); + } finally { + delete request.callback; + } + }); + + // Also convert complex permissions back to objects for events. The + // dispatchToListener call happens after argument validation, which works + // around the problem that Permissions.permissions is supposed to be a list + // of strings. + permissions.onAdded.dispatchToListener = function(callback, args) { + for (var i = 0; i < args[0].permissions.length; i += 1) { + args[0].permissions[i] = maybeConvertToObject(args[0].permissions[i]); + } + $Function.call(Event.prototype.dispatchToListener, this, callback, args); + }; + permissions.onRemoved.dispatchToListener = + permissions.onAdded.dispatchToListener; +}); + +exports.binding = binding.generate(); diff --git a/extensions/renderer/resources/platform_app.css b/extensions/renderer/resources/platform_app.css new file mode 100644 index 0000000..eb241de --- /dev/null +++ b/extensions/renderer/resources/platform_app.css @@ -0,0 +1,35 @@ +/* + * Copyright 2014 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * A style sheet for Chrome apps. + */ + +@namespace "http://www.w3.org/1999/xhtml"; + +body { + -webkit-user-select: none; + cursor: default; + font-family: $FONTFAMILY; + font-size: $FONTSIZE; +} + +webview, adview { + display: inline-block; + width: 300px; + height: 300px; +} + +html, body { + overflow: hidden; +} + +img, a { + -webkit-user-drag: none; +} + +[contenteditable], input { + -webkit-user-select: auto; +} + diff --git a/extensions/renderer/resources/platform_app.js b/extensions/renderer/resources/platform_app.js new file mode 100644 index 0000000..16470a5 --- /dev/null +++ b/extensions/renderer/resources/platform_app.js @@ -0,0 +1,207 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +var $console = window.console; + +/** + * Returns a function that logs a 'not available' error to the console and + * returns undefined. + * + * @param {string} messagePrefix text to prepend to the exception message. + */ +function generateDisabledMethodStub(messagePrefix, opt_messageSuffix) { + var message = messagePrefix + ' is not available in packaged apps.'; + if (opt_messageSuffix) message = message + ' ' + opt_messageSuffix; + return function() { + $console.error(message); + return; + }; +} + +/** + * Returns a function that throws a 'not available' error. + * + * @param {string} messagePrefix text to prepend to the exception message. + */ +function generateThrowingMethodStub(messagePrefix, opt_messageSuffix) { + var message = messagePrefix + ' is not available in packaged apps.'; + if (opt_messageSuffix) message = message + ' ' + opt_messageSuffix; + return function() { + throw new Error(message); + }; +} + +/** + * Replaces the given methods of the passed in object with stubs that log + * 'not available' errors to the console and return undefined. + * + * This should be used on methods attached via non-configurable properties, + * such as window.alert. disableGetters should be used when possible, because + * it is friendlier towards feature detection. + * + * In most cases, the useThrowingStubs should be false, so the stubs used to + * replace the methods log an error to the console, but allow the calling code + * to continue. We shouldn't break library code that uses feature detection + * responsibly, such as: + * if(window.confirm) { + * var result = window.confirm('Are you sure you want to delete ...?'); + * ... + * } + * + * useThrowingStubs should only be true for methods that are deprecated in the + * Web platform, and should not be used by a responsible library, even in + * conjunction with feature detection. A great example is document.write(), as + * the HTML5 specification recommends against using it, and says that its + * behavior is unreliable. No reasonable library code should ever use it. + * HTML5 spec: http://www.w3.org/TR/html5/dom.html#dom-document-write + * + * @param {Object} object The object with methods to disable. The prototype is + * preferred. + * @param {string} objectName The display name to use in the error message + * thrown by the stub (this is the name that the object is commonly referred + * to by web developers, e.g. "document" instead of "HTMLDocument"). + * @param {Array.<string>} methodNames names of methods to disable. + * @param {Boolean} useThrowingStubs if true, the replaced methods will throw + * an error instead of silently returning undefined + */ +function disableMethods(object, objectName, methodNames, useThrowingStubs) { + $Array.forEach(methodNames, function(methodName) { + var messagePrefix = objectName + '.' + methodName + '()'; + object[methodName] = useThrowingStubs ? + generateThrowingMethodStub(messagePrefix) : + generateDisabledMethodStub(messagePrefix); + }); +} + +/** + * Replaces the given properties of the passed in object with stubs that log + * 'not available' warnings to the console and return undefined when gotten. If + * a property's setter is later invoked, the getter and setter are restored to + * default behaviors. + * + * @param {Object} object The object with properties to disable. The prototype + * is preferred. + * @param {string} objectName The display name to use in the error message + * thrown by the getter stub (this is the name that the object is commonly + * referred to by web developers, e.g. "document" instead of + * "HTMLDocument"). + * @param {Array.<string>} propertyNames names of properties to disable. + */ +function disableGetters(object, objectName, propertyNames, opt_messageSuffix) { + $Array.forEach(propertyNames, function(propertyName) { + var stub = generateDisabledMethodStub(objectName + '.' + propertyName, + opt_messageSuffix); + stub._is_platform_app_disabled_getter = true; + $Object.defineProperty(object, propertyName, { + configurable: true, + enumerable: false, + get: stub, + set: function(value) { + var descriptor = $Object.getOwnPropertyDescriptor(this, propertyName); + if (!descriptor || !descriptor.get || + descriptor.get._is_platform_app_disabled_getter) { + // The stub getter is still defined. Blow-away the property to + // restore default getter/setter behaviors and re-create it with the + // given value. + delete this[propertyName]; + this[propertyName] = value; + } else { + // Do nothing. If some custom getter (not ours) has been defined, + // there would be no way to read back the value stored by a default + // setter. Also, the only way to clear a custom getter is to first + // delete the property. Therefore, the value we have here should + // just go into a black hole. + } + } + }); + }); +} + +/** + * Replaces the given properties of the passed in object with stubs that log + * 'not available' warnings to the console when set. + * + * @param {Object} object The object with properties to disable. The prototype + * is preferred. + * @param {string} objectName The display name to use in the error message + * thrown by the setter stub (this is the name that the object is commonly + * referred to by web developers, e.g. "document" instead of + * "HTMLDocument"). + * @param {Array.<string>} propertyNames names of properties to disable. + */ +function disableSetters(object, objectName, propertyNames, opt_messageSuffix) { + $Array.forEach(propertyNames, function(propertyName) { + var stub = generateDisabledMethodStub(objectName + '.' + propertyName, + opt_messageSuffix); + $Object.defineProperty(object, propertyName, { + configurable: true, + enumerable: false, + get: function() { + return; + }, + set: stub + }); + }); +} + +// Disable benign Document methods. +disableMethods(HTMLDocument.prototype, 'document', ['open', 'clear', 'close']); + +// Replace evil Document methods with exception-throwing stubs. +disableMethods(HTMLDocument.prototype, 'document', ['write', 'writeln'], true); + +// Disable history. +Object.defineProperty(window, "history", { value: {} }); +disableGetters(window.history, 'history', ['back', 'forward', 'go', 'length']); + +// Disable find. +disableMethods(Window.prototype, 'window', ['find']); + +// Disable modal dialogs. Shell windows disable these anyway, but it's nice to +// warn. +disableMethods(Window.prototype, 'window', ['alert', 'confirm', 'prompt']); + +// Disable window.*bar. +disableGetters(window, 'window', + ['locationbar', 'menubar', 'personalbar', 'scrollbars', 'statusbar', + 'toolbar']); + +// Disable window.localStorage. +// Sometimes DOM security policy prevents us from doing this (e.g. for data: +// URLs) so wrap in try-catch. +try { + disableGetters(window, 'window', + ['localStorage'], + 'Use chrome.storage.local instead.'); +} catch (e) {} + +// Document instance properties that we wish to disable need to be set when +// the document begins loading, since only then will the "document" reference +// point to the page's document (it will be reset between now and then). +// We can't listen for the "readystatechange" event on the document (because +// the object that it's dispatched on doesn't exist yet), but we can instead +// do it at the window level in the capturing phase. +window.addEventListener('readystatechange', function(event) { + if (document.readyState != 'loading') + return; + + // Deprecated document properties from + // https://developer.mozilla.org/en/DOM/document. + // To deprecate document.all, simply changing its getter and setter would + // activate its cache mechanism, and degrade the performance. Here we assign + // it first to 'undefined' to avoid this. + document.all = undefined; + disableGetters(document, 'document', + ['alinkColor', 'all', 'bgColor', 'fgColor', 'linkColor', 'vlinkColor']); +}, true); + +// Disable onunload, onbeforeunload. +disableSetters(Window.prototype, 'window', ['onbeforeunload', 'onunload']); +var windowAddEventListener = Window.prototype.addEventListener; +Window.prototype.addEventListener = function(type) { + if (type === 'unload' || type === 'beforeunload') + generateDisabledMethodStub(type)(); + else + return $Function.apply(windowAddEventListener, window, arguments); +}; diff --git a/extensions/renderer/resources/runtime_custom_bindings.js b/extensions/renderer/resources/runtime_custom_bindings.js new file mode 100644 index 0000000..88f4f5e --- /dev/null +++ b/extensions/renderer/resources/runtime_custom_bindings.js @@ -0,0 +1,205 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Custom binding for the runtime API. + +var binding = require('binding').Binding.create('runtime'); + +var messaging = require('messaging'); +var runtimeNatives = requireNative('runtime'); +var unloadEvent = require('unload_event'); +var process = requireNative('process'); +var forEach = require('utils').forEach; + +var backgroundPage = window; +var backgroundRequire = require; +var contextType = process.GetContextType(); +if (contextType == 'BLESSED_EXTENSION' || + contextType == 'UNBLESSED_EXTENSION') { + var manifest = runtimeNatives.GetManifest(); + if (manifest.app && manifest.app.background) { + // Get the background page if one exists. Otherwise, default to the current + // window. + backgroundPage = runtimeNatives.GetExtensionViews(-1, 'BACKGROUND')[0]; + if (backgroundPage) { + var GetModuleSystem = requireNative('v8_context').GetModuleSystem; + backgroundRequire = GetModuleSystem(backgroundPage).require; + } else { + backgroundPage = window; + } + } +} + +// For packaged apps, all windows use the bindFileEntryCallback from the +// background page so their FileEntry objects have the background page's context +// as their own. This allows them to be used from other windows (including the +// background page) after the original window is closed. +if (window == backgroundPage) { + var lastError = require('lastError'); + var fileSystemNatives = requireNative('file_system_natives'); + var GetIsolatedFileSystem = fileSystemNatives.GetIsolatedFileSystem; + var bindDirectoryEntryCallback = function(functionName, apiFunctions) { + apiFunctions.setCustomCallback(functionName, + function(name, request, response) { + if (request.callback && response) { + var callback = request.callback; + request.callback = null; + + var fileSystemId = response.fileSystemId; + var baseName = response.baseName; + var fs = GetIsolatedFileSystem(fileSystemId); + + try { + fs.root.getDirectory(baseName, {}, callback, function(fileError) { + lastError.run('runtime.' + functionName, + 'Error getting Entry, code: ' + fileError.code, + request.stack, + callback); + }); + } catch (e) { + lastError.run('runtime.' + functionName, + 'Error: ' + e.stack, + request.stack, + callback); + } + } + }); + }; +} else { + // Force the runtime API to be loaded in the background page. Using + // backgroundPageModuleSystem.require('runtime') is insufficient as + // requireNative is only allowed while lazily loading an API. + backgroundPage.chrome.runtime; + var bindDirectoryEntryCallback = backgroundRequire( + 'runtime').bindDirectoryEntryCallback; +} + +binding.registerCustomHook(function(binding, id, contextType) { + var apiFunctions = binding.apiFunctions; + var runtime = binding.compiledApi; + + // + // Unprivileged APIs. + // + + runtime.id = id; + + apiFunctions.setHandleRequest('getManifest', function() { + return runtimeNatives.GetManifest(); + }); + + apiFunctions.setHandleRequest('getURL', function(path) { + path = String(path); + if (!path.length || path[0] != '/') + path = '/' + path; + return 'chrome-extension://' + id + path; + }); + + var sendMessageUpdateArguments = messaging.sendMessageUpdateArguments; + apiFunctions.setUpdateArgumentsPreValidate('sendMessage', + $Function.bind(sendMessageUpdateArguments, null, 'sendMessage', + true /* hasOptionsArgument */)); + apiFunctions.setUpdateArgumentsPreValidate('sendNativeMessage', + $Function.bind(sendMessageUpdateArguments, null, 'sendNativeMessage', + false /* hasOptionsArgument */)); + + apiFunctions.setHandleRequest('sendMessage', + function(targetId, message, options, responseCallback) { + var connectOptions = {name: messaging.kMessageChannel}; + forEach(options, function(k, v) { + connectOptions[k] = v; + }); + var port = runtime.connect(targetId || runtime.id, connectOptions); + messaging.sendMessageImpl(port, message, responseCallback); + }); + + apiFunctions.setHandleRequest('sendNativeMessage', + function(targetId, message, responseCallback) { + var port = runtime.connectNative(targetId); + messaging.sendMessageImpl(port, message, responseCallback); + }); + + apiFunctions.setUpdateArgumentsPreValidate('connect', function() { + // Align missing (optional) function arguments with the arguments that + // schema validation is expecting, e.g. + // runtime.connect() -> runtime.connect(null, null) + // runtime.connect({}) -> runtime.connect(null, {}) + var nextArg = 0; + + // targetId (first argument) is optional. + var targetId = null; + if (typeof(arguments[nextArg]) == 'string') + targetId = arguments[nextArg++]; + + // connectInfo (second argument) is optional. + var connectInfo = null; + if (typeof(arguments[nextArg]) == 'object') + connectInfo = arguments[nextArg++]; + + if (nextArg != arguments.length) + throw new Error('Invalid arguments to connect.'); + return [targetId, connectInfo]; + }); + + apiFunctions.setUpdateArgumentsPreValidate('connectNative', + function(appName) { + if (typeof(appName) !== 'string') { + throw new Error('Invalid arguments to connectNative.'); + } + return [appName]; + }); + + apiFunctions.setHandleRequest('connect', function(targetId, connectInfo) { + // Don't let orphaned content scripts communicate with their extension. + // http://crbug.com/168263 + if (unloadEvent.wasDispatched) + throw new Error('Error connecting to extension ' + targetId); + + if (!targetId) + targetId = runtime.id; + + var name = ''; + if (connectInfo && connectInfo.name) + name = connectInfo.name; + + var includeTlsChannelId = + !!(connectInfo && connectInfo.includeTlsChannelId); + + var portId = runtimeNatives.OpenChannelToExtension(targetId, name, + includeTlsChannelId); + if (portId >= 0) + return messaging.createPort(portId, name); + }); + + // + // Privileged APIs. + // + if (contextType != 'BLESSED_EXTENSION') + return; + + apiFunctions.setHandleRequest('connectNative', + function(nativeAppName) { + if (!unloadEvent.wasDispatched) { + var portId = runtimeNatives.OpenChannelToNativeApp(runtime.id, + nativeAppName); + if (portId >= 0) + return messaging.createPort(portId, ''); + } + throw new Error('Error connecting to native app: ' + nativeAppName); + }); + + apiFunctions.setCustomCallback('getBackgroundPage', + function(name, request, response) { + if (request.callback) { + var bg = runtimeNatives.GetExtensionViews(-1, 'BACKGROUND')[0] || null; + request.callback(bg); + } + request.callback = null; + }); + + bindDirectoryEntryCallback('getPackageDirectoryEntry', apiFunctions); +}); + +exports.bindDirectoryEntryCallback = bindDirectoryEntryCallback; +exports.binding = binding.generate(); diff --git a/extensions/renderer/resources/schema_utils.js b/extensions/renderer/resources/schema_utils.js new file mode 100644 index 0000000..c0cb777 --- /dev/null +++ b/extensions/renderer/resources/schema_utils.js @@ -0,0 +1,156 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Routines used to validate and normalize arguments. +// TODO(benwells): unit test this file. + +var JSONSchemaValidator = require('json_schema').JSONSchemaValidator; + +var schemaValidator = new JSONSchemaValidator(); + +// Validate arguments. +function validate(args, parameterSchemas) { + if (args.length > parameterSchemas.length) + throw new Error("Too many arguments."); + for (var i = 0; i < parameterSchemas.length; i++) { + if (i in args && args[i] !== null && args[i] !== undefined) { + schemaValidator.resetErrors(); + schemaValidator.validate(args[i], parameterSchemas[i]); + if (schemaValidator.errors.length == 0) + continue; + var message = "Invalid value for argument " + (i + 1) + ". "; + for (var i = 0, err; + err = schemaValidator.errors[i]; i++) { + if (err.path) { + message += "Property '" + err.path + "': "; + } + message += err.message; + message = message.substring(0, message.length - 1); + message += ", "; + } + message = message.substring(0, message.length - 2); + message += "."; + throw new Error(message); + } else if (!parameterSchemas[i].optional) { + throw new Error("Parameter " + (i + 1) + " (" + + parameterSchemas[i].name + ") is required."); + } + } +} + +// Generate all possible signatures for a given API function. +function getSignatures(parameterSchemas) { + if (parameterSchemas.length === 0) + return [[]]; + var signatures = []; + var remaining = getSignatures($Array.slice(parameterSchemas, 1)); + for (var i = 0; i < remaining.length; i++) + $Array.push(signatures, $Array.concat([parameterSchemas[0]], remaining[i])) + if (parameterSchemas[0].optional) + return $Array.concat(signatures, remaining); + return signatures; +}; + +// Return true if arguments match a given signature's schema. +function argumentsMatchSignature(args, candidateSignature) { + if (args.length != candidateSignature.length) + return false; + for (var i = 0; i < candidateSignature.length; i++) { + var argType = JSONSchemaValidator.getType(args[i]); + if (!schemaValidator.isValidSchemaType(argType, + candidateSignature[i])) + return false; + } + return true; +}; + +// Finds the function signature for the given arguments. +function resolveSignature(args, definedSignature) { + var candidateSignatures = getSignatures(definedSignature); + for (var i = 0; i < candidateSignatures.length; i++) { + if (argumentsMatchSignature(args, candidateSignatures[i])) + return candidateSignatures[i]; + } + return null; +}; + +// Returns a string representing the defined signature of the API function. +// Example return value for chrome.windows.getCurrent: +// "windows.getCurrent(optional object populate, function callback)" +function getParameterSignatureString(name, definedSignature) { + var getSchemaTypeString = function(schema) { + var schemaTypes = schemaValidator.getAllTypesForSchema(schema); + var typeName = schemaTypes.join(" or ") + " " + schema.name; + if (schema.optional) + return "optional " + typeName; + return typeName; + }; + var typeNames = definedSignature.map(getSchemaTypeString); + return name + "(" + typeNames.join(", ") + ")"; +}; + +// Returns a string representing a call to an API function. +// Example return value for call: chrome.windows.get(1, callback) is: +// "windows.get(int, function)" +function getArgumentSignatureString(name, args) { + var typeNames = args.map(JSONSchemaValidator.getType); + return name + "(" + typeNames.join(", ") + ")"; +}; + +// Finds the correct signature for the given arguments, then validates the +// arguments against that signature. Returns a 'normalized' arguments list +// where nulls are inserted where optional parameters were omitted. +// |args| is expected to be an array. +function normalizeArgumentsAndValidate(args, funDef) { + if (funDef.allowAmbiguousOptionalArguments) { + validate(args, funDef.definition.parameters); + return args; + } + var definedSignature = funDef.definition.parameters; + var resolvedSignature = resolveSignature(args, definedSignature); + if (!resolvedSignature) + throw new Error("Invocation of form " + + getArgumentSignatureString(funDef.name, args) + + " doesn't match definition " + + getParameterSignatureString(funDef.name, definedSignature)); + validate(args, resolvedSignature); + var normalizedArgs = []; + var ai = 0; + for (var si = 0; si < definedSignature.length; si++) { + if (definedSignature[si] === resolvedSignature[ai]) + $Array.push(normalizedArgs, args[ai++]); + else + $Array.push(normalizedArgs, null); + } + return normalizedArgs; +}; + +// Validates that a given schema for an API function is not ambiguous. +function isFunctionSignatureAmbiguous(functionDef) { + if (functionDef.allowAmbiguousOptionalArguments) + return false; + var signaturesAmbiguous = function(signature1, signature2) { + if (signature1.length != signature2.length) + return false; + for (var i = 0; i < signature1.length; i++) { + if (!schemaValidator.checkSchemaOverlap( + signature1[i], signature2[i])) + return false; + } + return true; + }; + var candidateSignatures = getSignatures(functionDef.parameters); + for (var i = 0; i < candidateSignatures.length; i++) { + for (var j = i + 1; j < candidateSignatures.length; j++) { + if (signaturesAmbiguous(candidateSignatures[i], candidateSignatures[j])) + return true; + } + } + return false; +}; + +exports.isFunctionSignatureAmbiguous = isFunctionSignatureAmbiguous; +exports.normalizeArgumentsAndValidate = normalizeArgumentsAndValidate; +exports.schemaValidator = schemaValidator; +exports.validate = validate; diff --git a/extensions/renderer/resources/send_request.js b/extensions/renderer/resources/send_request.js new file mode 100644 index 0000000..8402843 --- /dev/null +++ b/extensions/renderer/resources/send_request.js @@ -0,0 +1,178 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +var handleUncaughtException = require('uncaught_exception_handler').handle; +var lastError = require('lastError'); +var logging = requireNative('logging'); +var natives = requireNative('sendRequest'); +var processNatives = requireNative('process'); +var validate = require('schemaUtils').validate; + +// All outstanding requests from sendRequest(). +var requests = {}; + +// Used to prevent double Activity Logging for API calls that use both custom +// bindings and ExtensionFunctions (via sendRequest). +var calledSendRequest = false; + +// Runs a user-supplied callback safely. +function safeCallbackApply(name, request, callback, args) { + try { + $Function.apply(callback, request, args); + } catch (e) { + var errorMessage = "Error in response to " + name + ": " + e; + if (request.stack && request.stack != '') + errorMessage += "\n" + request.stack; + handleUncaughtException(errorMessage, e); + } +} + +// Callback handling. +function handleResponse(requestId, name, success, responseList, error) { + // The chrome objects we will set lastError on. Really we should only be + // setting this on the callback's chrome object, but set on ours too since + // it's conceivable that something relies on that. + var callerChrome = chrome; + + try { + var request = requests[requestId]; + logging.DCHECK(request != null); + + // lastError needs to be set on the caller's chrome object no matter what, + // though chances are it's the same as ours (it will be different when + // calling API methods on other contexts). + if (request.callback) + callerChrome = natives.GetGlobal(request.callback).chrome; + + lastError.clear(chrome); + if (callerChrome !== chrome) + lastError.clear(callerChrome); + + if (!success) { + if (!error) + error = "Unknown error."; + lastError.set(name, error, request.stack, chrome); + if (callerChrome !== chrome) + lastError.set(name, error, request.stack, callerChrome); + } + + if (request.customCallback) { + safeCallbackApply(name, + request, + request.customCallback, + $Array.concat([name, request], responseList)); + } + + if (request.callback) { + // Validate callback in debug only -- and only when the + // caller has provided a callback. Implementations of api + // calls may not return data if they observe the caller + // has not provided a callback. + if (logging.DCHECK_IS_ON() && !error) { + if (!request.callbackSchema.parameters) + throw new Error(name + ": no callback schema defined"); + validate(responseList, request.callbackSchema.parameters); + } + safeCallbackApply(name, request, request.callback, responseList); + } + + if (error && + !lastError.hasAccessed(chrome) && + !lastError.hasAccessed(callerChrome)) { + // The native call caused an error, but the developer didn't check + // runtime.lastError. + // Notify the developer of the error via the (error) console. + console.error("Unchecked runtime.lastError while running " + + (name || "unknown") + ": " + error + + (request.stack ? "\n" + request.stack : "")); + } + } finally { + delete requests[requestId]; + lastError.clear(chrome); + if (callerChrome !== chrome) + lastError.clear(callerChrome); + } +}; + +function getExtensionStackTrace(call_name) { + var stack = $String.split(new Error().stack, '\n'); + var id = processNatives.GetExtensionId(); + + // Remove stack frames before and after that weren't associated with the + // extension. + return $Array.join(stack.filter(function(line) { + return line.indexOf(id) != -1; + }), '\n'); +} + +function prepareRequest(args, argSchemas) { + var request = {}; + var argCount = args.length; + + // Look for callback param. + if (argSchemas.length > 0 && + argSchemas[argSchemas.length - 1].type == "function") { + request.callback = args[args.length - 1]; + request.callbackSchema = argSchemas[argSchemas.length - 1]; + --argCount; + } + + request.args = []; + for (var k = 0; k < argCount; k++) { + request.args[k] = args[k]; + } + + return request; +} + +// Send an API request and optionally register a callback. +// |optArgs| is an object with optional parameters as follows: +// - customCallback: a callback that should be called instead of the standard +// callback. +// - nativeFunction: the v8 native function to handle the request, or +// StartRequest if missing. +// - forIOThread: true if this function should be handled on the browser IO +// thread. +// - preserveNullInObjects: true if it is safe for null to be in objects. +function sendRequest(functionName, args, argSchemas, optArgs) { + calledSendRequest = true; + if (!optArgs) + optArgs = {}; + var request = prepareRequest(args, argSchemas); + request.stack = getExtensionStackTrace(); + if (optArgs.customCallback) { + request.customCallback = optArgs.customCallback; + } + + var nativeFunction = optArgs.nativeFunction || natives.StartRequest; + + var requestId = natives.GetNextRequestId(); + request.id = requestId; + requests[requestId] = request; + + var hasCallback = request.callback || optArgs.customCallback; + return nativeFunction(functionName, + request.args, + requestId, + hasCallback, + optArgs.forIOThread, + optArgs.preserveNullInObjects); +} + +function getCalledSendRequest() { + return calledSendRequest; +} + +function clearCalledSendRequest() { + calledSendRequest = false; +} + +exports.sendRequest = sendRequest; +exports.getCalledSendRequest = getCalledSendRequest; +exports.clearCalledSendRequest = clearCalledSendRequest; +exports.safeCallbackApply = safeCallbackApply; +exports.getExtensionStackTrace = getExtensionStackTrace; + +// Called by C++. +exports.handleResponse = handleResponse; diff --git a/extensions/renderer/resources/set_icon.js b/extensions/renderer/resources/set_icon.js new file mode 100644 index 0000000..883fd67 --- /dev/null +++ b/extensions/renderer/resources/set_icon.js @@ -0,0 +1,131 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +var SetIconCommon = requireNative('setIcon').SetIconCommon; +var sendRequest = require('sendRequest').sendRequest; + +function loadImagePath(path, iconSize, actionType, callback) { + var img = new Image(); + img.onerror = function() { + console.error('Could not load ' + actionType + ' icon \'' + + path + '\'.'); + }; + img.onload = function() { + var canvas = document.createElement('canvas'); + canvas.width = img.width > iconSize ? iconSize : img.width; + canvas.height = img.height > iconSize ? iconSize : img.height; + + var canvas_context = canvas.getContext('2d'); + canvas_context.clearRect(0, 0, canvas.width, canvas.height); + canvas_context.drawImage(img, 0, 0, canvas.width, canvas.height); + var imageData = canvas_context.getImageData(0, 0, canvas.width, + canvas.height); + callback(imageData); + }; + img.src = path; +} + +function verifyImageData(imageData, iconSize) { + // Verify that this at least looks like an ImageData element. + // Unfortunately, we cannot use instanceof because the ImageData + // constructor is not public. + // + // We do this manually instead of using JSONSchema to avoid having these + // properties show up in the doc. + if (!('width' in imageData) || + !('height' in imageData) || + !('data' in imageData)) { + throw new Error( + 'The imageData property must contain an ImageData object or' + + ' dictionary of ImageData objects.'); + } + + if (imageData.width > iconSize || + imageData.height > iconSize) { + throw new Error( + 'The imageData property must contain an ImageData object that ' + + 'is no larger than ' + iconSize + ' pixels square.'); + } +} + +function setIcon(details, callback, name, parameters, actionType) { + var iconSizes = [19, 38]; + if ('iconIndex' in details) { + sendRequest(name, [details, callback], parameters); + } else if ('imageData' in details) { + if (typeof details.imageData == 'object') { + var isEmpty = true; + for (var i = 0; i < iconSizes.length; i++) { + var sizeKey = iconSizes[i].toString(); + if (sizeKey in details.imageData) { + verifyImageData(details.imageData[sizeKey], iconSizes[i]); + isEmpty =false; + } + } + + if (!isEmpty) { + sendRequest(name, [details, callback], parameters, + {nativeFunction: SetIconCommon}); + } else { + // If details.imageData is not dictionary with keys in set {'19', '38'}, + // it must be an ImageData object. + var sizeKey = iconSizes[0].toString(); + var imageData = details.imageData; + details.imageData = {}; + details.imageData[sizeKey] = imageData; + verifyImageData(details.imageData[sizeKey], iconSizes[0]); + sendRequest(name, [details, callback], parameters, + {nativeFunction: SetIconCommon}); + } + } else { + throw new Error('imageData property has unexpected type.'); + } + } else if ('path' in details) { + if (typeof details.path == 'object') { + details.imageData = {}; + var isEmpty = true; + var processIconSize = function(index) { + if (index == iconSizes.length) { + delete details.path; + if (isEmpty) + throw new Error('The path property must not be empty.'); + sendRequest(name, [details, callback], parameters, + {nativeFunction: SetIconCommon}); + return; + } + var sizeKey = iconSizes[index].toString(); + if (!(sizeKey in details.path)) { + processIconSize(index + 1); + return; + } + isEmpty = false; + loadImagePath(details.path[sizeKey], iconSizes[index], actionType, + function(imageData) { + details.imageData[sizeKey] = imageData; + processIconSize(index + 1); + }); + } + + processIconSize(0); + } else if (typeof details.path == 'string') { + var sizeKey = iconSizes[0].toString(); + details.imageData = {}; + loadImagePath(details.path, iconSizes[0], actionType, + function(imageData) { + details.imageData[sizeKey] = imageData; + delete details.path; + sendRequest(name, [details, callback], parameters, + {nativeFunction: SetIconCommon}); + }); + } else { + throw new Error('The path property should contain either string or ' + + 'dictionary of strings.'); + } + } else { + throw new Error( + 'Either the path or imageData property must be specified.'); + } +} + +exports.setIcon = setIcon; diff --git a/extensions/renderer/resources/storage_area.js b/extensions/renderer/resources/storage_area.js new file mode 100644 index 0000000..de737ab --- /dev/null +++ b/extensions/renderer/resources/storage_area.js @@ -0,0 +1,40 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +var normalizeArgumentsAndValidate = + require('schemaUtils').normalizeArgumentsAndValidate +var sendRequest = require('sendRequest').sendRequest; + +function extendSchema(schema) { + var extendedSchema = $Array.slice(schema); + extendedSchema.unshift({'type': 'string'}); + return extendedSchema; +} + +function StorageArea(namespace, schema) { + // Binds an API function for a namespace to its browser-side call, e.g. + // storage.sync.get('foo') -> (binds to) -> + // storage.get('sync', 'foo'). + // + // TODO(kalman): Put as a method on CustombindingObject and re-use (or + // even generate) for other APIs that need to do this. Same for other + // callers of registerCustomType(). + var self = this; + function bindApiFunction(functionName) { + self[functionName] = function() { + var funSchema = this.functionSchemas[functionName]; + var args = $Array.slice(arguments); + args = normalizeArgumentsAndValidate(args, funSchema); + return sendRequest( + 'storage.' + functionName, + $Array.concat([namespace], args), + extendSchema(funSchema.definition.parameters), + {preserveNullInObjects: true}); + }; + } + var apiFunctions = ['get', 'set', 'remove', 'clear', 'getBytesInUse']; + $Array.forEach(apiFunctions, bindApiFunction); +} + +exports.StorageArea = StorageArea; diff --git a/extensions/renderer/resources/test_custom_bindings.js b/extensions/renderer/resources/test_custom_bindings.js new file mode 100644 index 0000000..6e94b0c --- /dev/null +++ b/extensions/renderer/resources/test_custom_bindings.js @@ -0,0 +1,353 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// test_custom_bindings.js +// mini-framework for ExtensionApiTest browser tests + +var binding = require('binding').Binding.create('test'); + +var chrome = requireNative('chrome').GetChrome(); +var GetExtensionAPIDefinitionsForTest = + requireNative('apiDefinitions').GetExtensionAPIDefinitionsForTest; +var GetAvailability = requireNative('v8_context').GetAvailability; +var GetAPIFeatures = requireNative('test_features').GetAPIFeatures; +var uncaughtExceptionHandler = require('uncaught_exception_handler'); +var userGestures = requireNative('user_gestures'); + +binding.registerCustomHook(function(api) { + var chromeTest = api.compiledApi; + var apiFunctions = api.apiFunctions; + + chromeTest.tests = chromeTest.tests || []; + + var currentTest = null; + var lastTest = null; + var testsFailed = 0; + var testCount = 1; + var failureException = 'chrome.test.failure'; + + // Helper function to get around the fact that function names in javascript + // are read-only, and you can't assign one to anonymous functions. + function testName(test) { + return test ? (test.name || test.generatedName) : "(no test)"; + } + + function testDone() { + // Use setTimeout here to allow previous test contexts to be + // eligible for garbage collection. + setTimeout(chromeTest.runNextTest, 0); + } + + function allTestsDone() { + if (testsFailed == 0) { + chromeTest.notifyPass(); + } else { + chromeTest.notifyFail('Failed ' + testsFailed + ' of ' + + testCount + ' tests'); + } + } + + var pendingCallbacks = 0; + + apiFunctions.setHandleRequest('callbackAdded', function() { + pendingCallbacks++; + + var called = null; + return function() { + if (called != null) { + var redundantPrefix = 'Error\n'; + chrome.test.fail( + 'Callback has already been run. ' + + 'First call:\n' + + $String.slice(called, redundantPrefix.length) + '\n' + + 'Second call:\n' + + $String.slice(new Error().stack, redundantPrefix.length)); + } + called = new Error().stack; + + pendingCallbacks--; + if (pendingCallbacks == 0) { + chromeTest.succeed(); + } + }; + }); + + apiFunctions.setHandleRequest('runNextTest', function() { + // There may have been callbacks which were interrupted by failure + // exceptions. + pendingCallbacks = 0; + + lastTest = currentTest; + currentTest = chromeTest.tests.shift(); + + if (!currentTest) { + allTestsDone(); + return; + } + + try { + chromeTest.log("( RUN ) " + testName(currentTest)); + uncaughtExceptionHandler.setHandler(function(message, e) { + if (e !== failureException) + chromeTest.fail('uncaught exception: ' + message); + }); + currentTest.call(); + } catch (e) { + uncaughtExceptionHandler.handle(e.message, e); + } + }); + + apiFunctions.setHandleRequest('fail', function(message) { + chromeTest.log("( FAILED ) " + testName(currentTest)); + + var stack = {}; + Error.captureStackTrace(stack, chromeTest.fail); + + if (!message) + message = "FAIL (no message)"; + + message += "\n" + stack.stack; + console.log("[FAIL] " + testName(currentTest) + ": " + message); + testsFailed++; + testDone(); + + // Interrupt the rest of the test. + throw failureException; + }); + + apiFunctions.setHandleRequest('succeed', function() { + console.log("[SUCCESS] " + testName(currentTest)); + chromeTest.log("( SUCCESS )"); + testDone(); + }); + + apiFunctions.setHandleRequest('assertTrue', function(test, message) { + chromeTest.assertBool(test, true, message); + }); + + apiFunctions.setHandleRequest('assertFalse', function(test, message) { + chromeTest.assertBool(test, false, message); + }); + + apiFunctions.setHandleRequest('assertBool', + function(test, expected, message) { + if (test !== expected) { + if (typeof(test) == "string") { + if (message) + message = test + "\n" + message; + else + message = test; + } + chromeTest.fail(message); + } + }); + + apiFunctions.setHandleRequest('checkDeepEq', function(expected, actual) { + if ((expected === null) != (actual === null)) + return false; + + if (expected === actual) + return true; + + if (typeof(expected) !== typeof(actual)) + return false; + + for (var p in actual) { + if ($Object.hasOwnProperty(actual, p) && + !$Object.hasOwnProperty(expected, p)) { + return false; + } + } + for (var p in expected) { + if ($Object.hasOwnProperty(expected, p) && + !$Object.hasOwnProperty(actual, p)) { + return false; + } + } + + for (var p in expected) { + var eq = true; + switch (typeof(expected[p])) { + case 'object': + eq = chromeTest.checkDeepEq(expected[p], actual[p]); + break; + case 'function': + eq = (typeof(actual[p]) != 'undefined' && + expected[p].toString() == actual[p].toString()); + break; + default: + eq = (expected[p] == actual[p] && + typeof(expected[p]) == typeof(actual[p])); + break; + } + if (!eq) + return false; + } + return true; + }); + + apiFunctions.setHandleRequest('assertEq', + function(expected, actual, message) { + var error_msg = "API Test Error in " + testName(currentTest); + if (message) + error_msg += ": " + message; + if (typeof(expected) == 'object') { + if (!chromeTest.checkDeepEq(expected, actual)) { + // Note: these JSON.stringify calls may fail in tests that explicitly + // override JSON.stringfy, so surround in try-catch. + try { + error_msg += "\nActual: " + JSON.stringify(actual) + + "\nExpected: " + JSON.stringify(expected); + } catch (e) {} + chromeTest.fail(error_msg); + } + return; + } + if (expected != actual) { + chromeTest.fail(error_msg + + "\nActual: " + actual + "\nExpected: " + expected); + } + if (typeof(expected) != typeof(actual)) { + chromeTest.fail(error_msg + + " (type mismatch)\nActual Type: " + typeof(actual) + + "\nExpected Type:" + typeof(expected)); + } + }); + + apiFunctions.setHandleRequest('assertNoLastError', function() { + if (chrome.runtime.lastError != undefined) { + chromeTest.fail("lastError.message == " + + chrome.runtime.lastError.message); + } + }); + + apiFunctions.setHandleRequest('assertLastError', function(expectedError) { + chromeTest.assertEq(typeof(expectedError), 'string'); + chromeTest.assertTrue(chrome.runtime.lastError != undefined, + "No lastError, but expected " + expectedError); + chromeTest.assertEq(expectedError, chrome.runtime.lastError.message); + }); + + apiFunctions.setHandleRequest('assertThrows', + function(fn, self, args, message) { + chromeTest.assertTrue(typeof fn == 'function'); + try { + fn.apply(self, args); + chromeTest.fail('Did not throw error: ' + fn); + } catch (e) { + if (e != failureException && message !== undefined) { + if (message instanceof RegExp) { + chromeTest.assertTrue(message.test(e.message), + e.message + ' should match ' + message) + } else { + chromeTest.assertEq(message, e.message); + } + } + } + }); + + function safeFunctionApply(func, args) { + try { + if (func) + return $Function.apply(func, undefined, args); + } catch (e) { + var msg = "uncaught exception " + e; + chromeTest.fail(msg); + } + }; + + // Wrapper for generating test functions, that takes care of calling + // assertNoLastError() and (optionally) succeed() for you. + apiFunctions.setHandleRequest('callback', function(func, expectedError) { + if (func) { + chromeTest.assertEq(typeof(func), 'function'); + } + var callbackCompleted = chromeTest.callbackAdded(); + + return function() { + if (expectedError == null) { + chromeTest.assertNoLastError(); + } else { + chromeTest.assertLastError(expectedError); + } + + var result; + if (func) { + result = safeFunctionApply(func, arguments); + } + + callbackCompleted(); + return result; + }; + }); + + apiFunctions.setHandleRequest('listenOnce', function(event, func) { + var callbackCompleted = chromeTest.callbackAdded(); + var listener = function() { + event.removeListener(listener); + safeFunctionApply(func, arguments); + callbackCompleted(); + }; + event.addListener(listener); + }); + + apiFunctions.setHandleRequest('listenForever', function(event, func) { + var callbackCompleted = chromeTest.callbackAdded(); + + var listener = function() { + safeFunctionApply(func, arguments); + }; + + var done = function() { + event.removeListener(listener); + callbackCompleted(); + }; + + event.addListener(listener); + return done; + }); + + apiFunctions.setHandleRequest('callbackPass', function(func) { + return chromeTest.callback(func); + }); + + apiFunctions.setHandleRequest('callbackFail', function(expectedError, func) { + return chromeTest.callback(func, expectedError); + }); + + apiFunctions.setHandleRequest('runTests', function(tests) { + chromeTest.tests = tests; + testCount = chromeTest.tests.length; + chromeTest.runNextTest(); + }); + + apiFunctions.setHandleRequest('getApiDefinitions', function() { + return GetExtensionAPIDefinitionsForTest(); + }); + + apiFunctions.setHandleRequest('getApiFeatures', function() { + return GetAPIFeatures(); + }); + + apiFunctions.setHandleRequest('isProcessingUserGesture', function() { + return userGestures.IsProcessingUserGesture(); + }); + + apiFunctions.setHandleRequest('runWithUserGesture', function(callback) { + chromeTest.assertEq(typeof(callback), 'function'); + return userGestures.RunWithUserGesture(callback); + }); + + apiFunctions.setHandleRequest('runWithoutUserGesture', function(callback) { + chromeTest.assertEq(typeof(callback), 'function'); + return userGestures.RunWithoutUserGesture(callback); + }); + + apiFunctions.setHandleRequest('setExceptionHandler', function(callback) { + chromeTest.assertEq(typeof(callback), 'function'); + uncaughtExceptionHandler.setHandler(callback); + }); +}); + +exports.binding = binding.generate(); diff --git a/extensions/renderer/resources/uncaught_exception_handler.js b/extensions/renderer/resources/uncaught_exception_handler.js new file mode 100644 index 0000000..a3709b5 --- /dev/null +++ b/extensions/renderer/resources/uncaught_exception_handler.js @@ -0,0 +1,21 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Handles uncaught exceptions thrown by extensions. By default this is to +// log an error message, but tests may override this behaviour. + +var handler = function(message, e) { + console.error(message); +}; + +// |message| The message associated with the error. +// |e| The object that was thrown. +exports.handle = function(message, e) { + handler(message, e); +}; + +// |newHandler| A function which matches |exports.handle|. +exports.setHandler = function(newHandler) { + handler = newHandler; +}; diff --git a/extensions/renderer/resources/unload_event.js b/extensions/renderer/resources/unload_event.js new file mode 100644 index 0000000..753c2f0 --- /dev/null +++ b/extensions/renderer/resources/unload_event.js @@ -0,0 +1,33 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Special unload event. We don't use the DOM unload because that slows down +// tab shutdown. On the other hand, onUnload might not always fire, since +// Chrome will terminate renderers on shutdown (SuddenTermination). + +// Implement a primitive subset of the Event interface as needed, since if this +// was to use the real event object there would be a circular dependency. +var listeners = []; + +exports.addListener = function(listener) { + $Array.push(listeners, listener); +}; + +exports.removeListener = function(listener) { + for (var i = 0; i < listeners.length; ++i) { + if (listeners[i] == listener) { + $Array.splice(listeners, i, 1); + return; + } + } +}; + +exports.wasDispatched = false; + +// dispatch() is called from C++. +exports.dispatch = function() { + exports.wasDispatched = true; + for (var i = 0; i < listeners.length; ++i) + listeners[i](); +}; diff --git a/extensions/renderer/resources/utils.js b/extensions/renderer/resources/utils.js new file mode 100644 index 0000000..dd1ec2a --- /dev/null +++ b/extensions/renderer/resources/utils.js @@ -0,0 +1,127 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +var createClassWrapper = requireNative('utils').createClassWrapper; +var schemaRegistry = requireNative('schema_registry'); +var CHECK = requireNative('logging').CHECK; +var WARNING = requireNative('logging').WARNING; + +/** + * An object forEach. Calls |f| with each (key, value) pair of |obj|, using + * |self| as the target. + * @param {Object} obj The object to iterate over. + * @param {function} f The function to call in each iteration. + * @param {Object} self The object to use as |this| in each function call. + */ +function forEach(obj, f, self) { + for (var key in obj) { + if ($Object.hasOwnProperty(obj, key)) + $Function.call(f, self, key, obj[key]); + } +} + +/** + * Assuming |array_of_dictionaries| is structured like this: + * [{id: 1, ... }, {id: 2, ...}, ...], you can use + * lookup(array_of_dictionaries, 'id', 2) to get the dictionary with id == 2. + * @param {Array.<Object.<string, ?>>} array_of_dictionaries + * @param {string} field + * @param {?} value + */ +function lookup(array_of_dictionaries, field, value) { + var filter = function (dict) {return dict[field] == value;}; + var matches = array_of_dictionaries.filter(filter); + if (matches.length == 0) { + return undefined; + } else if (matches.length == 1) { + return matches[0] + } else { + throw new Error("Failed lookup of field '" + field + "' with value '" + + value + "'"); + } +} + +function loadTypeSchema(typeName, defaultSchema) { + var parts = $String.split(typeName, '.'); + if (parts.length == 1) { + if (defaultSchema == null) { + WARNING('Trying to reference "' + typeName + '" ' + + 'with neither namespace nor default schema.'); + return null; + } + var types = defaultSchema.types; + } else { + var schemaName = $Array.join($Array.slice(parts, 0, parts.length - 1), '.'); + var types = schemaRegistry.GetSchema(schemaName).types; + } + for (var i = 0; i < types.length; ++i) { + if (types[i].id == typeName) + return types[i]; + } + return null; +} + +/** + * Takes a private class implementation |cls| and exposes a subset of its + * methods |functions| and properties |properties| and |readonly| in a public + * wrapper class that it returns. Within bindings code, you can access the + * implementation from an instance of the wrapper class using + * privates(instance).impl, and from the implementation class you can access + * the wrapper using this.wrapper (or implInstance.wrapper if you have another + * instance of the implementation class). + * @param {string} name The name of the exposed wrapper class. + * @param {Object} cls The class implementation. + * @param {{functions: ?Array.<string>, + * properties: ?Array.<string>, + * readonly: ?Array.<string>}} exposed The names of properties on the + * implementation class to be exposed. |functions| represents the names of + * functions which should be delegated to the implementation; |properties| + * are gettable/settable properties and |readonly| are read-only properties. + */ +function expose(name, cls, exposed) { + var publicClass = createClassWrapper(name, cls); + + if ('functions' in exposed) { + $Array.forEach(exposed.functions, function(func) { + publicClass.prototype[func] = function() { + var impl = privates(this).impl; + return $Function.apply(impl[func], impl, arguments); + }; + }); + } + + if ('properties' in exposed) { + $Array.forEach(exposed.properties, function(prop) { + $Object.defineProperty(publicClass.prototype, prop, { + enumerable: true, + get: function() { + return privates(this).impl[prop]; + }, + set: function(value) { + var impl = privates(this).impl; + delete impl[prop]; + impl[prop] = value; + } + }); + }); + } + + if ('readonly' in exposed) { + $Array.forEach(exposed.readonly, function(readonly) { + $Object.defineProperty(publicClass.prototype, readonly, { + enumerable: true, + get: function() { + return privates(this).impl[readonly]; + }, + }); + }); + } + + return publicClass; +} + +exports.forEach = forEach; +exports.loadTypeSchema = loadTypeSchema; +exports.lookup = lookup; +exports.expose = expose; diff --git a/extensions/renderer/script_injection.cc b/extensions/renderer/script_injection.cc index d3113cd..036dfc7 100644 --- a/extensions/renderer/script_injection.cc +++ b/extensions/renderer/script_injection.cc @@ -19,7 +19,7 @@ #include "extensions/renderer/extension_helper.h" #include "extensions/renderer/script_context.h" #include "extensions/renderer/user_script_slave.h" -#include "grit/renderer_resources.h" +#include "grit/extensions_renderer_resources.h" #include "third_party/WebKit/public/web/WebDocument.h" #include "third_party/WebKit/public/web/WebFrame.h" #include "third_party/WebKit/public/web/WebScriptSource.h" |