Created
May 28, 2015 01:31
-
-
Save eyecatchup/272bdcc7173b983987a1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function($Object, $Function, privates, cls, superclass) {'use strict'; | |
function Event() { | |
var privateObj = $Object.create(cls.prototype); | |
$Function.apply(cls, privateObj, arguments); | |
privateObj.wrapper = this; | |
privates(this).impl = privateObj; | |
}; | |
if (superclass) { | |
Event.prototype = Object.create(superclass.prototype); | |
} | |
return Event; | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(define, require, requireNative, requireAsync, exports, console, privates,$Array, $Function, $JSON, $Object, $RegExp, $String, $Error) {'use strict';// 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 exceptionHandler = require('uncaught_exception_handler'); | |
var eventNatives = requireNative('event_natives'); | |
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) { | |
exceptionHandler.handle('Error in event handler for ' + | |
(this.eventName ? this.eventName : '(unknown)'), | |
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 = exceptionHandler.getStackTrace(); | |
}; | |
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; | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(define, require, requireNative, requireAsync, exports, console, privates,$Array, $Function, $JSON, $Object, $RegExp, $String, $Error) {'use strict';// 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 (Object.prototype.toString.call(value) == | |
"[object 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; | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(define, require, requireNative, requireAsync, exports, console, privates,$Array, $Function, $JSON, $Object, $RegExp, $String, $Error) {'use strict';// 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. | |
// chrome.runtime.messaging API implementation. | |
// 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 logging = requireNative('logging'); | |
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, | |
sourceFrameId, | |
guestProcessId, | |
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(); | |
// messaging_bindings.cc should ensure that this method only gets called for | |
// the right extension. | |
logging.CHECK(targetExtensionId == extensionId); | |
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 (sourceFrameId >= 0) | |
sender.frameId = sourceFrameId; | |
if (typeof guestProcessId != 'undefined') { | |
// Note that |guestProcessId| is not a standard field on MessageSender and | |
// should not be exposed to drive-by extensions; it is only exposed to | |
// component extensions. | |
logging.CHECK(processNatives.IsComponentExtension(), | |
"GuestProcessId can only be exposed to component extensions."); | |
sender.guestProcessId = guestProcessId; | |
} | |
if (typeof 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; | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(define, require, requireNative, requireAsync, exports, console, privates,$Array, $Function, $JSON, $Object, $RegExp, $String, $Error) {'use strict';// 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 exceptionHandler = require('uncaught_exception_handler'); | |
var lastError = require('lastError'); | |
var logging = requireNative('logging'); | |
var natives = requireNative('sendRequest'); | |
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) { | |
exceptionHandler.handle('Error in response to ' + name, e, request.stack); | |
} | |
} | |
// 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, request.callback], | |
responseList)); | |
} else 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)) { | |
// The native call caused an error, but the developer might not have | |
// checked runtime.lastError. | |
lastError.reportIfUnchecked(name, callerChrome, request.stack); | |
} | |
} finally { | |
delete requests[requestId]; | |
lastError.clear(chrome); | |
if (callerChrome !== chrome) | |
lastError.clear(callerChrome); | |
} | |
} | |
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. | |
// - stack: An optional string that contains the stack trace, to be displayed | |
// to the user if an error occurs. | |
function sendRequest(functionName, args, argSchemas, optArgs) { | |
calledSendRequest = true; | |
if (!optArgs) | |
optArgs = {}; | |
var request = prepareRequest(args, argSchemas); | |
request.stack = optArgs.stack || exceptionHandler.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; | |
// Called by C++. | |
exports.handleResponse = handleResponse; | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(define, require, requireNative, requireAsync, exports, console, privates,$Array, $Function, $JSON, $Object, $RegExp, $String, $Error) {'use strict';// 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 nativeDeepCopy = requireNative('utils').deepCopy; | |
var schemaRegistry = requireNative('schema_registry'); | |
var CHECK = requireNative('logging').CHECK; | |
var DCHECK = requireNative('logging').DCHECK; | |
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 {{superclass: ?Function, | |
* functions: ?Array<string>, | |
* properties: ?Array<string>, | |
* readonly: ?Array<string>}} exposed The names of properties on the | |
* implementation class to be exposed. |superclass| represents the | |
* constructor of the class to be used as the superclass of the exposed | |
* class; |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, exposed.superclass); | |
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; | |
} | |
/** | |
* Returns a deep copy of |value|. The copy will have no references to nested | |
* values of |value|. | |
*/ | |
function deepCopy(value) { | |
return nativeDeepCopy(value); | |
} | |
/** | |
* Wrap an asynchronous API call to a function |func| in a promise. The | |
* remaining arguments will be passed to |func|. Returns a promise that will be | |
* resolved to the result passed to the callback or rejected if an error occurs | |
* (if chrome.runtime.lastError is set). If there are multiple results, the | |
* promise will be resolved with an array containing those results. | |
* | |
* For example, | |
* promise(chrome.storage.get, 'a').then(function(result) { | |
* // Use result. | |
* }).catch(function(error) { | |
* // Report error.message. | |
* }); | |
*/ | |
function promise(func) { | |
var args = $Array.slice(arguments, 1); | |
DCHECK(typeof func == 'function'); | |
return new Promise(function(resolve, reject) { | |
args.push(function() { | |
if (chrome.runtime.lastError) { | |
reject(new Error(chrome.runtime.lastError)); | |
return; | |
} | |
if (arguments.length <= 1) | |
resolve(arguments[0]); | |
else | |
resolve($Array.slice(arguments)); | |
}); | |
$Function.apply(func, null, args); | |
}); | |
} | |
exports.forEach = forEach; | |
exports.loadTypeSchema = loadTypeSchema; | |
exports.lookup = lookup; | |
exports.expose = expose; | |
exports.deepCopy = deepCopy; | |
exports.promise = promise; | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment