| 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 |