summaryrefslogtreecommitdiffstats
path: root/extensions
diff options
context:
space:
mode:
authoryoz@chromium.org <yoz@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-06-03 17:15:54 +0000
committeryoz@chromium.org <yoz@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-06-03 17:15:54 +0000
commit70ebc3142a8b434411b4fc442fabba334a165ae3 (patch)
treeaeff96f2161d25d27c92178862920fcbf790967f /extensions
parente15801b5bbfd68e19c91bce40a91e6bf7f6d675d (diff)
downloadchromium_src-70ebc3142a8b434411b4fc442fabba334a165ae3.zip
chromium_src-70ebc3142a8b434411b4fc442fabba334a165ae3.tar.gz
chromium_src-70ebc3142a8b434411b4fc442fabba334a165ae3.tar.bz2
Move some extensions renderer resources to extensions_renderer_resources.grd.
This breaks the remaining dependency from src/extensions to chrome resources files. BUG=368334 Review URL: https://codereview.chromium.org/307833002 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@274558 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'extensions')
-rw-r--r--extensions/DEPS4
-rw-r--r--extensions/extensions_resources.gyp12
-rw-r--r--extensions/renderer/dispatcher.cc2
-rw-r--r--extensions/renderer/resources/app_runtime_custom_bindings.js56
-rw-r--r--extensions/renderer/resources/binding.js434
-rw-r--r--extensions/renderer/resources/context_menus_custom_bindings.js101
-rw-r--r--extensions/renderer/resources/entry_id_manager.js52
-rw-r--r--extensions/renderer/resources/event.js528
-rw-r--r--extensions/renderer/resources/extension_custom_bindings.js113
-rw-r--r--extensions/renderer/resources/extensions_renderer_resources.grd45
-rw-r--r--extensions/renderer/resources/greasemonkey_api.js82
-rw-r--r--extensions/renderer/resources/i18n_custom_bindings.js41
-rw-r--r--extensions/renderer/resources/image_util.js82
-rw-r--r--extensions/renderer/resources/json_schema.js525
-rw-r--r--extensions/renderer/resources/last_error.js124
-rw-r--r--extensions/renderer/resources/messaging.js386
-rw-r--r--extensions/renderer/resources/messaging_utils.js53
-rw-r--r--extensions/renderer/resources/permissions_custom_bindings.js97
-rw-r--r--extensions/renderer/resources/platform_app.css35
-rw-r--r--extensions/renderer/resources/platform_app.js207
-rw-r--r--extensions/renderer/resources/runtime_custom_bindings.js205
-rw-r--r--extensions/renderer/resources/schema_utils.js156
-rw-r--r--extensions/renderer/resources/send_request.js178
-rw-r--r--extensions/renderer/resources/set_icon.js131
-rw-r--r--extensions/renderer/resources/storage_area.js40
-rw-r--r--extensions/renderer/resources/test_custom_bindings.js353
-rw-r--r--extensions/renderer/resources/uncaught_exception_handler.js21
-rw-r--r--extensions/renderer/resources/unload_event.js33
-rw-r--r--extensions/renderer/resources/utils.js127
-rw-r--r--extensions/renderer/script_injection.cc2
30 files changed, 4220 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_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 04de939..a84a42d 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 4db534a..9bbf4a6 100644
--- a/extensions/renderer/script_injection.cc
+++ b/extensions/renderer/script_injection.cc
@@ -16,7 +16,7 @@
#include "extensions/renderer/extension_groups.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"