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