1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 var exceptionHandler
= require('uncaught_exception_handler');
6 var eventNatives
= requireNative('event_natives');
7 var logging
= requireNative('logging');
8 var schemaRegistry
= requireNative('schema_registry');
9 var sendRequest
= require('sendRequest').sendRequest
;
10 var utils
= require('utils');
11 var validate
= require('schemaUtils').validate
;
13 // Schemas for the rule-style functions on the events API that
14 // only need to be generated occasionally, so populate them lazily.
15 var ruleFunctionSchemas
= {
16 // These values are set lazily:
22 // This function ensures that |ruleFunctionSchemas| is populated.
23 function ensureRuleSchemasLoaded() {
24 if (ruleFunctionSchemas
.addRules
)
26 var eventsSchema
= schemaRegistry
.GetSchema("events");
27 var eventType
= utils
.lookup(eventsSchema
.types
, 'id', 'events.Event');
29 ruleFunctionSchemas
.addRules
=
30 utils
.lookup(eventType
.functions
, 'name', 'addRules');
31 ruleFunctionSchemas
.getRules
=
32 utils
.lookup(eventType
.functions
, 'name', 'getRules');
33 ruleFunctionSchemas
.removeRules
=
34 utils
.lookup(eventType
.functions
, 'name', 'removeRules');
37 // A map of event names to the event object that is registered to that name.
38 var attachedNamedEvents
= {};
40 // A map of functions that massage event arguments before they are dispatched.
41 // Key is event name, value is function.
42 var eventArgumentMassagers
= {};
44 // An attachment strategy for events that aren't attached to the browser.
45 // This applies to events with the "unmanaged" option and events without
47 var NullAttachmentStrategy = function(event
) {
50 NullAttachmentStrategy
.prototype.onAddedListener
=
53 NullAttachmentStrategy
.prototype.onRemovedListener
=
56 NullAttachmentStrategy
.prototype.detach = function(manual
) {
58 NullAttachmentStrategy
.prototype.getListenersByIDs = function(ids
) {
59 // |ids| is for filtered events only.
60 return this.event_
.listeners
;
63 // Handles adding/removing/dispatching listeners for unfiltered events.
64 var UnfilteredAttachmentStrategy = function(event
) {
68 UnfilteredAttachmentStrategy
.prototype.onAddedListener
=
70 // Only attach / detach on the first / last listener removed.
71 if (this.event_
.listeners
.length
== 0)
72 eventNatives
.AttachEvent(this.event_
.eventName
);
75 UnfilteredAttachmentStrategy
.prototype.onRemovedListener
=
77 if (this.event_
.listeners
.length
== 0)
81 UnfilteredAttachmentStrategy
.prototype.detach = function(manual
) {
82 eventNatives
.DetachEvent(this.event_
.eventName
, manual
);
85 UnfilteredAttachmentStrategy
.prototype.getListenersByIDs = function(ids
) {
86 // |ids| is for filtered events only.
87 return this.event_
.listeners
;
90 var FilteredAttachmentStrategy = function(event
) {
92 this.listenerMap_
= {};
95 FilteredAttachmentStrategy
.idToEventMap
= {};
97 FilteredAttachmentStrategy
.prototype.onAddedListener = function(listener
) {
98 var id
= eventNatives
.AttachFilteredEvent(this.event_
.eventName
,
99 listener
.filters
|| {});
101 throw new Error("Can't add listener");
103 this.listenerMap_
[id
] = listener
;
104 FilteredAttachmentStrategy
.idToEventMap
[id
] = this.event_
;
107 FilteredAttachmentStrategy
.prototype.onRemovedListener = function(listener
) {
108 this.detachListener(listener
, true);
111 FilteredAttachmentStrategy
.prototype.detachListener
=
112 function(listener
, manual
) {
113 if (listener
.id
== undefined)
114 throw new Error("listener.id undefined - '" + listener
+ "'");
115 var id
= listener
.id
;
116 delete this.listenerMap_
[id
];
117 delete FilteredAttachmentStrategy
.idToEventMap
[id
];
118 eventNatives
.DetachFilteredEvent(id
, manual
);
121 FilteredAttachmentStrategy
.prototype.detach = function(manual
) {
122 for (var i
in this.listenerMap_
)
123 this.detachListener(this.listenerMap_
[i
], manual
);
126 FilteredAttachmentStrategy
.prototype.getListenersByIDs = function(ids
) {
128 for (var i
= 0; i
< ids
.length
; i
++)
129 $Array
.push(result
, this.listenerMap_
[ids
[i
]]);
133 function parseEventOptions(opt_eventOptions
) {
134 function merge(dest
, src
) {
136 if (!$Object
.hasOwnProperty(dest
, k
)) {
142 var options
= opt_eventOptions
|| {};
144 // Event supports adding listeners with filters ("filtered events"), for
145 // example as used in the webNavigation API.
147 // event.addListener(listener, [filter1, filter2]);
148 supportsFilters
: false,
150 // Events supports vanilla events. Most APIs use these.
152 // event.addListener(listener);
153 supportsListeners
: true,
155 // Event supports adding rules ("declarative events") rather than
156 // listeners, for example as used in the declarativeWebRequest API.
158 // event.addRules([rule1, rule2]);
159 supportsRules
: false,
161 // Event is unmanaged in that the browser has no knowledge of its
162 // existence; it's never invoked, doesn't keep the renderer alive, and
163 // the bindings system has no knowledge of it.
165 // Both events created by user code (new chrome.Event()) and messaging
166 // events are unmanaged, though in the latter case the browser *does*
167 // interact indirectly with them via IPCs written by hand.
173 // Event object. If opt_eventName is provided, this object represents
174 // the unique instance of that named event, and dispatching an event
175 // with that name will route through this object's listeners. Note that
176 // opt_eventName is required for events that support rules.
179 // var Event = require('event_bindings').Event;
180 // chrome.tabs.onChanged = new Event("tab-changed");
181 // chrome.tabs.onChanged.addListener(function(data) { alert(data); });
182 // Event.dispatch("tab-changed", "hi");
183 // will result in an alert dialog that says 'hi'.
185 // If opt_eventOptions exists, it is a dictionary that contains the boolean
186 // entries "supportsListeners" and "supportsRules".
187 // If opt_webViewInstanceId exists, it is an integer uniquely identifying a
188 // <webview> tag within the embedder. If it does not exist, then this is an
189 // extension event rather than a <webview> event.
190 var EventImpl = function(opt_eventName
, opt_argSchemas
, opt_eventOptions
,
191 opt_webViewInstanceId
) {
192 this.eventName
= opt_eventName
;
193 this.argSchemas
= opt_argSchemas
;
195 this.eventOptions
= parseEventOptions(opt_eventOptions
);
196 this.webViewInstanceId
= opt_webViewInstanceId
|| 0;
198 if (!this.eventName
) {
199 if (this.eventOptions
.supportsRules
)
200 throw new Error("Events that support rules require an event name.");
201 // Events without names cannot be managed by the browser by definition
202 // (the browser has no way of identifying them).
203 this.eventOptions
.unmanaged
= true;
206 // Track whether the event has been destroyed to help track down the cause
207 // of http://crbug.com/258526.
208 // This variable will eventually hold the stack trace of the destroy call.
209 // TODO(kalman): Delete this and replace with more sound logic that catches
210 // when events are used without being *attached*.
211 this.destroyed
= null;
213 if (this.eventOptions
.unmanaged
)
214 this.attachmentStrategy
= new NullAttachmentStrategy(this);
215 else if (this.eventOptions
.supportsFilters
)
216 this.attachmentStrategy
= new FilteredAttachmentStrategy(this);
218 this.attachmentStrategy
= new UnfilteredAttachmentStrategy(this);
221 // callback is a function(args, dispatch). args are the args we receive from
222 // dispatchEvent(), and dispatch is a function(args) that dispatches args to
224 function registerArgumentMassager(name
, callback
) {
225 if (eventArgumentMassagers
[name
])
226 throw new Error("Massager already registered for event: " + name
);
227 eventArgumentMassagers
[name
] = callback
;
230 // Dispatches a named event with the given argument array. The args array is
231 // the list of arguments that will be sent to the event callback.
232 function dispatchEvent(name
, args
, filteringInfo
) {
233 var listenerIDs
= [];
236 listenerIDs
= eventNatives
.MatchAgainstEventFilter(name
, filteringInfo
);
238 var event
= attachedNamedEvents
[name
];
242 var dispatchArgs = function(args
) {
243 var result
= event
.dispatch_(args
, listenerIDs
);
245 logging
.DCHECK(!result
.validationErrors
, result
.validationErrors
);
249 if (eventArgumentMassagers
[name
])
250 eventArgumentMassagers
[name
](args
, dispatchArgs
);
255 // Registers a callback to be called when this event is dispatched.
256 EventImpl
.prototype.addListener = function(cb
, filters
) {
257 if (!this.eventOptions
.supportsListeners
)
258 throw new Error("This event does not support listeners.");
259 if (this.eventOptions
.maxListeners
&&
260 this.getListenerCount_() >= this.eventOptions
.maxListeners
) {
261 throw new Error("Too many listeners for " + this.eventName
);
264 if (!this.eventOptions
.supportsFilters
)
265 throw new Error("This event does not support filters.");
266 if (filters
.url
&& !(filters
.url
instanceof Array
))
267 throw new Error("filters.url should be an array.");
268 if (filters
.serviceType
&&
269 !(typeof filters
.serviceType
=== 'string')) {
270 throw new Error("filters.serviceType should be a string.")
273 var listener
= {callback
: cb
, filters
: filters
};
274 this.attach_(listener
);
275 $Array
.push(this.listeners
, listener
);
278 EventImpl
.prototype.attach_ = function(listener
) {
279 this.attachmentStrategy
.onAddedListener(listener
);
281 if (this.listeners
.length
== 0) {
282 if (this.eventName
) {
283 if (attachedNamedEvents
[this.eventName
]) {
284 throw new Error("Event '" + this.eventName
+
285 "' is already attached.");
287 attachedNamedEvents
[this.eventName
] = this;
292 // Unregisters a callback.
293 EventImpl
.prototype.removeListener = function(cb
) {
294 if (!this.eventOptions
.supportsListeners
)
295 throw new Error("This event does not support listeners.");
297 var idx
= this.findListener_(cb
);
301 var removedListener
= $Array
.splice(this.listeners
, idx
, 1)[0];
302 this.attachmentStrategy
.onRemovedListener(removedListener
);
304 if (this.listeners
.length
== 0) {
305 if (this.eventName
) {
306 if (!attachedNamedEvents
[this.eventName
]) {
308 "Event '" + this.eventName
+ "' is not attached.");
310 delete attachedNamedEvents
[this.eventName
];
315 // Test if the given callback is registered for this event.
316 EventImpl
.prototype.hasListener = function(cb
) {
317 if (!this.eventOptions
.supportsListeners
)
318 throw new Error("This event does not support listeners.");
319 return this.findListener_(cb
) > -1;
322 // Test if any callbacks are registered for this event.
323 EventImpl
.prototype.hasListeners = function() {
324 return this.getListenerCount_() > 0;
327 // Returns the number of listeners on this event.
328 EventImpl
.prototype.getListenerCount_ = function() {
329 if (!this.eventOptions
.supportsListeners
)
330 throw new Error("This event does not support listeners.");
331 return this.listeners
.length
;
334 // Returns the index of the given callback if registered, or -1 if not
336 EventImpl
.prototype.findListener_ = function(cb
) {
337 for (var i
= 0; i
< this.listeners
.length
; i
++) {
338 if (this.listeners
[i
].callback
== cb
) {
346 EventImpl
.prototype.dispatch_ = function(args
, listenerIDs
) {
347 if (this.destroyed
) {
348 throw new Error(this.eventName
+ ' was already destroyed at: ' +
351 if (!this.eventOptions
.supportsListeners
)
352 throw new Error("This event does not support listeners.");
354 if (this.argSchemas
&& logging
.DCHECK_IS_ON()) {
356 validate(args
, this.argSchemas
);
358 e
.message
+= ' in ' + this.eventName
;
363 // Make a copy of the listeners in case the listener list is modified
364 // while dispatching the event.
365 var listeners
= $Array
.slice(
366 this.attachmentStrategy
.getListenersByIDs(listenerIDs
));
369 for (var i
= 0; i
< listeners
.length
; i
++) {
371 var result
= this.wrapper
.dispatchToListener(listeners
[i
].callback
,
373 if (result
!== undefined)
374 $Array
.push(results
, result
);
376 exceptionHandler
.handle('Error in event handler for ' +
377 (this.eventName
? this.eventName
: '(unknown)'),
382 return {results
: results
};
385 // Can be overridden to support custom dispatching.
386 EventImpl
.prototype.dispatchToListener = function(callback
, args
) {
387 return $Function
.apply(callback
, null, args
);
390 // Dispatches this event object to all listeners, passing all supplied
391 // arguments to this function each listener.
392 EventImpl
.prototype.dispatch = function(varargs
) {
393 return this.dispatch_($Array
.slice(arguments
), undefined);
396 // Detaches this event object from its name.
397 EventImpl
.prototype.detach_ = function() {
398 this.attachmentStrategy
.detach(false);
401 EventImpl
.prototype.destroy_ = function() {
402 this.listeners
.length
= 0;
404 this.destroyed
= exceptionHandler
.getStackTrace();
407 EventImpl
.prototype.addRules = function(rules
, opt_cb
) {
408 if (!this.eventOptions
.supportsRules
)
409 throw new Error("This event does not support rules.");
411 // Takes a list of JSON datatype identifiers and returns a schema fragment
412 // that verifies that a JSON object corresponds to an array of only these
414 function buildArrayOfChoicesSchema(typesList
) {
418 'choices': typesList
.map(function(el
) {return {'$ref': el
};})
423 // Validate conditions and actions against specific schemas of this
424 // event object type.
425 // |rules| is an array of JSON objects that follow the Rule type of the
426 // declarative extension APIs. |conditions| is an array of JSON type
427 // identifiers that are allowed to occur in the conditions attribute of each
428 // rule. Likewise, |actions| is an array of JSON type identifiers that are
429 // allowed to occur in the actions attribute of each rule.
430 function validateRules(rules
, conditions
, actions
) {
431 var conditionsSchema
= buildArrayOfChoicesSchema(conditions
);
432 var actionsSchema
= buildArrayOfChoicesSchema(actions
);
433 $Array
.forEach(rules
, function(rule
) {
434 validate([rule
.conditions
], [conditionsSchema
]);
435 validate([rule
.actions
], [actionsSchema
]);
439 if (!this.eventOptions
.conditions
|| !this.eventOptions
.actions
) {
440 throw new Error('Event ' + this.eventName
+ ' misses ' +
441 'conditions or actions in the API specification.');
445 this.eventOptions
.conditions
,
446 this.eventOptions
.actions
);
448 ensureRuleSchemasLoaded();
449 // We remove the first parameter from the validation to give the user more
450 // meaningful error messages.
451 validate([this.webViewInstanceId
, rules
, opt_cb
],
453 $Array
.slice(ruleFunctionSchemas
.addRules
.parameters
), 1));
456 [this.eventName
, this.webViewInstanceId
, rules
, opt_cb
],
457 ruleFunctionSchemas
.addRules
.parameters
);
460 EventImpl
.prototype.removeRules = function(ruleIdentifiers
, opt_cb
) {
461 if (!this.eventOptions
.supportsRules
)
462 throw new Error("This event does not support rules.");
463 ensureRuleSchemasLoaded();
464 // We remove the first parameter from the validation to give the user more
465 // meaningful error messages.
466 validate([this.webViewInstanceId
, ruleIdentifiers
, opt_cb
],
468 $Array
.slice(ruleFunctionSchemas
.removeRules
.parameters
), 1));
469 sendRequest("events.removeRules",
471 this.webViewInstanceId
,
474 ruleFunctionSchemas
.removeRules
.parameters
);
477 EventImpl
.prototype.getRules = function(ruleIdentifiers
, cb
) {
478 if (!this.eventOptions
.supportsRules
)
479 throw new Error("This event does not support rules.");
480 ensureRuleSchemasLoaded();
481 // We remove the first parameter from the validation to give the user more
482 // meaningful error messages.
483 validate([this.webViewInstanceId
, ruleIdentifiers
, cb
],
485 $Array
.slice(ruleFunctionSchemas
.getRules
.parameters
), 1));
489 [this.eventName
, this.webViewInstanceId
, ruleIdentifiers
, cb
],
490 ruleFunctionSchemas
.getRules
.parameters
);
493 var Event
= utils
.expose('Event', EventImpl
, { functions
: [
498 'dispatchToListener',
505 // NOTE: Event is (lazily) exposed as chrome.Event from dispatcher.cc.
506 exports
.Event
= Event
;
508 exports
.dispatchEvent
= dispatchEvent
;
509 exports
.parseEventOptions
= parseEventOptions
;
510 exports
.registerArgumentMassager
= registerArgumentMassager
;