blob: 8961ce56fc112758d971b496ccaa842b070af33b [file] [log] [blame]
// Copyright (c) 2012 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 eventBindingsNatives = requireNative('event_bindings');
var AttachEvent = eventBindingsNatives.AttachEvent;
var DetachEvent = eventBindingsNatives.DetachEvent;
var sendRequest = require('sendRequest').sendRequest;
var utils = require('utils');
var chromeHidden = requireNative('chrome_hidden').GetChromeHidden();
var GetExtensionAPIDefinition =
requireNative('apiDefinitions').GetExtensionAPIDefinition;
// 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 = GetExtensionAPIDefinition("events")[0];
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');
}
// Local implementation of JSON.parse & JSON.stringify that protect us
// from being clobbered by an extension.
//
// TODO(aa): This makes me so sad. We shouldn't need it, as we can just pass
// Values directly over IPC without serializing to strings and use
// JSONValueConverter.
chromeHidden.JSON = new (function() {
var $Object = Object;
var $Array = Array;
var $jsonStringify = JSON.stringify;
var $jsonParse = JSON.parse;
this.stringify = function(thing) {
var customizedObjectToJSON = $Object.prototype.toJSON;
var customizedArrayToJSON = $Array.prototype.toJSON;
if (customizedObjectToJSON !== undefined) {
$Object.prototype.toJSON = null;
}
if (customizedArrayToJSON !== undefined) {
$Array.prototype.toJSON = null;
}
try {
return $jsonStringify(thing);
} finally {
if (customizedObjectToJSON !== undefined) {
$Object.prototype.toJSON = customizedObjectToJSON;
}
if (customizedArrayToJSON !== undefined) {
$Array.prototype.toJSON = customizedArrayToJSON;
}
}
};
this.parse = function(thing) {
return $jsonParse(thing);
};
})();
// 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:
// chrome.tabs.onChanged = new chrome.Event("tab-changed");
// chrome.tabs.onChanged.addListener(function(data) { alert(data); });
// chromeHidden.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".
chrome.Event = function(opt_eventName, opt_argSchemas, opt_eventOptions) {
this.eventName_ = opt_eventName;
this.listeners_ = [];
this.eventOptions_ = opt_eventOptions ||
{"supportsListeners": true, "supportsRules": false};
if (this.eventOptions_.supportsRules && !opt_eventName)
throw new Error("Events that support rules require an event name.");
// Validate event arguments (the data that is passed to the callbacks)
// if we are in debug.
if (opt_argSchemas &&
chromeHidden.validateCallbacks &&
chromeHidden.validate) {
this.validateEventArgs_ = function(args) {
try {
chromeHidden.validate(args, opt_argSchemas);
} catch (exception) {
return "Event validation error during " + opt_eventName + " -- " +
exception;
}
};
} else {
this.validateEventArgs_ = function() {}
}
};
// 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 = {};
chromeHidden.Event = {};
chromeHidden.Event.registerArgumentMassager = function(name, fn) {
if (eventArgumentMassagers[name])
throw new Error("Massager already registered for event: " + name);
eventArgumentMassagers[name] = fn;
};
// Dispatches a named event with the given JSON array, which is deserialized
// before dispatch. The JSON array is the list of arguments that will be
// sent with the event callback.
chromeHidden.Event.dispatchJSON = function(name, args) {
if (attachedNamedEvents[name]) {
if (args) {
args = chromeHidden.JSON.parse(args);
if (eventArgumentMassagers[name])
eventArgumentMassagers[name](args);
}
var result = attachedNamedEvents[name].dispatch.apply(
attachedNamedEvents[name], args);
if (result && result.validationErrors)
return result.validationErrors;
}
};
// Dispatches a named event with the given arguments, supplied as an array.
chromeHidden.Event.dispatch = function(name, args) {
if (attachedNamedEvents[name]) {
attachedNamedEvents[name].dispatch.apply(
attachedNamedEvents[name], args);
}
};
// Test if a named event has any listeners.
chromeHidden.Event.hasListener = function(name) {
return (attachedNamedEvents[name] &&
attachedNamedEvents[name].listeners_.length > 0);
};
// Registers a callback to be called when this event is dispatched.
chrome.Event.prototype.addListener = function(cb) {
if (!this.eventOptions_.supportsListeners)
throw new Error("This event does not support listeners.");
if (this.listeners_.length == 0) {
this.attach_();
}
this.listeners_.push(cb);
};
// Unregisters a callback.
chrome.Event.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;
}
this.listeners_.splice(idx, 1);
if (this.listeners_.length == 0) {
this.detach_(true);
}
};
// Test if the given callback is registered for this event.
chrome.Event.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.
chrome.Event.prototype.hasListeners = function() {
if (!this.eventOptions_.supportsListeners)
throw new Error("This event does not support listeners.");
return this.listeners_.length > 0;
};
// Returns the index of the given callback if registered, or -1 if not
// found.
chrome.Event.prototype.findListener_ = function(cb) {
for (var i = 0; i < this.listeners_.length; i++) {
if (this.listeners_[i] == cb) {
return i;
}
}
return -1;
};
// Dispatches this event object to all listeners, passing all supplied
// arguments to this function each listener.
chrome.Event.prototype.dispatch = function(varargs) {
if (!this.eventOptions_.supportsListeners)
throw new Error("This event does not support listeners.");
var args = Array.prototype.slice.call(arguments);
var validationErrors = this.validateEventArgs_(args);
if (validationErrors) {
return {validationErrors: validationErrors};
}
var results = [];
for (var i = 0; i < this.listeners_.length; i++) {
try {
var result = this.listeners_[i].apply(null, args);
if (result !== undefined)
results.push(result);
} catch (e) {
console.error("Error in event handler for '" + this.eventName_ +
"': " + e.message + ' ' + e.stack);
}
}
if (results.length)
return {results: results};
};
// Attaches this event object to its name. Only one object can have a given
// name.
chrome.Event.prototype.attach_ = function() {
AttachEvent(this.eventName_);
allAttachedEvents[allAttachedEvents.length] = this;
if (!this.eventName_)
return;
if (attachedNamedEvents[this.eventName_]) {
throw new Error("chrome.Event '" + this.eventName_ +
"' is already attached.");
}
attachedNamedEvents[this.eventName_] = this;
};
// Detaches this event object from its name.
chrome.Event.prototype.detach_ = function(manual) {
var i = allAttachedEvents.indexOf(this);
if (i >= 0)
delete allAttachedEvents[i];
DetachEvent(this.eventName_, manual);
if (!this.eventName_)
return;
if (!attachedNamedEvents[this.eventName_]) {
throw new Error("chrome.Event '" + this.eventName_ +
"' is not attached.");
}
delete attachedNamedEvents[this.eventName_];
};
chrome.Event.prototype.destroy_ = function() {
this.listeners_ = [];
this.validateEventArgs_ = [];
this.detach_(false);
};
chrome.Event.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);
rules.forEach(function(rule) {
chromeHidden.validate([rule.conditions], [conditionsSchema]);
chromeHidden.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();
sendRequest("events.addRules", [this.eventName_, rules, opt_cb],
ruleFunctionSchemas.addRules.parameters);
}
chrome.Event.prototype.removeRules = function(ruleIdentifiers, opt_cb) {
if (!this.eventOptions_.supportsRules)
throw new Error("This event does not support rules.");
ensureRuleSchemasLoaded();
sendRequest("events.removeRules",
[this.eventName_, ruleIdentifiers, opt_cb],
ruleFunctionSchemas.removeRules.parameters);
}
chrome.Event.prototype.getRules = function(ruleIdentifiers, cb) {
if (!this.eventOptions_.supportsRules)
throw new Error("This event does not support rules.");
ensureRuleSchemasLoaded();
sendRequest("events.getRules",
[this.eventName_, ruleIdentifiers, cb],
ruleFunctionSchemas.getRules.parameters);
}
// Special load events: 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).
chromeHidden.onLoad = new chrome.Event();
chromeHidden.onUnload = new chrome.Event();
chromeHidden.dispatchOnLoad =
chromeHidden.onLoad.dispatch.bind(chromeHidden.onLoad);
chromeHidden.dispatchOnUnload = function() {
chromeHidden.onUnload.dispatch();
for (var i = 0; i < allAttachedEvents.length; ++i) {
var event = allAttachedEvents[i];
if (event)
event.detach_(false);
}
};
chromeHidden.dispatchError = function(msg) {
console.error(msg);
};
exports.Event = chrome.Event;