summaryrefslogtreecommitdiffstats
path: root/extensions/renderer/resources/event.js
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/renderer/resources/event.js')
-rw-r--r--extensions/renderer/resources/event.js528
1 files changed, 528 insertions, 0 deletions
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;