web_view.js revision d0247b1b59f9c528cb6df88b4f2b9afaf80d181e
1// Copyright (c) 2012 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// Shim that simulates a <webview> tag via Mutation Observers. 6// 7// The actual tag is implemented via the browser plugin. The internals of this 8// are hidden via Shadow DOM. 9 10'use strict'; 11 12var DocumentNatives = requireNative('document_natives'); 13var EventBindings = require('event_bindings'); 14var MessagingNatives = requireNative('messaging_natives'); 15var WebRequestEvent = require('webRequestInternal').WebRequestEvent; 16var WebRequestSchema = 17 requireNative('schema_registry').GetSchema('webRequest'); 18var WebView = require('binding').Binding.create('webview').generate(); 19var WebViewNatives = requireNative('webview_natives'); 20 21// This secret enables hiding <webview> private members from the outside scope. 22// Outside of this file, |secret| is inaccessible. The only way to access the 23// <webview> element's internal members is via the |secret|. Since it's only 24// accessible by code here (and in web_view_experimental), only <webview>'s 25// API can access it and not external developers. 26var secret = {}; 27 28var WEB_VIEW_ATTRIBUTE_MAXHEIGHT = 'maxheight'; 29var WEB_VIEW_ATTRIBUTE_MAXWIDTH = 'maxwidth'; 30var WEB_VIEW_ATTRIBUTE_MINHEIGHT = 'minheight'; 31var WEB_VIEW_ATTRIBUTE_MINWIDTH = 'minwidth'; 32 33/** @type {Array.<string>} */ 34var WEB_VIEW_ATTRIBUTES = [ 35 'name', 36 'partition', 37 'autosize', 38 WEB_VIEW_ATTRIBUTE_MINHEIGHT, 39 WEB_VIEW_ATTRIBUTE_MINWIDTH, 40 WEB_VIEW_ATTRIBUTE_MAXHEIGHT, 41 WEB_VIEW_ATTRIBUTE_MAXWIDTH 42]; 43 44var CreateEvent = function(name) { 45 var eventOpts = {supportsListeners: true, supportsFilters: true}; 46 return new EventBindings.Event(name, undefined, eventOpts); 47}; 48 49var WEB_VIEW_EVENTS = { 50 'close': { 51 evt: CreateEvent('webview.onClose'), 52 fields: [] 53 }, 54 'consolemessage': { 55 evt: CreateEvent('webview.onConsoleMessage'), 56 fields: ['level', 'message', 'line', 'sourceId'] 57 }, 58 'contentload': { 59 evt: CreateEvent('webview.onContentLoad'), 60 fields: [] 61 }, 62 'exit': { 63 evt: CreateEvent('webview.onExit'), 64 fields: ['processId', 'reason'] 65 }, 66 'loadabort': { 67 evt: CreateEvent('webview.onLoadAbort'), 68 fields: ['url', 'isTopLevel', 'reason'] 69 }, 70 'loadcommit': { 71 customHandler: function(webViewInternal, event, webViewEvent) { 72 webViewInternal.handleLoadCommitEvent_(event, webViewEvent); 73 }, 74 evt: CreateEvent('webview.onLoadCommit'), 75 fields: ['url', 'isTopLevel'] 76 }, 77 'loadprogress': { 78 evt: CreateEvent('webview.onLoadProgress'), 79 fields: ['url', 'progress'] 80 }, 81 'loadredirect': { 82 evt: CreateEvent('webview.onLoadRedirect'), 83 fields: ['isTopLevel', 'oldUrl', 'newUrl'] 84 }, 85 'loadstart': { 86 evt: CreateEvent('webview.onLoadStart'), 87 fields: ['url', 'isTopLevel'] 88 }, 89 'loadstop': { 90 evt: CreateEvent('webview.onLoadStop'), 91 fields: [] 92 }, 93 'newwindow': { 94 cancelable: true, 95 customHandler: function(webViewInternal, event, webViewEvent) { 96 webViewInternal.handleNewWindowEvent_(event, webViewEvent); 97 }, 98 evt: CreateEvent('webview.onNewWindow'), 99 fields: [ 100 'initialHeight', 101 'initialWidth', 102 'targetUrl', 103 'windowOpenDisposition', 104 'name' 105 ] 106 }, 107 'permissionrequest': { 108 cancelable: true, 109 customHandler: function(webViewInternal, event, webViewEvent) { 110 webViewInternal.handlePermissionEvent_(event, webViewEvent); 111 }, 112 evt: CreateEvent('webview.onPermissionRequest'), 113 fields: [ 114 'lastUnlockedBySelf', 115 'permission', 116 'requestMethod', 117 'url', 118 'userGesture' 119 ] 120 }, 121 'responsive': { 122 evt: CreateEvent('webview.onResponsive'), 123 fields: ['processId'] 124 }, 125 'sizechanged': { 126 evt: CreateEvent('webview.onSizeChanged'), 127 customHandler: function(webViewInternal, event, webViewEvent) { 128 webViewInternal.handleSizeChangedEvent_(event, webViewEvent); 129 }, 130 fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth'] 131 }, 132 'unresponsive': { 133 evt: CreateEvent('webview.onUnresponsive'), 134 fields: ['processId'] 135 } 136}; 137 138// Implemented when the experimental API is available. 139WebViewInternal.maybeRegisterExperimentalAPIs = function(proto) {} 140 141/** 142 * @constructor 143 */ 144function WebViewInternal(webviewNode) { 145 this.webviewNode_ = webviewNode; 146 this.browserPluginNode_ = this.createBrowserPluginNode_(); 147 var shadowRoot = this.webviewNode_.webkitCreateShadowRoot(); 148 shadowRoot.appendChild(this.browserPluginNode_); 149 150 this.setupWebviewNodeAttributes_(); 151 this.setupFocusPropagation_(); 152 this.setupWebviewNodeProperties_(); 153 this.setupWebviewNodeEvents_(); 154} 155 156/** 157 * @private 158 */ 159WebViewInternal.prototype.createBrowserPluginNode_ = function() { 160 // We create BrowserPlugin as a custom element in order to observe changes 161 // to attributes synchronously. 162 var browserPluginNode = new WebViewInternal.BrowserPlugin(); 163 Object.defineProperty(browserPluginNode, 'internal_', { 164 enumerable: false, 165 writable: false, 166 value: function(key) { 167 if (key !== secret) { 168 return null; 169 } 170 return this; 171 }.bind(this) 172 }); 173 174 var ALL_ATTRIBUTES = WEB_VIEW_ATTRIBUTES.concat(['src']); 175 $Array.forEach(ALL_ATTRIBUTES, function(attributeName) { 176 // Only copy attributes that have been assigned values, rather than copying 177 // a series of undefined attributes to BrowserPlugin. 178 if (this.webviewNode_.hasAttribute(attributeName)) { 179 browserPluginNode.setAttribute( 180 attributeName, this.webviewNode_.getAttribute(attributeName)); 181 } else if (this.webviewNode_[attributeName]){ 182 // Reading property using has/getAttribute does not work on 183 // document.DOMContentLoaded event (but works on 184 // window.DOMContentLoaded event). 185 // So copy from property if copying from attribute fails. 186 browserPluginNode.setAttribute( 187 attributeName, this.webviewNode_[attributeName]); 188 } 189 }, this); 190 191 return browserPluginNode; 192}; 193 194/** 195 * @private 196 */ 197WebViewInternal.prototype.setupFocusPropagation_ = function() { 198 if (!this.webviewNode_.hasAttribute('tabIndex')) { 199 // <webview> needs a tabIndex in order to respond to keyboard focus. 200 // TODO(fsamuel): This introduces unexpected tab ordering. We need to find 201 // a way to take keyboard focus without messing with tab ordering. 202 // See http://crbug.com/231664. 203 this.webviewNode_.setAttribute('tabIndex', 0); 204 } 205 var self = this; 206 this.webviewNode_.addEventListener('focus', function(e) { 207 // Focus the BrowserPlugin when the <webview> takes focus. 208 self.browserPluginNode_.focus(); 209 }); 210 this.webviewNode_.addEventListener('blur', function(e) { 211 // Blur the BrowserPlugin when the <webview> loses focus. 212 self.browserPluginNode_.blur(); 213 }); 214}; 215 216/** 217 * @private 218 */ 219WebViewInternal.prototype.canGoBack_ = function() { 220 return this.entryCount_ > 1 && this.currentEntryIndex_ > 0; 221}; 222 223/** 224 * @private 225 */ 226WebViewInternal.prototype.canGoForward_ = function() { 227 return this.currentEntryIndex_ >= 0 && 228 this.currentEntryIndex_ < (this.entryCount_ - 1); 229}; 230 231/** 232 * @private 233 */ 234WebViewInternal.prototype.getProcessId_ = function() { 235 return this.processId_; 236}; 237 238/** 239 * @private 240 */ 241WebViewInternal.prototype.go_ = function(relativeIndex) { 242 if (!this.instanceId_) { 243 return; 244 } 245 WebView.go(this.instanceId_, relativeIndex); 246}; 247 248/** 249 * @private 250 */ 251WebViewInternal.prototype.reload_ = function() { 252 if (!this.instanceId_) { 253 return; 254 } 255 WebView.reload(this.instanceId_); 256}; 257 258/** 259 * @private 260 */ 261WebViewInternal.prototype.stop_ = function() { 262 if (!this.instanceId_) { 263 return; 264 } 265 WebView.stop(this.instanceId_); 266}; 267 268/** 269 * @private 270 */ 271WebViewInternal.prototype.terminate_ = function() { 272 if (!this.instanceId_) { 273 return; 274 } 275 WebView.terminate(this.instanceId_); 276}; 277 278/** 279 * @private 280 */ 281WebViewInternal.prototype.validateExecuteCodeCall_ = function() { 282 var ERROR_MSG_CANNOT_INJECT_SCRIPT = '<webview>: ' + 283 'Script cannot be injected into content until the page has loaded.'; 284 if (!this.instanceId_) { 285 throw new Error(ERROR_MSG_CANNOT_INJECT_SCRIPT); 286 } 287}; 288 289/** 290 * @private 291 */ 292WebViewInternal.prototype.executeScript_ = function(var_args) { 293 this.validateExecuteCodeCall_(); 294 var args = $Array.concat([this.instanceId_], $Array.slice(arguments)); 295 $Function.apply(WebView.executeScript, null, args); 296}; 297 298/** 299 * @private 300 */ 301WebViewInternal.prototype.insertCSS_ = function(var_args) { 302 this.validateExecuteCodeCall_(); 303 var args = $Array.concat([this.instanceId_], $Array.slice(arguments)); 304 $Function.apply(WebView.insertCSS, null, args); 305}; 306 307/** 308 * @private 309 */ 310WebViewInternal.prototype.setupWebviewNodeProperties_ = function() { 311 var ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE = '<webview>: ' + 312 'contentWindow is not available at this time. It will become available ' + 313 'when the page has finished loading.'; 314 315 var self = this; 316 var browserPluginNode = this.browserPluginNode_; 317 // Expose getters and setters for the attributes. 318 $Array.forEach(WEB_VIEW_ATTRIBUTES, function(attributeName) { 319 Object.defineProperty(this.webviewNode_, attributeName, { 320 get: function() { 321 if (browserPluginNode.hasOwnProperty(attributeName)) { 322 return browserPluginNode[attributeName]; 323 } else { 324 return browserPluginNode.getAttribute(attributeName); 325 } 326 }, 327 set: function(value) { 328 if (browserPluginNode.hasOwnProperty(attributeName)) { 329 // Give the BrowserPlugin first stab at the attribute so that it can 330 // throw an exception if there is a problem. This attribute will then 331 // be propagated back to the <webview>. 332 browserPluginNode[attributeName] = value; 333 } else { 334 browserPluginNode.setAttribute(attributeName, value); 335 } 336 }, 337 enumerable: true 338 }); 339 }, this); 340 341 // <webview> src does not quite behave the same as BrowserPlugin src, and so 342 // we don't simply keep the two in sync. 343 this.src_ = this.webviewNode_.getAttribute('src'); 344 Object.defineProperty(this.webviewNode_, 'src', { 345 get: function() { 346 return self.src_; 347 }, 348 set: function(value) { 349 self.webviewNode_.setAttribute('src', value); 350 }, 351 // No setter. 352 enumerable: true 353 }); 354 355 // We cannot use {writable: true} property descriptor because we want a 356 // dynamic getter value. 357 Object.defineProperty(this.webviewNode_, 'contentWindow', { 358 get: function() { 359 if (browserPluginNode.contentWindow) 360 return browserPluginNode.contentWindow; 361 console.error(ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE); 362 }, 363 // No setter. 364 enumerable: true 365 }); 366}; 367 368/** 369 * @private 370 */ 371WebViewInternal.prototype.setupWebviewNodeAttributes_ = function() { 372 Object.defineProperty(this.webviewNode_, 'internal_', { 373 enumerable: false, 374 writable: false, 375 value: function(key) { 376 if (key !== secret) { 377 return null; 378 } 379 return this; 380 }.bind(this) 381 }); 382 this.setupWebViewSrcAttributeMutationObserver_(); 383}; 384 385/** 386 * @private 387 */ 388WebViewInternal.prototype.setupWebViewSrcAttributeMutationObserver_ = 389 function() { 390 // The purpose of this mutation observer is to catch assignment to the src 391 // attribute without any changes to its value. This is useful in the case 392 // where the webview guest has crashed and navigating to the same address 393 // spawns off a new process. 394 var self = this; 395 this.srcObserver_ = new MutationObserver(function(mutations) { 396 $Array.forEach(mutations, function(mutation) { 397 var oldValue = mutation.oldValue; 398 var newValue = self.webviewNode_.getAttribute(mutation.attributeName); 399 if (oldValue != newValue) { 400 return; 401 } 402 self.handleWebviewAttributeMutation_( 403 mutation.attributeName, oldValue, newValue); 404 }); 405 }); 406 var params = { 407 attributes: true, 408 attributeOldValue: true, 409 attributeFilter: ['src'] 410 }; 411 this.srcObserver_.observe(this.webviewNode_, params); 412}; 413 414/** 415 * @private 416 */ 417WebViewInternal.prototype.handleWebviewAttributeMutation_ = 418 function(name, oldValue, newValue) { 419 // This observer monitors mutations to attributes of the <webview> and 420 // updates the BrowserPlugin properties accordingly. In turn, updating 421 // a BrowserPlugin property will update the corresponding BrowserPlugin 422 // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more 423 // details. 424 if (name == 'src') { 425 // We treat null attribute (attribute removed) and the empty string as 426 // one case. 427 oldValue = oldValue || ''; 428 newValue = newValue || ''; 429 // Once we have navigated, we don't allow clearing the src attribute. 430 // Once <webview> enters a navigated state, it cannot be return back to a 431 // placeholder state. 432 if (newValue == '' && oldValue != '') { 433 // src attribute changes normally initiate a navigation. We suppress 434 // the next src attribute handler call to avoid reloading the page 435 // on every guest-initiated navigation. 436 this.ignoreNextSrcAttributeChange_ = true; 437 this.webviewNode_.setAttribute('src', oldValue); 438 return; 439 } 440 this.src_ = newValue; 441 if (this.ignoreNextSrcAttributeChange_) { 442 // Don't allow the src mutation observer to see this change. 443 this.srcObserver_.takeRecords(); 444 this.ignoreNextSrcAttributeChange_ = false; 445 return; 446 } 447 } 448 if (this.browserPluginNode_.hasOwnProperty(name)) { 449 this.browserPluginNode_[name] = newValue; 450 } else { 451 this.browserPluginNode_.setAttribute(name, newValue); 452 } 453}; 454 455/** 456 * @private 457 */ 458WebViewInternal.prototype.handleBrowserPluginAttributeMutation_ = 459 function(name, newValue) { 460 // This observer monitors mutations to attributes of the BrowserPlugin and 461 // updates the <webview> attributes accordingly. 462 // |newValue| is null if the attribute |name| has been removed. 463 if (newValue != null) { 464 // Update the <webview> attribute to match the BrowserPlugin attribute. 465 // Note: Calling setAttribute on <webview> will trigger its mutation 466 // observer which will then propagate that attribute to BrowserPlugin. In 467 // cases where we permit assigning a BrowserPlugin attribute the same value 468 // again (such as navigation when crashed), this could end up in an infinite 469 // loop. Thus, we avoid this loop by only updating the <webview> attribute 470 // if the BrowserPlugin attributes differs from it. 471 if (newValue != this.webviewNode_.getAttribute(name)) { 472 this.webviewNode_.setAttribute(name, newValue); 473 } 474 } else { 475 // If an attribute is removed from the BrowserPlugin, then remove it 476 // from the <webview> as well. 477 this.webviewNode_.removeAttribute(name); 478 } 479}; 480 481/** 482 * @private 483 */ 484WebViewInternal.prototype.getEvents_ = function() { 485 var experimentalEvents = this.maybeGetExperimentalEvents_(); 486 for (var eventName in experimentalEvents) { 487 WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName]; 488 } 489 return WEB_VIEW_EVENTS; 490}; 491 492WebViewInternal.prototype.handleSizeChangedEvent_ = 493 function(event, webViewEvent) { 494 var node = this.webviewNode_; 495 496 var width = node.offsetWidth; 497 var height = node.offsetHeight; 498 499 // Check the current bounds to make sure we do not resize <webview> 500 // outside of current constraints. 501 var maxWidth; 502 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXWIDTH) && 503 node[WEB_VIEW_ATTRIBUTE_MAXWIDTH]) { 504 maxWidth = node[WEB_VIEW_ATTRIBUTE_MAXWIDTH]; 505 } else { 506 maxWidth = width; 507 } 508 509 var minWidth; 510 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINWIDTH) && 511 node[WEB_VIEW_ATTRIBUTE_MINWIDTH]) { 512 minWidth = node[WEB_VIEW_ATTRIBUTE_MINWIDTH]; 513 } else { 514 minWidth = width; 515 } 516 if (minWidth > maxWidth) { 517 minWidth = maxWidth; 518 } 519 520 var maxHeight; 521 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXHEIGHT) && 522 node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT]) { 523 maxHeight = node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT]; 524 } else { 525 maxHeight = height; 526 } 527 var minHeight; 528 if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINHEIGHT) && 529 node[WEB_VIEW_ATTRIBUTE_MINHEIGHT]) { 530 minHeight = node[WEB_VIEW_ATTRIBUTE_MINHEIGHT]; 531 } else { 532 minHeight = height; 533 } 534 if (minHeight > maxHeight) { 535 minHeight = maxHeight; 536 } 537 538 if (webViewEvent.newWidth >= minWidth && 539 webViewEvent.newWidth <= maxWidth && 540 webViewEvent.newHeight >= minHeight && 541 webViewEvent.newHeight <= maxHeight) { 542 node.style.width = webViewEvent.newWidth + 'px'; 543 node.style.height = webViewEvent.newHeight + 'px'; 544 } 545 node.dispatchEvent(webViewEvent); 546}; 547 548/** 549 * @private 550 */ 551WebViewInternal.prototype.setupWebviewNodeEvents_ = function() { 552 var self = this; 553 this.viewInstanceId_ = WebViewNatives.GetNextInstanceID(); 554 var onInstanceIdAllocated = function(e) { 555 var detail = e.detail ? JSON.parse(e.detail) : {}; 556 self.instanceId_ = detail.windowId; 557 var params = { 558 'api': 'webview', 559 'instanceId': self.viewInstanceId_ 560 }; 561 self.browserPluginNode_['-internal-attach'](params); 562 563 var events = self.getEvents_(); 564 for (var eventName in events) { 565 self.setupEvent_(eventName, events[eventName]); 566 } 567 }; 568 this.browserPluginNode_.addEventListener('-internal-instanceid-allocated', 569 onInstanceIdAllocated); 570 this.setupWebRequestEvents_(); 571}; 572 573/** 574 * @private 575 */ 576WebViewInternal.prototype.setupEvent_ = function(eventName, eventInfo) { 577 var self = this; 578 var webviewNode = this.webviewNode_; 579 eventInfo.evt.addListener(function(event) { 580 var details = {bubbles:true}; 581 if (eventInfo.cancelable) 582 details.cancelable = true; 583 var webViewEvent = new Event(eventName, details); 584 $Array.forEach(eventInfo.fields, function(field) { 585 if (event[field] !== undefined) { 586 webViewEvent[field] = event[field]; 587 } 588 }); 589 if (eventInfo.customHandler) { 590 eventInfo.customHandler(self, event, webViewEvent); 591 return; 592 } 593 webviewNode.dispatchEvent(webViewEvent); 594 }, {instanceId: self.instanceId_}); 595}; 596 597/** 598 * @private 599 */ 600WebViewInternal.prototype.getPermissionTypes_ = function() { 601 return ['media', 'geolocation', 'pointerLock', 'download']; 602}; 603 604/** 605 * @private 606 */ 607WebViewInternal.prototype.handleLoadCommitEvent_ = 608 function(event, webViewEvent) { 609 this.currentEntryIndex_ = event.currentEntryIndex; 610 this.entryCount_ = event.entryCount; 611 this.processId_ = event.processId; 612 var oldValue = this.webviewNode_.getAttribute('src'); 613 var newValue = event.url; 614 if (event.isTopLevel && (oldValue != newValue)) { 615 // Touching the src attribute triggers a navigation. To avoid 616 // triggering a page reload on every guest-initiated navigation, 617 // we use the flag ignoreNextSrcAttributeChange_ here. 618 this.ignoreNextSrcAttributeChange_ = true; 619 this.webviewNode_.setAttribute('src', newValue); 620 } 621 this.webviewNode_.dispatchEvent(webViewEvent); 622} 623 624/** 625 * @private 626 */ 627WebViewInternal.prototype.handleNewWindowEvent_ = 628 function(event, webViewEvent) { 629 var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' + 630 'An action has already been taken for this "newwindow" event.'; 631 632 var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' + 633 'Unable to attach the new window to the provided webview.'; 634 635 var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.'; 636 637 var showWarningMessage = function() { 638 var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.'; 639 console.warn(WARNING_MSG_NEWWINDOW_BLOCKED); 640 }; 641 642 var self = this; 643 var browserPluginNode = this.browserPluginNode_; 644 var webviewNode = this.webviewNode_; 645 646 var requestId = event.requestId; 647 var actionTaken = false; 648 649 var validateCall = function () { 650 if (actionTaken) { 651 throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN); 652 } 653 actionTaken = true; 654 }; 655 656 var window = { 657 attach: function(webview) { 658 validateCall(); 659 if (!webview) 660 throw new Error(ERROR_MSG_WEBVIEW_EXPECTED); 661 // Attach happens asynchronously to give the tagWatcher an opportunity 662 // to pick up the new webview before attach operates on it, if it hasn't 663 // been attached to the DOM already. 664 // Note: Any subsequent errors cannot be exceptions because they happen 665 // asynchronously. 666 setTimeout(function() { 667 var attached = 668 browserPluginNode['-internal-attachWindowTo'](webview, 669 event.windowId); 670 if (!attached) { 671 console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH); 672 } 673 // If the object being passed into attach is not a valid <webview> 674 // then we will fail and it will be treated as if the new window 675 // was rejected. The permission API plumbing is used here to clean 676 // up the state created for the new window if attaching fails. 677 WebView.setPermission(self.instanceId_, requestId, attached, ''); 678 }, 0); 679 }, 680 discard: function() { 681 validateCall(); 682 WebView.setPermission(self.instanceId_, requestId, false, ''); 683 } 684 }; 685 webViewEvent.window = window; 686 687 var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent); 688 if (actionTaken) { 689 return; 690 } 691 692 if (defaultPrevented) { 693 // Make browser plugin track lifetime of |window|. 694 MessagingNatives.BindToGC(window, function() { 695 // Avoid showing a warning message if the decision has already been made. 696 if (actionTaken) { 697 return; 698 } 699 WebView.setPermission(self.instanceId_, requestId, false, ''); 700 showWarningMessage(); 701 }); 702 } else { 703 actionTaken = true; 704 // The default action is to discard the window. 705 WebView.setPermission(self.instanceId_, requestId, false, ''); 706 showWarningMessage(); 707 } 708}; 709 710WebViewInternal.prototype.handlePermissionEvent_ = 711 function(event, webViewEvent) { 712 var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' + 713 'Permission has already been decided for this "permissionrequest" event.'; 714 715 var showWarningMessage = function(permission) { 716 var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' + 717 'The permission request for "%1" has been denied.'; 718 console.warn(WARNING_MSG_PERMISSION_DENIED.replace('%1', permission)); 719 }; 720 721 var PERMISSION_TYPES = this.getPermissionTypes_(); 722 723 var self = this; 724 var browserPluginNode = this.browserPluginNode_; 725 var webviewNode = this.webviewNode_; 726 727 var requestId = event.requestId; 728 var decisionMade = false; 729 730 var validateCall = function() { 731 if (decisionMade) { 732 throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED); 733 } 734 decisionMade = true; 735 }; 736 737 // Construct the event.request object. 738 var request = { 739 allow: function() { 740 validateCall(); 741 WebView.setPermission(self.instanceId_, requestId, true, ''); 742 }, 743 deny: function() { 744 validateCall(); 745 WebView.setPermission(self.instanceId_, requestId, false, ''); 746 } 747 }; 748 webViewEvent.request = request; 749 750 var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent); 751 if (decisionMade) { 752 return; 753 } 754 755 if (defaultPrevented) { 756 // Make browser plugin track lifetime of |request|. 757 MessagingNatives.BindToGC(request, function() { 758 // Avoid showing a warning message if the decision has already been made. 759 if (decisionMade) { 760 return; 761 } 762 WebView.setPermission(self.instanceId_, requestId, false, ''); 763 showWarningMessage(event.permission); 764 }); 765 } else { 766 decisionMade = true; 767 WebView.setPermission(self.instanceId_, requestId, false, ''); 768 showWarningMessage(event.permission); 769 } 770}; 771 772/** 773 * @private 774 */ 775WebViewInternal.prototype.setupWebRequestEvents_ = function() { 776 var self = this; 777 var request = {}; 778 var createWebRequestEvent = function(webRequestEvent) { 779 return function() { 780 if (!self[webRequestEvent.name + '_']) { 781 self[webRequestEvent.name + '_'] = 782 new WebRequestEvent( 783 'webview.' + webRequestEvent.name, 784 webRequestEvent.parameters, 785 webRequestEvent.extraParameters, null, 786 self.viewInstanceId_); 787 } 788 return self[webRequestEvent.name + '_']; 789 }; 790 }; 791 792 // Populate the WebRequest events from the API definition. 793 for (var i = 0; i < WebRequestSchema.events.length; ++i) { 794 var webRequestEvent = createWebRequestEvent(WebRequestSchema.events[i]); 795 Object.defineProperty( 796 request, 797 WebRequestSchema.events[i].name, 798 { 799 get: webRequestEvent, 800 enumerable: true 801 } 802 ); 803 this.maybeAttachWebRequestEventToWebview_(WebRequestSchema.events[i].name, 804 webRequestEvent); 805 } 806 Object.defineProperty( 807 this.webviewNode_, 808 'request', 809 { 810 value: request, 811 enumerable: true, 812 writable: false 813 } 814 ); 815}; 816 817// Registers browser plugin <object> custom element. 818function registerBrowserPluginElement() { 819 var proto = Object.create(HTMLObjectElement.prototype); 820 821 proto.createdCallback = function() { 822 this.setAttribute('type', 'application/browser-plugin'); 823 // The <object> node fills in the <webview> container. 824 this.style.width = '100%'; 825 this.style.height = '100%'; 826 }; 827 828 proto.attributeChangedCallback = function(name, oldValue, newValue) { 829 if (!this.internal_) { 830 return; 831 } 832 var internal = this.internal_(secret); 833 internal.handleBrowserPluginAttributeMutation_(name, newValue); 834 }; 835 836 WebViewInternal.BrowserPlugin = 837 DocumentNatives.RegisterElement('browser-plugin', {extends: 'object', 838 prototype: proto}); 839 840 delete proto.createdCallback; 841 delete proto.enteredDocumentCallback; 842 delete proto.leftDocumentCallback; 843 delete proto.attributeChangedCallback; 844} 845 846// Registers <webview> custom element. 847function registerWebViewElement() { 848 var proto = Object.create(HTMLElement.prototype); 849 850 proto.createdCallback = function() { 851 new WebViewInternal(this); 852 }; 853 854 proto.attributeChangedCallback = function(name, oldValue, newValue) { 855 var internal = this.internal_(secret); 856 internal.handleWebviewAttributeMutation_(name, oldValue, newValue); 857 }; 858 859 proto.back = function() { 860 this.go(-1); 861 }; 862 863 proto.forward = function() { 864 this.go(1); 865 }; 866 867 proto.canGoBack = function() { 868 return this.internal_(secret).canGoBack_(); 869 }; 870 871 proto.canGoForward = function() { 872 return this.internal_(secret).canGoForward_(); 873 }; 874 875 proto.getProcessId = function() { 876 return this.internal_(secret).getProcessId_(); 877 }; 878 879 proto.go = function(relativeIndex) { 880 this.internal_(secret).go_(relativeIndex); 881 }; 882 883 proto.reload = function() { 884 this.internal_(secret).reload_(); 885 }; 886 887 proto.stop = function() { 888 this.internal_(secret).stop_(); 889 }; 890 891 proto.terminate = function() { 892 this.internal_(secret).terminate_(); 893 }; 894 895 proto.executeScript = function(var_args) { 896 var internal = this.internal_(secret); 897 $Function.apply(internal.executeScript_, internal, arguments); 898 }; 899 900 proto.insertCSS = function(var_args) { 901 var internal = this.internal_(secret); 902 $Function.apply(internal.insertCSS_, internal, arguments); 903 }; 904 WebViewInternal.maybeRegisterExperimentalAPIs(proto, secret); 905 906 window.WebView = 907 DocumentNatives.RegisterElement('webview', {prototype: proto}); 908 909 // Delete the callbacks so developers cannot call them and produce unexpected 910 // behavior. 911 delete proto.createdCallback; 912 delete proto.enteredDocumentCallback; 913 delete proto.leftDocumentCallback; 914 delete proto.attributeChangedCallback; 915} 916 917var useCapture = true; 918window.addEventListener('readystatechange', function listener(event) { 919 if (document.readyState == 'loading') 920 return; 921 922 registerBrowserPluginElement(); 923 registerWebViewElement(); 924 window.removeEventListener(event.type, listener, useCapture); 925}, useCapture); 926 927/** 928 * Implemented when the experimental API is available. 929 * @private 930 */ 931WebViewInternal.prototype.maybeGetExperimentalEvents_ = function() {}; 932 933/** 934 * Implemented when the experimental API is available. 935 * @private 936 */ 937WebViewInternal.prototype.maybeAttachWebRequestEventToWebview_ = function() {}; 938 939exports.WebView = WebView; 940exports.WebViewInternal = WebViewInternal; 941exports.CreateEvent = CreateEvent; 942