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