OLD | NEW |
| (Empty) |
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. | |
4 | |
5 var eventNatives = requireNative('event_natives'); | |
6 var handleUncaughtException = require('uncaught_exception_handler').handle; | |
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'); | |
13 | |
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: | |
18 // addRules: {}, | |
19 // getRules: {}, | |
20 // removeRules: {} | |
21 }; | |
22 | |
23 // This function ensures that |ruleFunctionSchemas| is populated. | |
24 function ensureRuleSchemasLoaded() { | |
25 if (ruleFunctionSchemas.addRules) | |
26 return; | |
27 var eventsSchema = schemaRegistry.GetSchema("events"); | |
28 var eventType = utils.lookup(eventsSchema.types, 'id', 'events.Event'); | |
29 | |
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'); | |
36 } | |
37 | |
38 // A map of event names to the event object that is registered to that name. | |
39 var attachedNamedEvents = {}; | |
40 | |
41 // An array of all attached event objects, used for detaching on unload. | |
42 var allAttachedEvents = []; | |
43 | |
44 // A map of functions that massage event arguments before they are dispatched. | |
45 // Key is event name, value is function. | |
46 var eventArgumentMassagers = {}; | |
47 | |
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 | |
50 // names. | |
51 var NullAttachmentStrategy = function(event) { | |
52 this.event_ = event; | |
53 }; | |
54 NullAttachmentStrategy.prototype.onAddedListener = | |
55 function(listener) { | |
56 }; | |
57 NullAttachmentStrategy.prototype.onRemovedListener = | |
58 function(listener) { | |
59 }; | |
60 NullAttachmentStrategy.prototype.detach = function(manual) { | |
61 }; | |
62 NullAttachmentStrategy.prototype.getListenersByIDs = function(ids) { | |
63 // |ids| is for filtered events only. | |
64 return this.event_.listeners; | |
65 }; | |
66 | |
67 // Handles adding/removing/dispatching listeners for unfiltered events. | |
68 var UnfilteredAttachmentStrategy = function(event) { | |
69 this.event_ = event; | |
70 }; | |
71 | |
72 UnfilteredAttachmentStrategy.prototype.onAddedListener = | |
73 function(listener) { | |
74 // Only attach / detach on the first / last listener removed. | |
75 if (this.event_.listeners.length == 0) | |
76 eventNatives.AttachEvent(this.event_.eventName); | |
77 }; | |
78 | |
79 UnfilteredAttachmentStrategy.prototype.onRemovedListener = | |
80 function(listener) { | |
81 if (this.event_.listeners.length == 0) | |
82 this.detach(true); | |
83 }; | |
84 | |
85 UnfilteredAttachmentStrategy.prototype.detach = function(manual) { | |
86 eventNatives.DetachEvent(this.event_.eventName, manual); | |
87 }; | |
88 | |
89 UnfilteredAttachmentStrategy.prototype.getListenersByIDs = function(ids) { | |
90 // |ids| is for filtered events only. | |
91 return this.event_.listeners; | |
92 }; | |
93 | |
94 var FilteredAttachmentStrategy = function(event) { | |
95 this.event_ = event; | |
96 this.listenerMap_ = {}; | |
97 }; | |
98 | |
99 FilteredAttachmentStrategy.idToEventMap = {}; | |
100 | |
101 FilteredAttachmentStrategy.prototype.onAddedListener = function(listener) { | |
102 var id = eventNatives.AttachFilteredEvent(this.event_.eventName, | |
103 listener.filters || {}); | |
104 if (id == -1) | |
105 throw new Error("Can't add listener"); | |
106 listener.id = id; | |
107 this.listenerMap_[id] = listener; | |
108 FilteredAttachmentStrategy.idToEventMap[id] = this.event_; | |
109 }; | |
110 | |
111 FilteredAttachmentStrategy.prototype.onRemovedListener = function(listener) { | |
112 this.detachListener(listener, true); | |
113 }; | |
114 | |
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); | |
123 }; | |
124 | |
125 FilteredAttachmentStrategy.prototype.detach = function(manual) { | |
126 for (var i in this.listenerMap_) | |
127 this.detachListener(this.listenerMap_[i], manual); | |
128 }; | |
129 | |
130 FilteredAttachmentStrategy.prototype.getListenersByIDs = function(ids) { | |
131 var result = []; | |
132 for (var i = 0; i < ids.length; i++) | |
133 $Array.push(result, this.listenerMap_[ids[i]]); | |
134 return result; | |
135 }; | |
136 | |
137 function parseEventOptions(opt_eventOptions) { | |
138 function merge(dest, src) { | |
139 for (var k in src) { | |
140 if (!$Object.hasOwnProperty(dest, k)) { | |
141 dest[k] = src[k]; | |
142 } | |
143 } | |
144 } | |
145 | |
146 var options = opt_eventOptions || {}; | |
147 merge(options, { | |
148 // Event supports adding listeners with filters ("filtered events"), for | |
149 // example as used in the webNavigation API. | |
150 // | |
151 // event.addListener(listener, [filter1, filter2]); | |
152 supportsFilters: false, | |
153 | |
154 // Events supports vanilla events. Most APIs use these. | |
155 // | |
156 // event.addListener(listener); | |
157 supportsListeners: true, | |
158 | |
159 // Event supports adding rules ("declarative events") rather than | |
160 // listeners, for example as used in the declarativeWebRequest API. | |
161 // | |
162 // event.addRules([rule1, rule2]); | |
163 supportsRules: false, | |
164 | |
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. | |
168 // | |
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. | |
172 unmanaged: false, | |
173 }); | |
174 return options; | |
175 }; | |
176 | |
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. | |
181 // | |
182 // Example: | |
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'. | |
188 // | |
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; | |
198 this.listeners = []; | |
199 this.eventOptions = parseEventOptions(opt_eventOptions); | |
200 this.webViewInstanceId = opt_webViewInstanceId || 0; | |
201 | |
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; | |
208 } | |
209 | |
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; | |
216 | |
217 if (this.eventOptions.unmanaged) | |
218 this.attachmentStrategy = new NullAttachmentStrategy(this); | |
219 else if (this.eventOptions.supportsFilters) | |
220 this.attachmentStrategy = new FilteredAttachmentStrategy(this); | |
221 else | |
222 this.attachmentStrategy = new UnfilteredAttachmentStrategy(this); | |
223 }; | |
224 | |
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 | |
227 // its listeners. | |
228 function registerArgumentMassager(name, callback) { | |
229 if (eventArgumentMassagers[name]) | |
230 throw new Error("Massager already registered for event: " + name); | |
231 eventArgumentMassagers[name] = callback; | |
232 } | |
233 | |
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 = []; | |
238 | |
239 if (filteringInfo) | |
240 listenerIDs = eventNatives.MatchAgainstEventFilter(name, filteringInfo); | |
241 | |
242 var event = attachedNamedEvents[name]; | |
243 if (!event) | |
244 return; | |
245 | |
246 var dispatchArgs = function(args) { | |
247 var result = event.dispatch_(args, listenerIDs); | |
248 if (result) | |
249 logging.DCHECK(!result.validationErrors, result.validationErrors); | |
250 return result; | |
251 }; | |
252 | |
253 if (eventArgumentMassagers[name]) | |
254 eventArgumentMassagers[name](args, dispatchArgs); | |
255 else | |
256 dispatchArgs(args); | |
257 } | |
258 | |
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); | |
266 } | |
267 if (filters) { | |
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.") | |
275 } | |
276 } | |
277 var listener = {callback: cb, filters: filters}; | |
278 this.attach_(listener); | |
279 $Array.push(this.listeners, listener); | |
280 }; | |
281 | |
282 EventImpl.prototype.attach_ = function(listener) { | |
283 this.attachmentStrategy.onAddedListener(listener); | |
284 | |
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."); | |
291 } | |
292 attachedNamedEvents[this.eventName] = this; | |
293 } | |
294 } | |
295 }; | |
296 | |
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."); | |
301 | |
302 var idx = this.findListener_(cb); | |
303 if (idx == -1) | |
304 return; | |
305 | |
306 var removedListener = $Array.splice(this.listeners, idx, 1)[0]; | |
307 this.attachmentStrategy.onRemovedListener(removedListener); | |
308 | |
309 if (this.listeners.length == 0) { | |
310 var i = $Array.indexOf(allAttachedEvents, this); | |
311 if (i >= 0) | |
312 delete allAttachedEvents[i]; | |
313 if (this.eventName) { | |
314 if (!attachedNamedEvents[this.eventName]) { | |
315 throw new Error( | |
316 "Event '" + this.eventName + "' is not attached."); | |
317 } | |
318 delete attachedNamedEvents[this.eventName]; | |
319 } | |
320 } | |
321 }; | |
322 | |
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; | |
328 }; | |
329 | |
330 // Test if any callbacks are registered for this event. | |
331 EventImpl.prototype.hasListeners = function() { | |
332 return this.getListenerCount_() > 0; | |
333 }; | |
334 | |
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; | |
340 }; | |
341 | |
342 // Returns the index of the given callback if registered, or -1 if not | |
343 // found. | |
344 EventImpl.prototype.findListener_ = function(cb) { | |
345 for (var i = 0; i < this.listeners.length; i++) { | |
346 if (this.listeners[i].callback == cb) { | |
347 return i; | |
348 } | |
349 } | |
350 | |
351 return -1; | |
352 }; | |
353 | |
354 EventImpl.prototype.dispatch_ = function(args, listenerIDs) { | |
355 if (this.destroyed) { | |
356 throw new Error(this.eventName + ' was already destroyed at: ' + | |
357 this.destroyed); | |
358 } | |
359 if (!this.eventOptions.supportsListeners) | |
360 throw new Error("This event does not support listeners."); | |
361 | |
362 if (this.argSchemas && logging.DCHECK_IS_ON()) { | |
363 try { | |
364 validate(args, this.argSchemas); | |
365 } catch (e) { | |
366 e.message += ' in ' + this.eventName; | |
367 throw e; | |
368 } | |
369 } | |
370 | |
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)); | |
375 | |
376 var results = []; | |
377 for (var i = 0; i < listeners.length; i++) { | |
378 try { | |
379 var result = this.wrapper.dispatchToListener(listeners[i].callback, | |
380 args); | |
381 if (result !== undefined) | |
382 $Array.push(results, result); | |
383 } catch (e) { | |
384 handleUncaughtException( | |
385 'Error in event handler for ' + | |
386 (this.eventName ? this.eventName : '(unknown)') + | |
387 ': ' + e.message + '\nStack trace: ' + e.stack, | |
388 e); | |
389 } | |
390 } | |
391 if (results.length) | |
392 return {results: results}; | |
393 } | |
394 | |
395 // Can be overridden to support custom dispatching. | |
396 EventImpl.prototype.dispatchToListener = function(callback, args) { | |
397 return $Function.apply(callback, null, args); | |
398 } | |
399 | |
400 // Dispatches this event object to all listeners, passing all supplied | |
401 // arguments to this function each listener. | |
402 EventImpl.prototype.dispatch = function(varargs) { | |
403 return this.dispatch_($Array.slice(arguments), undefined); | |
404 }; | |
405 | |
406 // Detaches this event object from its name. | |
407 EventImpl.prototype.detach_ = function() { | |
408 this.attachmentStrategy.detach(false); | |
409 }; | |
410 | |
411 EventImpl.prototype.destroy_ = function() { | |
412 this.listeners.length = 0; | |
413 this.detach_(); | |
414 this.destroyed = new Error().stack; | |
415 }; | |
416 | |
417 EventImpl.prototype.addRules = function(rules, opt_cb) { | |
418 if (!this.eventOptions.supportsRules) | |
419 throw new Error("This event does not support rules."); | |
420 | |
421 // Takes a list of JSON datatype identifiers and returns a schema fragment | |
422 // that verifies that a JSON object corresponds to an array of only these | |
423 // data types. | |
424 function buildArrayOfChoicesSchema(typesList) { | |
425 return { | |
426 'type': 'array', | |
427 'items': { | |
428 'choices': typesList.map(function(el) {return {'$ref': el};}) | |
429 } | |
430 }; | |
431 }; | |
432 | |
433 // Validate conditions and actions against specific schemas of this | |
434 // event object type. | |
435 // |rules| is an array of JSON objects that follow the Rule type of the | |
436 // declarative extension APIs. |conditions| is an array of JSON type | |
437 // identifiers that are allowed to occur in the conditions attribute of each | |
438 // rule. Likewise, |actions| is an array of JSON type identifiers that are | |
439 // allowed to occur in the actions attribute of each rule. | |
440 function validateRules(rules, conditions, actions) { | |
441 var conditionsSchema = buildArrayOfChoicesSchema(conditions); | |
442 var actionsSchema = buildArrayOfChoicesSchema(actions); | |
443 $Array.forEach(rules, function(rule) { | |
444 validate([rule.conditions], [conditionsSchema]); | |
445 validate([rule.actions], [actionsSchema]); | |
446 }); | |
447 }; | |
448 | |
449 if (!this.eventOptions.conditions || !this.eventOptions.actions) { | |
450 throw new Error('Event ' + this.eventName + ' misses ' + | |
451 'conditions or actions in the API specification.'); | |
452 } | |
453 | |
454 validateRules(rules, | |
455 this.eventOptions.conditions, | |
456 this.eventOptions.actions); | |
457 | |
458 ensureRuleSchemasLoaded(); | |
459 // We remove the first parameter from the validation to give the user more | |
460 // meaningful error messages. | |
461 validate([this.webViewInstanceId, rules, opt_cb], | |
462 $Array.splice( | |
463 $Array.slice(ruleFunctionSchemas.addRules.parameters), 1)); | |
464 sendRequest( | |
465 "events.addRules", | |
466 [this.eventName, this.webViewInstanceId, rules, opt_cb], | |
467 ruleFunctionSchemas.addRules.parameters); | |
468 } | |
469 | |
470 EventImpl.prototype.removeRules = function(ruleIdentifiers, opt_cb) { | |
471 if (!this.eventOptions.supportsRules) | |
472 throw new Error("This event does not support rules."); | |
473 ensureRuleSchemasLoaded(); | |
474 // We remove the first parameter from the validation to give the user more | |
475 // meaningful error messages. | |
476 validate([this.webViewInstanceId, ruleIdentifiers, opt_cb], | |
477 $Array.splice( | |
478 $Array.slice(ruleFunctionSchemas.removeRules.parameters), 1)); | |
479 sendRequest("events.removeRules", | |
480 [this.eventName, | |
481 this.webViewInstanceId, | |
482 ruleIdentifiers, | |
483 opt_cb], | |
484 ruleFunctionSchemas.removeRules.parameters); | |
485 } | |
486 | |
487 EventImpl.prototype.getRules = function(ruleIdentifiers, cb) { | |
488 if (!this.eventOptions.supportsRules) | |
489 throw new Error("This event does not support rules."); | |
490 ensureRuleSchemasLoaded(); | |
491 // We remove the first parameter from the validation to give the user more | |
492 // meaningful error messages. | |
493 validate([this.webViewInstanceId, ruleIdentifiers, cb], | |
494 $Array.splice( | |
495 $Array.slice(ruleFunctionSchemas.getRules.parameters), 1)); | |
496 | |
497 sendRequest( | |
498 "events.getRules", | |
499 [this.eventName, this.webViewInstanceId, ruleIdentifiers, cb], | |
500 ruleFunctionSchemas.getRules.parameters); | |
501 } | |
502 | |
503 unloadEvent.addListener(function() { | |
504 for (var i = 0; i < allAttachedEvents.length; ++i) { | |
505 var event = allAttachedEvents[i]; | |
506 if (event) | |
507 event.detach_(); | |
508 } | |
509 }); | |
510 | |
511 var Event = utils.expose('Event', EventImpl, { functions: [ | |
512 'addListener', | |
513 'removeListener', | |
514 'hasListener', | |
515 'hasListeners', | |
516 'dispatchToListener', | |
517 'dispatch', | |
518 'addRules', | |
519 'removeRules', | |
520 'getRules' | |
521 ] }); | |
522 | |
523 // NOTE: Event is (lazily) exposed as chrome.Event from dispatcher.cc. | |
524 exports.Event = Event; | |
525 | |
526 exports.dispatchEvent = dispatchEvent; | |
527 exports.parseEventOptions = parseEventOptions; | |
528 exports.registerArgumentMassager = registerArgumentMassager; | |
OLD | NEW |