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
;
12 var unloadEvent
= require('unload_event');
14 // Schemas for the rule-style functions on the events API that
15 // only need to be generated occasionally, so populate them lazily.
16 var ruleFunctionSchemas
= {
17 // These values are set lazily:
23 // This function ensures that |ruleFunctionSchemas| is populated.
24 function ensureRuleSchemasLoaded() {
25 if (ruleFunctionSchemas
.addRules
)
27 var eventsSchema
= schemaRegistry
.GetSchema("events");
28 var eventType
= utils
.lookup(eventsSchema
.types
, 'id', 'events.Event');
30 ruleFunctionSchemas
.addRules
=
31 utils
.lookup(eventType
.functions
, 'name', 'addRules');
32 ruleFunctionSchemas
.getRules
=
33 utils
.lookup(eventType
.functions
, 'name', 'getRules');
34 ruleFunctionSchemas
.removeRules
=
35 utils
.lookup(eventType
.functions
, 'name', 'removeRules');
38 // A map of event names to the event object that is registered to that name.
39 var attachedNamedEvents
= {};
41 // An array of all attached event objects, used for detaching on unload.
42 var allAttachedEvents
= [];
44 // A map of functions that massage event arguments before they are dispatched.
45 // Key is event name, value is function.
46 var eventArgumentMassagers
= {};
48 // An attachment strategy for events that aren't attached to the browser.
49 // This applies to events with the "unmanaged" option and events without
51 var NullAttachmentStrategy = function(event
) {
54 NullAttachmentStrategy
.prototype.onAddedListener
=
57 NullAttachmentStrategy
.prototype.onRemovedListener
=
60 NullAttachmentStrategy
.prototype.detach = function(manual
) {
62 NullAttachmentStrategy
.prototype.getListenersByIDs = function(ids
) {
63 // |ids| is for filtered events only.
64 return this.event_
.listeners
;
67 // Handles adding/removing/dispatching listeners for unfiltered events.
68 var UnfilteredAttachmentStrategy = function(event
) {
72 UnfilteredAttachmentStrategy
.prototype.onAddedListener
=
74 // Only attach / detach on the first / last listener removed.
75 if (this.event_
.listeners
.length
== 0)
76 eventNatives
.AttachEvent(this.event_
.eventName
);
79 UnfilteredAttachmentStrategy
.prototype.onRemovedListener
=
81 if (this.event_
.listeners
.length
== 0)
85 UnfilteredAttachmentStrategy
.prototype.detach = function(manual
) {
86 eventNatives
.DetachEvent(this.event_
.eventName
, manual
);
89 UnfilteredAttachmentStrategy
.prototype.getListenersByIDs = function(ids
) {
90 // |ids| is for filtered events only.
91 return this.event_
.listeners
;
94 var FilteredAttachmentStrategy = function(event
) {
96 this.listenerMap_
= {};
99 FilteredAttachmentStrategy
.idToEventMap
= {};
101 FilteredAttachmentStrategy
.prototype.onAddedListener = function(listener
) {
102 var id
= eventNatives
.AttachFilteredEvent(this.event_
.eventName
,
103 listener
.filters
|| {});
105 throw new Error("Can't add listener");
107 this.listenerMap_
[id
] = listener
;
108 FilteredAttachmentStrategy
.idToEventMap
[id
] = this.event_
;
111 FilteredAttachmentStrategy
.prototype.onRemovedListener = function(listener
) {
112 this.detachListener(listener
, true);
115 FilteredAttachmentStrategy
.prototype.detachListener
=
116 function(listener
, manual
) {
117 if (listener
.id
== undefined)
118 throw new Error("listener.id undefined - '" + listener
+ "'");
119 var id
= listener
.id
;
120 delete this.listenerMap_
[id
];
121 delete FilteredAttachmentStrategy
.idToEventMap
[id
];
122 eventNatives
.DetachFilteredEvent(id
, manual
);
125 FilteredAttachmentStrategy
.prototype.detach = function(manual
) {
126 for (var i
in this.listenerMap_
)
127 this.detachListener(this.listenerMap_
[i
], manual
);
130 FilteredAttachmentStrategy
.prototype.getListenersByIDs = function(ids
) {
132 for (var i
= 0; i
< ids
.length
; i
++)
133 $Array
.push(result
, this.listenerMap_
[ids
[i
]]);
137 function parseEventOptions(opt_eventOptions
) {
138 function merge(dest
, src
) {
140 if (!$Object
.hasOwnProperty(dest
, k
)) {
146 var options
= opt_eventOptions
|| {};
148 // Event supports adding listeners with filters ("filtered events"), for
149 // example as used in the webNavigation API.
151 // event.addListener(listener, [filter1, filter2]);
152 supportsFilters
: false,
154 // Events supports vanilla events. Most APIs use these.
156 // event.addListener(listener);
157 supportsListeners
: true,
159 // Event supports adding rules ("declarative events") rather than
160 // listeners, for example as used in the declarativeWebRequest API.
162 // event.addRules([rule1, rule2]);
163 supportsRules
: false,
165 // Event is unmanaged in that the browser has no knowledge of its
166 // existence; it's never invoked, doesn't keep the renderer alive, and
167 // the bindings system has no knowledge of it.
169 // Both events created by user code (new chrome.Event()) and messaging
170 // events are unmanaged, though in the latter case the browser *does*
171 // interact indirectly with them via IPCs written by hand.
177 // Event object. If opt_eventName is provided, this object represents
178 // the unique instance of that named event, and dispatching an event
179 // with that name will route through this object's listeners. Note that
180 // opt_eventName is required for events that support rules.
183 // var Event = require('event_bindings').Event;
184 // chrome.tabs.onChanged = new Event("tab-changed");
185 // chrome.tabs.onChanged.addListener(function(data) { alert(data); });
186 // Event.dispatch("tab-changed", "hi");
187 // will result in an alert dialog that says 'hi'.
189 // If opt_eventOptions exists, it is a dictionary that contains the boolean
190 // entries "supportsListeners" and "supportsRules".
191 // If opt_webViewInstanceId exists, it is an integer uniquely identifying a
192 // <webview> tag within the embedder. If it does not exist, then this is an
193 // extension event rather than a <webview> event.
194 var EventImpl = function(opt_eventName
, opt_argSchemas
, opt_eventOptions
,
195 opt_webViewInstanceId
) {
196 this.eventName
= opt_eventName
;
197 this.argSchemas
= opt_argSchemas
;
199 this.eventOptions
= parseEventOptions(opt_eventOptions
);
200 this.webViewInstanceId
= opt_webViewInstanceId
|| 0;
202 if (!this.eventName
) {
203 if (this.eventOptions
.supportsRules
)
204 throw new Error("Events that support rules require an event name.");
205 // Events without names cannot be managed by the browser by definition
206 // (the browser has no way of identifying them).
207 this.eventOptions
.unmanaged
= true;
210 // Track whether the event has been destroyed to help track down the cause
211 // of http://crbug.com/258526.
212 // This variable will eventually hold the stack trace of the destroy call.
213 // TODO(kalman): Delete this and replace with more sound logic that catches
214 // when events are used without being *attached*.
215 this.destroyed
= null;
217 if (this.eventOptions
.unmanaged
)
218 this.attachmentStrategy
= new NullAttachmentStrategy(this);
219 else if (this.eventOptions
.supportsFilters
)
220 this.attachmentStrategy
= new FilteredAttachmentStrategy(this);
222 this.attachmentStrategy
= new UnfilteredAttachmentStrategy(this);
225 // callback is a function(args, dispatch). args are the args we receive from
226 // dispatchEvent(), and dispatch is a function(args) that dispatches args to
228 function registerArgumentMassager(name
, callback
) {
229 if (eventArgumentMassagers
[name
])
230 throw new Error("Massager already registered for event: " + name
);
231 eventArgumentMassagers
[name
] = callback
;
234 // Dispatches a named event with the given argument array. The args array is
235 // the list of arguments that will be sent to the event callback.
236 function dispatchEvent(name
, args
, filteringInfo
) {
237 var listenerIDs
= [];
240 listenerIDs
= eventNatives
.MatchAgainstEventFilter(name
, filteringInfo
);
242 var event
= attachedNamedEvents
[name
];
246 var dispatchArgs = function(args
) {
247 var result
= event
.dispatch_(args
, listenerIDs
);
249 logging
.DCHECK(!result
.validationErrors
, result
.validationErrors
);
253 if (eventArgumentMassagers
[name
])
254 eventArgumentMassagers
[name
](args
, dispatchArgs
);
259 // Registers a callback to be called when this event is dispatched.
260 EventImpl
.prototype.addListener = function(cb
, filters
) {
261 if (!this.eventOptions
.supportsListeners
)
262 throw new Error("This event does not support listeners.");
263 if (this.eventOptions
.maxListeners
&&
264 this.getListenerCount_() >= this.eventOptions
.maxListeners
) {
265 throw new Error("Too many listeners for " + this.eventName
);
268 if (!this.eventOptions
.supportsFilters
)
269 throw new Error("This event does not support filters.");
270 if (filters
.url
&& !(filters
.url
instanceof Array
))
271 throw new Error("filters.url should be an array.");
272 if (filters
.serviceType
&&
273 !(typeof filters
.serviceType
=== 'string')) {
274 throw new Error("filters.serviceType should be a string.")
277 var listener
= {callback
: cb
, filters
: filters
};
278 this.attach_(listener
);
279 $Array
.push(this.listeners
, listener
);
282 EventImpl
.prototype.attach_ = function(listener
) {
283 this.attachmentStrategy
.onAddedListener(listener
);
285 if (this.listeners
.length
== 0) {
286 allAttachedEvents
[allAttachedEvents
.length
] = this;
287 if (this.eventName
) {
288 if (attachedNamedEvents
[this.eventName
]) {
289 throw new Error("Event '" + this.eventName
+
290 "' is already attached.");
292 attachedNamedEvents
[this.eventName
] = this;
297 // Unregisters a callback.
298 EventImpl
.prototype.removeListener = function(cb
) {
299 if (!this.eventOptions
.supportsListeners
)
300 throw new Error("This event does not support listeners.");
302 var idx
= this.findListener_(cb
);
306 var removedListener
= $Array
.splice(this.listeners
, idx
, 1)[0];
307 this.attachmentStrategy
.onRemovedListener(removedListener
);
309 if (this.listeners
.length
== 0) {
310 var i
= $Array
.indexOf(allAttachedEvents
, this);
312 delete allAttachedEvents
[i
];
313 if (this.eventName
) {
314 if (!attachedNamedEvents
[this.eventName
]) {
316 "Event '" + this.eventName
+ "' is not attached.");
318 delete attachedNamedEvents
[this.eventName
];
323 // Test if the given callback is registered for this event.
324 EventImpl
.prototype.hasListener = function(cb
) {
325 if (!this.eventOptions
.supportsListeners
)
326 throw new Error("This event does not support listeners.");
327 return this.findListener_(cb
) > -1;
330 // Test if any callbacks are registered for this event.
331 EventImpl
.prototype.hasListeners = function() {
332 return this.getListenerCount_() > 0;
335 // Returns the number of listeners on this event.
336 EventImpl
.prototype.getListenerCount_ = function() {
337 if (!this.eventOptions
.supportsListeners
)
338 throw new Error("This event does not support listeners.");
339 return this.listeners
.length
;
342 // Returns the index of the given callback if registered, or -1 if not
344 EventImpl
.prototype.findListener_ = function(cb
) {
345 for (var i
= 0; i
< this.listeners
.length
; i
++) {
346 if (this.listeners
[i
].callback
== cb
) {
354 EventImpl
.prototype.dispatch_ = function(args
, listenerIDs
) {
355 if (this.destroyed
) {
356 throw new Error(this.eventName
+ ' was already destroyed at: ' +
359 if (!this.eventOptions
.supportsListeners
)
360 throw new Error("This event does not support listeners.");
362 if (this.argSchemas
&& logging
.DCHECK_IS_ON()) {
364 validate(args
, this.argSchemas
);
366 e
.message
+= ' in ' + this.eventName
;
371 // Make a copy of the listeners in case the listener list is modified
372 // while dispatching the event.
373 var listeners
= $Array
.slice(
374 this.attachmentStrategy
.getListenersByIDs(listenerIDs
));
377 for (var i
= 0; i
< listeners
.length
; i
++) {
379 var result
= this.wrapper
.dispatchToListener(listeners
[i
].callback
,
381 if (result
!== undefined)
382 $Array
.push(results
, result
);
384 exceptionHandler
.handle('Error in event handler for ' +
385 (this.eventName
? this.eventName
: '(unknown)'),
390 return {results
: results
};
393 // Can be overridden to support custom dispatching.
394 EventImpl
.prototype.dispatchToListener = function(callback
, args
) {
395 return $Function
.apply(callback
, null, args
);
398 // Dispatches this event object to all listeners, passing all supplied
399 // arguments to this function each listener.
400 EventImpl
.prototype.dispatch = function(varargs
) {
401 return this.dispatch_($Array
.slice(arguments
), undefined);
404 // Detaches this event object from its name.
405 EventImpl
.prototype.detach_ = function() {
406 this.attachmentStrategy
.detach(false);
409 EventImpl
.prototype.destroy_ = function() {
410 this.listeners
.length
= 0;
412 this.destroyed
= exceptionHandler
.getStackTrace();
415 EventImpl
.prototype.addRules = function(rules
, opt_cb
) {
416 if (!this.eventOptions
.supportsRules
)
417 throw new Error("This event does not support rules.");
419 // Takes a list of JSON datatype identifiers and returns a schema fragment
420 // that verifies that a JSON object corresponds to an array of only these
422 function buildArrayOfChoicesSchema(typesList
) {
426 'choices': typesList
.map(function(el
) {return {'$ref': el
};})
431 // Validate conditions and actions against specific schemas of this
432 // event object type.
433 // |rules| is an array of JSON objects that follow the Rule type of the
434 // declarative extension APIs. |conditions| is an array of JSON type
435 // identifiers that are allowed to occur in the conditions attribute of each
436 // rule. Likewise, |actions| is an array of JSON type identifiers that are
437 // allowed to occur in the actions attribute of each rule.
438 function validateRules(rules
, conditions
, actions
) {
439 var conditionsSchema
= buildArrayOfChoicesSchema(conditions
);
440 var actionsSchema
= buildArrayOfChoicesSchema(actions
);
441 $Array
.forEach(rules
, function(rule
) {
442 validate([rule
.conditions
], [conditionsSchema
]);
443 validate([rule
.actions
], [actionsSchema
]);
447 if (!this.eventOptions
.conditions
|| !this.eventOptions
.actions
) {
448 throw new Error('Event ' + this.eventName
+ ' misses ' +
449 'conditions or actions in the API specification.');
453 this.eventOptions
.conditions
,
454 this.eventOptions
.actions
);
456 ensureRuleSchemasLoaded();
457 // We remove the first parameter from the validation to give the user more
458 // meaningful error messages.
459 validate([this.webViewInstanceId
, rules
, opt_cb
],
461 $Array
.slice(ruleFunctionSchemas
.addRules
.parameters
), 1));
464 [this.eventName
, this.webViewInstanceId
, rules
, opt_cb
],
465 ruleFunctionSchemas
.addRules
.parameters
);
468 EventImpl
.prototype.removeRules = function(ruleIdentifiers
, opt_cb
) {
469 if (!this.eventOptions
.supportsRules
)
470 throw new Error("This event does not support rules.");
471 ensureRuleSchemasLoaded();
472 // We remove the first parameter from the validation to give the user more
473 // meaningful error messages.
474 validate([this.webViewInstanceId
, ruleIdentifiers
, opt_cb
],
476 $Array
.slice(ruleFunctionSchemas
.removeRules
.parameters
), 1));
477 sendRequest("events.removeRules",
479 this.webViewInstanceId
,
482 ruleFunctionSchemas
.removeRules
.parameters
);
485 EventImpl
.prototype.getRules = function(ruleIdentifiers
, cb
) {
486 if (!this.eventOptions
.supportsRules
)
487 throw new Error("This event does not support rules.");
488 ensureRuleSchemasLoaded();
489 // We remove the first parameter from the validation to give the user more
490 // meaningful error messages.
491 validate([this.webViewInstanceId
, ruleIdentifiers
, cb
],
493 $Array
.slice(ruleFunctionSchemas
.getRules
.parameters
), 1));
497 [this.eventName
, this.webViewInstanceId
, ruleIdentifiers
, cb
],
498 ruleFunctionSchemas
.getRules
.parameters
);
501 unloadEvent
.addListener(function() {
502 for (var i
= 0; i
< allAttachedEvents
.length
; ++i
) {
503 var event
= allAttachedEvents
[i
];
509 var Event
= utils
.expose('Event', EventImpl
, { functions
: [
514 'dispatchToListener',
521 // NOTE: Event is (lazily) exposed as chrome.Event from dispatcher.cc.
522 exports
.Event
= Event
;
524 exports
.dispatchEvent
= dispatchEvent
;
525 exports
.parseEventOptions
= parseEventOptions
;
526 exports
.registerArgumentMassager
= registerArgumentMassager
;