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 // Event management for WebViewInternal. 6 7 var DeclarativeWebRequestSchema = 8 requireNative('schema_registry').GetSchema('declarativeWebRequest'); 9 var EventBindings = require('event_bindings'); 10 var IdGenerator = requireNative('id_generator'); 11 var MessagingNatives = requireNative('messaging_natives'); 12 var WebRequestEvent = require('webRequestInternal').WebRequestEvent; 13 var WebRequestSchema = 14 requireNative('schema_registry').GetSchema('webRequest'); 15 var WebView = require('webview').WebView; 16 17 var CreateEvent = function(name) { 18 var eventOpts = {supportsListeners: true, supportsFilters: true}; 19 return new EventBindings.Event(name, undefined, eventOpts); 20 }; 21 22 var FrameNameChangedEvent = CreateEvent('webview.onFrameNameChanged'); 23 var WebRequestMessageEvent = CreateEvent('webview.onMessage'); 24 25 // WEB_VIEW_EVENTS is a map of stable <webview> DOM event names to their 26 // associated extension event descriptor objects. 27 // An event listener will be attached to the extension event |evt| specified in 28 // the descriptor. 29 // |fields| specifies the public-facing fields in the DOM event that are 30 // accessible to <webview> developers. 31 // |customHandler| allows a handler function to be called each time an extension 32 // event is caught by its event listener. The DOM event should be dispatched 33 // within this handler function. With no handler function, the DOM event 34 // will be dispatched by default each time the extension event is caught. 35 // |cancelable| (default: false) specifies whether the event's default 36 // behavior can be canceled. If the default action associated with the event 37 // is prevented, then its dispatch function will return false in its event 38 // handler. The event must have a custom handler for this to be meaningful. 39 var WEB_VIEW_EVENTS = { 40 'close': { 41 evt: CreateEvent('webview.onClose'), 42 fields: [] 43 }, 44 'consolemessage': { 45 evt: CreateEvent('webview.onConsoleMessage'), 46 fields: ['level', 'message', 'line', 'sourceId'] 47 }, 48 'contentload': { 49 evt: CreateEvent('webview.onContentLoad'), 50 fields: [] 51 }, 52 'contextmenu': { 53 evt: CreateEvent('webview.contextmenu'), 54 cancelable: true, 55 customHandler: function(handler, event, webViewEvent) { 56 handler.handleContextMenu(event, webViewEvent); 57 }, 58 fields: ['items'] 59 }, 60 'dialog': { 61 cancelable: true, 62 customHandler: function(handler, event, webViewEvent) { 63 handler.handleDialogEvent(event, webViewEvent); 64 }, 65 evt: CreateEvent('webview.onDialog'), 66 fields: ['defaultPromptText', 'messageText', 'messageType', 'url'] 67 }, 68 'exit': { 69 evt: CreateEvent('webview.onExit'), 70 fields: ['processId', 'reason'] 71 }, 72 'loadabort': { 73 cancelable: true, 74 customHandler: function(handler, event, webViewEvent) { 75 handler.handleLoadAbortEvent(event, webViewEvent); 76 }, 77 evt: CreateEvent('webview.onLoadAbort'), 78 fields: ['url', 'isTopLevel', 'reason'] 79 }, 80 'loadcommit': { 81 customHandler: function(handler, event, webViewEvent) { 82 handler.handleLoadCommitEvent(event, webViewEvent); 83 }, 84 evt: CreateEvent('webview.onLoadCommit'), 85 fields: ['url', 'isTopLevel'] 86 }, 87 'loadprogress': { 88 evt: CreateEvent('webview.onLoadProgress'), 89 fields: ['url', 'progress'] 90 }, 91 'loadredirect': { 92 evt: CreateEvent('webview.onLoadRedirect'), 93 fields: ['isTopLevel', 'oldUrl', 'newUrl'] 94 }, 95 'loadstart': { 96 evt: CreateEvent('webview.onLoadStart'), 97 fields: ['url', 'isTopLevel'] 98 }, 99 'loadstop': { 100 evt: CreateEvent('webview.onLoadStop'), 101 fields: [] 102 }, 103 'newwindow': { 104 cancelable: true, 105 customHandler: function(handler, event, webViewEvent) { 106 handler.handleNewWindowEvent(event, webViewEvent); 107 }, 108 evt: CreateEvent('webview.onNewWindow'), 109 fields: [ 110 'initialHeight', 111 'initialWidth', 112 'targetUrl', 113 'windowOpenDisposition', 114 'name' 115 ] 116 }, 117 'permissionrequest': { 118 cancelable: true, 119 customHandler: function(handler, event, webViewEvent) { 120 handler.handlePermissionEvent(event, webViewEvent); 121 }, 122 evt: CreateEvent('webview.onPermissionRequest'), 123 fields: [ 124 'identifier', 125 'lastUnlockedBySelf', 126 'name', 127 'permission', 128 'requestMethod', 129 'url', 130 'userGesture' 131 ] 132 }, 133 'responsive': { 134 evt: CreateEvent('webview.onResponsive'), 135 fields: ['processId'] 136 }, 137 'sizechanged': { 138 evt: CreateEvent('webview.onSizeChanged'), 139 customHandler: function(handler, event, webViewEvent) { 140 handler.handleSizeChangedEvent(event, webViewEvent); 141 }, 142 fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth'] 143 }, 144 'unresponsive': { 145 evt: CreateEvent('webview.onUnresponsive'), 146 fields: ['processId'] 147 } 148 }; 149 150 function DeclarativeWebRequestEvent(opt_eventName, 151 opt_argSchemas, 152 opt_eventOptions, 153 opt_webViewInstanceId) { 154 var subEventName = opt_eventName + '/' + IdGenerator.GetNextId(); 155 EventBindings.Event.call(this, subEventName, opt_argSchemas, opt_eventOptions, 156 opt_webViewInstanceId); 157 158 var self = this; 159 // TODO(lazyboy): When do we dispose this listener? 160 WebRequestMessageEvent.addListener(function() { 161 // Re-dispatch to subEvent's listeners. 162 $Function.apply(self.dispatch, self, $Array.slice(arguments)); 163 }, {instanceId: opt_webViewInstanceId || 0}); 164 } 165 166 DeclarativeWebRequestEvent.prototype = { 167 __proto__: EventBindings.Event.prototype 168 }; 169 170 // Constructor. 171 function WebViewEvents(webViewInternal, viewInstanceId) { 172 this.webViewInternal = webViewInternal; 173 this.viewInstanceId = viewInstanceId; 174 this.setup(); 175 } 176 177 // Sets up events. 178 WebViewEvents.prototype.setup = function() { 179 this.setupFrameNameChangedEvent(); 180 this.setupWebRequestEvents(); 181 this.webViewInternal.setupExperimentalContextMenus(); 182 183 var events = this.getEvents(); 184 for (var eventName in events) { 185 this.setupEvent(eventName, events[eventName]); 186 } 187 }; 188 189 WebViewEvents.prototype.setupFrameNameChangedEvent = function() { 190 var self = this; 191 FrameNameChangedEvent.addListener(function(e) { 192 self.webViewInternal.onFrameNameChanged(e.name); 193 }, {instanceId: self.viewInstanceId}); 194 }; 195 196 WebViewEvents.prototype.setupWebRequestEvents = function() { 197 var self = this; 198 var request = {}; 199 var createWebRequestEvent = function(webRequestEvent) { 200 return function() { 201 if (!self[webRequestEvent.name]) { 202 self[webRequestEvent.name] = 203 new WebRequestEvent( 204 'webview.' + webRequestEvent.name, 205 webRequestEvent.parameters, 206 webRequestEvent.extraParameters, webRequestEvent.options, 207 self.viewInstanceId); 208 } 209 return self[webRequestEvent.name]; 210 }; 211 }; 212 213 var createDeclarativeWebRequestEvent = function(webRequestEvent) { 214 return function() { 215 if (!self[webRequestEvent.name]) { 216 // The onMessage event gets a special event type because we want 217 // the listener to fire only for messages targeted for this particular 218 // <webview>. 219 var EventClass = webRequestEvent.name === 'onMessage' ? 220 DeclarativeWebRequestEvent : EventBindings.Event; 221 self[webRequestEvent.name] = 222 new EventClass( 223 'webview.' + webRequestEvent.name, 224 webRequestEvent.parameters, 225 webRequestEvent.options, 226 self.viewInstanceId); 227 } 228 return self[webRequestEvent.name]; 229 }; 230 }; 231 232 for (var i = 0; i < DeclarativeWebRequestSchema.events.length; ++i) { 233 var eventSchema = DeclarativeWebRequestSchema.events[i]; 234 var webRequestEvent = createDeclarativeWebRequestEvent(eventSchema); 235 Object.defineProperty( 236 request, 237 eventSchema.name, 238 { 239 get: webRequestEvent, 240 enumerable: true 241 } 242 ); 243 } 244 245 // Populate the WebRequest events from the API definition. 246 for (var i = 0; i < WebRequestSchema.events.length; ++i) { 247 var webRequestEvent = createWebRequestEvent(WebRequestSchema.events[i]); 248 Object.defineProperty( 249 request, 250 WebRequestSchema.events[i].name, 251 { 252 get: webRequestEvent, 253 enumerable: true 254 } 255 ); 256 } 257 258 this.webViewInternal.setRequestPropertyOnWebViewNode(request); 259 }; 260 261 WebViewEvents.prototype.getEvents = function() { 262 var experimentalEvents = this.webViewInternal.maybeGetExperimentalEvents(); 263 for (var eventName in experimentalEvents) { 264 WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName]; 265 } 266 return WEB_VIEW_EVENTS; 267 }; 268 269 WebViewEvents.prototype.setupEvent = function(name, info) { 270 var self = this; 271 info.evt.addListener(function(e) { 272 var details = {bubbles:true}; 273 if (info.cancelable) 274 details.cancelable = true; 275 var webViewEvent = new Event(name, details); 276 $Array.forEach(info.fields, function(field) { 277 if (e[field] !== undefined) { 278 webViewEvent[field] = e[field]; 279 } 280 }); 281 if (info.customHandler) { 282 info.customHandler(self, e, webViewEvent); 283 return; 284 } 285 self.webViewInternal.dispatchEvent(webViewEvent); 286 }, {instanceId: self.viewInstanceId}); 287 288 this.webViewInternal.setupEventProperty(name); 289 }; 290 291 292 // Event handlers. 293 WebViewEvents.prototype.handleContextMenu = function(e, webViewEvent) { 294 this.webViewInternal.maybeHandleContextMenu(e, webViewEvent); 295 }; 296 297 WebViewEvents.prototype.handleDialogEvent = function(event, webViewEvent) { 298 var showWarningMessage = function(dialogType) { 299 var VOWELS = ['a', 'e', 'i', 'o', 'u']; 300 var WARNING_MSG_DIALOG_BLOCKED = '<webview>: %1 %2 dialog was blocked.'; 301 var article = (VOWELS.indexOf(dialogType.charAt(0)) >= 0) ? 'An' : 'A'; 302 var output = WARNING_MSG_DIALOG_BLOCKED.replace('%1', article); 303 output = output.replace('%2', dialogType); 304 window.console.warn(output); 305 }; 306 307 var self = this; 308 var requestId = event.requestId; 309 var actionTaken = false; 310 311 var validateCall = function() { 312 var ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN = '<webview>: ' + 313 'An action has already been taken for this "dialog" event.'; 314 315 if (actionTaken) { 316 throw new Error(ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN); 317 } 318 actionTaken = true; 319 }; 320 321 var getInstanceId = function() { 322 return self.webViewInternal.getInstanceId(); 323 }; 324 325 var dialog = { 326 ok: function(user_input) { 327 validateCall(); 328 user_input = user_input || ''; 329 WebView.setPermission(getInstanceId(), requestId, 'allow', user_input); 330 }, 331 cancel: function() { 332 validateCall(); 333 WebView.setPermission(getInstanceId(), requestId, 'deny'); 334 } 335 }; 336 webViewEvent.dialog = dialog; 337 338 var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent); 339 if (actionTaken) { 340 return; 341 } 342 343 if (defaultPrevented) { 344 // Tell the JavaScript garbage collector to track lifetime of |dialog| and 345 // call back when the dialog object has been collected. 346 MessagingNatives.BindToGC(dialog, function() { 347 // Avoid showing a warning message if the decision has already been made. 348 if (actionTaken) { 349 return; 350 } 351 WebView.setPermission( 352 getInstanceId(), requestId, 'default', '', function(allowed) { 353 if (allowed) { 354 return; 355 } 356 showWarningMessage(event.messageType); 357 }); 358 }); 359 } else { 360 actionTaken = true; 361 // The default action is equivalent to canceling the dialog. 362 WebView.setPermission( 363 getInstanceId(), requestId, 'default', '', function(allowed) { 364 if (allowed) { 365 return; 366 } 367 showWarningMessage(event.messageType); 368 }); 369 } 370 }; 371 372 WebViewEvents.prototype.handleLoadAbortEvent = function(event, webViewEvent) { 373 var showWarningMessage = function(reason) { 374 var WARNING_MSG_LOAD_ABORTED = '<webview>: ' + 375 'The load has aborted with reason "%1".'; 376 window.console.warn(WARNING_MSG_LOAD_ABORTED.replace('%1', reason)); 377 }; 378 if (this.webViewInternal.dispatchEvent(webViewEvent)) { 379 showWarningMessage(event.reason); 380 } 381 }; 382 383 WebViewEvents.prototype.handleLoadCommitEvent = function(event, webViewEvent) { 384 this.webViewInternal.onLoadCommit(event.currentEntryIndex, event.entryCount, 385 event.processId, event.url, 386 event.isTopLevel); 387 this.webViewInternal.dispatchEvent(webViewEvent); 388 }; 389 390 WebViewEvents.prototype.handleNewWindowEvent = function(event, webViewEvent) { 391 var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' + 392 'An action has already been taken for this "newwindow" event.'; 393 394 var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' + 395 'Unable to attach the new window to the provided webview.'; 396 397 var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.'; 398 399 var showWarningMessage = function() { 400 var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.'; 401 window.console.warn(WARNING_MSG_NEWWINDOW_BLOCKED); 402 }; 403 404 var requestId = event.requestId; 405 var actionTaken = false; 406 var self = this; 407 var getInstanceId = function() { 408 return self.webViewInternal.getInstanceId(); 409 }; 410 411 var validateCall = function () { 412 if (actionTaken) { 413 throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN); 414 } 415 actionTaken = true; 416 }; 417 418 var windowObj = { 419 attach: function(webview) { 420 validateCall(); 421 if (!webview || !webview.tagName || webview.tagName != 'WEBVIEW') 422 throw new Error(ERROR_MSG_WEBVIEW_EXPECTED); 423 // Attach happens asynchronously to give the tagWatcher an opportunity 424 // to pick up the new webview before attach operates on it, if it hasn't 425 // been attached to the DOM already. 426 // Note: Any subsequent errors cannot be exceptions because they happen 427 // asynchronously. 428 setTimeout(function() { 429 var webViewInternal = privates(webview).internal; 430 // Update the partition. 431 if (event.storagePartitionId) { 432 webViewInternal.onAttach(event.storagePartitionId); 433 } 434 435 var attached = webViewInternal.attachWindow(event.windowId, true); 436 437 if (!attached) { 438 window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH); 439 } 440 // If the object being passed into attach is not a valid <webview> 441 // then we will fail and it will be treated as if the new window 442 // was rejected. The permission API plumbing is used here to clean 443 // up the state created for the new window if attaching fails. 444 WebView.setPermission( 445 getInstanceId(), requestId, attached ? 'allow' : 'deny'); 446 }, 0); 447 }, 448 discard: function() { 449 validateCall(); 450 WebView.setPermission(getInstanceId(), requestId, 'deny'); 451 } 452 }; 453 webViewEvent.window = windowObj; 454 455 var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent); 456 if (actionTaken) { 457 return; 458 } 459 460 if (defaultPrevented) { 461 // Make browser plugin track lifetime of |windowObj|. 462 MessagingNatives.BindToGC(windowObj, function() { 463 // Avoid showing a warning message if the decision has already been made. 464 if (actionTaken) { 465 return; 466 } 467 WebView.setPermission( 468 getInstanceId(), requestId, 'default', '', function(allowed) { 469 if (allowed) { 470 return; 471 } 472 showWarningMessage(); 473 }); 474 }); 475 } else { 476 actionTaken = true; 477 // The default action is to discard the window. 478 WebView.setPermission( 479 getInstanceId(), requestId, 'default', '', function(allowed) { 480 if (allowed) { 481 return; 482 } 483 showWarningMessage(); 484 }); 485 } 486 }; 487 488 WebViewEvents.prototype.getPermissionTypes = function() { 489 var permissions = 490 ['media', 491 'geolocation', 492 'pointerLock', 493 'download', 494 'loadplugin', 495 'filesystem']; 496 return permissions.concat( 497 this.webViewInternal.maybeGetExperimentalPermissions()); 498 }; 499 500 WebViewEvents.prototype.handlePermissionEvent = 501 function(event, webViewEvent) { 502 var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' + 503 'Permission has already been decided for this "permissionrequest" event.'; 504 505 var showWarningMessage = function(permission) { 506 var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' + 507 'The permission request for "%1" has been denied.'; 508 window.console.warn( 509 WARNING_MSG_PERMISSION_DENIED.replace('%1', permission)); 510 }; 511 512 var requestId = event.requestId; 513 var self = this; 514 var getInstanceId = function() { 515 return self.webViewInternal.getInstanceId(); 516 }; 517 518 if (this.getPermissionTypes().indexOf(event.permission) < 0) { 519 // The permission type is not allowed. Trigger the default response. 520 WebView.setPermission( 521 getInstanceId(), requestId, 'default', '', function(allowed) { 522 if (allowed) { 523 return; 524 } 525 showWarningMessage(event.permission); 526 }); 527 return; 528 } 529 530 var decisionMade = false; 531 var validateCall = function() { 532 if (decisionMade) { 533 throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED); 534 } 535 decisionMade = true; 536 }; 537 538 // Construct the event.request object. 539 var request = { 540 allow: function() { 541 validateCall(); 542 WebView.setPermission(getInstanceId(), requestId, 'allow'); 543 }, 544 deny: function() { 545 validateCall(); 546 WebView.setPermission(getInstanceId(), requestId, 'deny'); 547 } 548 }; 549 webViewEvent.request = request; 550 551 var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent); 552 if (decisionMade) { 553 return; 554 } 555 556 if (defaultPrevented) { 557 // Make browser plugin track lifetime of |request|. 558 MessagingNatives.BindToGC(request, function() { 559 // Avoid showing a warning message if the decision has already been made. 560 if (decisionMade) { 561 return; 562 } 563 WebView.setPermission( 564 getInstanceId(), requestId, 'default', '', function(allowed) { 565 if (allowed) { 566 return; 567 } 568 showWarningMessage(event.permission); 569 }); 570 }); 571 } else { 572 decisionMade = true; 573 WebView.setPermission( 574 getInstanceId(), requestId, 'default', '', function(allowed) { 575 if (allowed) { 576 return; 577 } 578 showWarningMessage(event.permission); 579 }); 580 } 581 }; 582 583 WebViewEvents.prototype.handleSizeChangedEvent = function( 584 event, webViewEvent) { 585 this.webViewInternal.onSizeChanged(webViewEvent.newWidth, 586 webViewEvent.newHeight); 587 this.webViewInternal.dispatchEvent(webViewEvent); 588 }; 589 590 exports.WebViewEvents = WebViewEvents; 591 exports.CreateEvent = CreateEvent; 592