Javascript  |  1072行  |  31.55 KB

// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Shim that simulates a <webview> tag via Mutation Observers.
//
// The actual tag is implemented via the browser plugin. The internals of this
// are hidden via Shadow DOM.

'use strict';

var DocumentNatives = requireNative('document_natives');
var EventBindings = require('event_bindings');
var IdGenerator = requireNative('id_generator');
var MessagingNatives = requireNative('messaging_natives');
var WebRequestEvent = require('webRequestInternal').WebRequestEvent;
var WebRequestSchema =
    requireNative('schema_registry').GetSchema('webRequest');
var DeclarativeWebRequestSchema =
    requireNative('schema_registry').GetSchema('declarativeWebRequest');
var WebView = require('binding').Binding.create('webview').generate();

// This secret enables hiding <webview> private members from the outside scope.
// Outside of this file, |secret| is inaccessible. The only way to access the
// <webview> element's internal members is via the |secret|. Since it's only
// accessible by code here (and in web_view_experimental), only <webview>'s
// API can access it and not external developers.
var secret = {};

var WEB_VIEW_ATTRIBUTE_MAXHEIGHT = 'maxheight';
var WEB_VIEW_ATTRIBUTE_MAXWIDTH = 'maxwidth';
var WEB_VIEW_ATTRIBUTE_MINHEIGHT = 'minheight';
var WEB_VIEW_ATTRIBUTE_MINWIDTH = 'minwidth';

/** @type {Array.<string>} */
var WEB_VIEW_ATTRIBUTES = [
    'allowtransparency',
    'autosize',
    'name',
    'partition',
    WEB_VIEW_ATTRIBUTE_MINHEIGHT,
    WEB_VIEW_ATTRIBUTE_MINWIDTH,
    WEB_VIEW_ATTRIBUTE_MAXHEIGHT,
    WEB_VIEW_ATTRIBUTE_MAXWIDTH
];

var CreateEvent = function(name) {
  var eventOpts = {supportsListeners: true, supportsFilters: true};
  return new EventBindings.Event(name, undefined, eventOpts);
};

var WEB_VIEW_EVENTS = {
  'close': {
    evt: CreateEvent('webview.onClose'),
    fields: []
  },
  'consolemessage': {
    evt: CreateEvent('webview.onConsoleMessage'),
    fields: ['level', 'message', 'line', 'sourceId']
  },
  'contentload': {
    evt: CreateEvent('webview.onContentLoad'),
    fields: []
  },
  'exit': {
     evt: CreateEvent('webview.onExit'),
     fields: ['processId', 'reason']
  },
  'loadabort': {
    cancelable: true,
    customHandler: function(webViewInternal, event, webViewEvent) {
      webViewInternal.handleLoadAbortEvent_(event, webViewEvent);
    },
    evt: CreateEvent('webview.onLoadAbort'),
    fields: ['url', 'isTopLevel', 'reason']
  },
  'loadcommit': {
    customHandler: function(webViewInternal, event, webViewEvent) {
      webViewInternal.handleLoadCommitEvent_(event, webViewEvent);
    },
    evt: CreateEvent('webview.onLoadCommit'),
    fields: ['url', 'isTopLevel']
  },
  'loadprogress': {
    evt: CreateEvent('webview.onLoadProgress'),
    fields: ['url', 'progress']
  },
  'loadredirect': {
    evt: CreateEvent('webview.onLoadRedirect'),
    fields: ['isTopLevel', 'oldUrl', 'newUrl']
  },
  'loadstart': {
    evt: CreateEvent('webview.onLoadStart'),
    fields: ['url', 'isTopLevel']
  },
  'loadstop': {
    evt: CreateEvent('webview.onLoadStop'),
    fields: []
  },
  'newwindow': {
    cancelable: true,
    customHandler: function(webViewInternal, event, webViewEvent) {
      webViewInternal.handleNewWindowEvent_(event, webViewEvent);
    },
    evt: CreateEvent('webview.onNewWindow'),
    fields: [
      'initialHeight',
      'initialWidth',
      'targetUrl',
      'windowOpenDisposition',
      'name'
    ]
  },
  'permissionrequest': {
    cancelable: true,
    customHandler: function(webViewInternal, event, webViewEvent) {
      webViewInternal.handlePermissionEvent_(event, webViewEvent);
    },
    evt: CreateEvent('webview.onPermissionRequest'),
    fields: [
      'identifier',
      'lastUnlockedBySelf',
      'name',
      'permission',
      'requestMethod',
      'url',
      'userGesture'
    ]
  },
  'responsive': {
    evt: CreateEvent('webview.onResponsive'),
    fields: ['processId']
  },
  'sizechanged': {
    evt: CreateEvent('webview.onSizeChanged'),
    customHandler: function(webViewInternal, event, webViewEvent) {
      webViewInternal.handleSizeChangedEvent_(event, webViewEvent);
    },
    fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth']
  },
  'unresponsive': {
    evt: CreateEvent('webview.onUnresponsive'),
    fields: ['processId']
  }
};

// Implemented when the experimental API is available.
WebViewInternal.maybeRegisterExperimentalAPIs = function(proto) {}

/**
 * @constructor
 */
function WebViewInternal(webviewNode) {
  this.webviewNode_ = webviewNode;
  this.browserPluginNode_ = this.createBrowserPluginNode_();
  var shadowRoot = this.webviewNode_.webkitCreateShadowRoot();
  shadowRoot.appendChild(this.browserPluginNode_);

  this.setupWebviewNodeAttributes_();
  this.setupFocusPropagation_();
  this.setupWebviewNodeProperties_();
  this.setupWebviewNodeEvents_();
}

/**
 * @private
 */
WebViewInternal.prototype.createBrowserPluginNode_ = function() {
  // We create BrowserPlugin as a custom element in order to observe changes
  // to attributes synchronously.
  var browserPluginNode = new WebViewInternal.BrowserPlugin();
  Object.defineProperty(browserPluginNode, 'internal_', {
    enumerable: false,
    writable: false,
    value: function(key) {
      if (key !== secret) {
        return null;
      }
      return this;
    }.bind(this)
  });

  var ALL_ATTRIBUTES = WEB_VIEW_ATTRIBUTES.concat(['src']);
  $Array.forEach(ALL_ATTRIBUTES, function(attributeName) {
    // Only copy attributes that have been assigned values, rather than copying
    // a series of undefined attributes to BrowserPlugin.
    if (this.webviewNode_.hasAttribute(attributeName)) {
      browserPluginNode.setAttribute(
        attributeName, this.webviewNode_.getAttribute(attributeName));
    } else if (this.webviewNode_[attributeName]){
      // Reading property using has/getAttribute does not work on
      // document.DOMContentLoaded event (but works on
      // window.DOMContentLoaded event).
      // So copy from property if copying from attribute fails.
      browserPluginNode.setAttribute(
        attributeName, this.webviewNode_[attributeName]);
    }
  }, this);

  return browserPluginNode;
};

/**
 * @private
 */
WebViewInternal.prototype.setupFocusPropagation_ = function() {
  if (!this.webviewNode_.hasAttribute('tabIndex')) {
    // <webview> needs a tabIndex in order to be focusable.
    // TODO(fsamuel): It would be nice to avoid exposing a tabIndex attribute
    // to allow <webview> to be focusable.
    // See http://crbug.com/231664.
    this.webviewNode_.setAttribute('tabIndex', -1);
  }
  var self = this;
  this.webviewNode_.addEventListener('focus', function(e) {
    // Focus the BrowserPlugin when the <webview> takes focus.
    self.browserPluginNode_.focus();
  });
  this.webviewNode_.addEventListener('blur', function(e) {
    // Blur the BrowserPlugin when the <webview> loses focus.
    self.browserPluginNode_.blur();
  });
};

/**
 * @private
 */
WebViewInternal.prototype.canGoBack_ = function() {
  return this.entryCount_ > 1 && this.currentEntryIndex_ > 0;
};

/**
 * @private
 */
WebViewInternal.prototype.canGoForward_ = function() {
  return this.currentEntryIndex_ >= 0 &&
      this.currentEntryIndex_ < (this.entryCount_ - 1);
};

/**
 * @private
 */
WebViewInternal.prototype.getProcessId_ = function() {
  return this.processId_;
};

/**
 * @private
 */
WebViewInternal.prototype.go_ = function(relativeIndex) {
  if (!this.instanceId_) {
    return;
  }
  WebView.go(this.instanceId_, relativeIndex);
};

/**
 * @private
 */
WebViewInternal.prototype.reload_ = function() {
  if (!this.instanceId_) {
    return;
  }
  WebView.reload(this.instanceId_);
};

/**
 * @private
 */
WebViewInternal.prototype.stop_ = function() {
  if (!this.instanceId_) {
    return;
  }
  WebView.stop(this.instanceId_);
};

/**
 * @private
 */
WebViewInternal.prototype.terminate_ = function() {
  if (!this.instanceId_) {
    return;
  }
  WebView.terminate(this.instanceId_);
};

/**
 * @private
 */
WebViewInternal.prototype.validateExecuteCodeCall_  = function() {
  var ERROR_MSG_CANNOT_INJECT_SCRIPT = '<webview>: ' +
      'Script cannot be injected into content until the page has loaded.';
  if (!this.instanceId_) {
    throw new Error(ERROR_MSG_CANNOT_INJECT_SCRIPT);
  }
};

/**
 * @private
 */
WebViewInternal.prototype.executeScript_ = function(var_args) {
  this.validateExecuteCodeCall_();
  var args = $Array.concat([this.instanceId_], $Array.slice(arguments));
  $Function.apply(WebView.executeScript, null, args);
};

/**
 * @private
 */
WebViewInternal.prototype.insertCSS_ = function(var_args) {
  this.validateExecuteCodeCall_();
  var args = $Array.concat([this.instanceId_], $Array.slice(arguments));
  $Function.apply(WebView.insertCSS, null, args);
};

/**
 * @private
 */
WebViewInternal.prototype.setupWebviewNodeProperties_ = function() {
  var ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE = '<webview>: ' +
    'contentWindow is not available at this time. It will become available ' +
        'when the page has finished loading.';

  var self = this;
  var browserPluginNode = this.browserPluginNode_;
  // Expose getters and setters for the attributes.
  $Array.forEach(WEB_VIEW_ATTRIBUTES, function(attributeName) {
    Object.defineProperty(this.webviewNode_, attributeName, {
      get: function() {
        if (browserPluginNode.hasOwnProperty(attributeName)) {
          return browserPluginNode[attributeName];
        } else {
          return browserPluginNode.getAttribute(attributeName);
        }
      },
      set: function(value) {
        if (browserPluginNode.hasOwnProperty(attributeName)) {
          // Give the BrowserPlugin first stab at the attribute so that it can
          // throw an exception if there is a problem. This attribute will then
          // be propagated back to the <webview>.
          browserPluginNode[attributeName] = value;
        } else {
          browserPluginNode.setAttribute(attributeName, value);
        }
      },
      enumerable: true
    });
  }, this);

  // <webview> src does not quite behave the same as BrowserPlugin src, and so
  // we don't simply keep the two in sync.
  this.src_ = this.webviewNode_.getAttribute('src');
  Object.defineProperty(this.webviewNode_, 'src', {
    get: function() {
      return self.src_;
    },
    set: function(value) {
      self.webviewNode_.setAttribute('src', value);
    },
    // No setter.
    enumerable: true
  });

  // We cannot use {writable: true} property descriptor because we want a
  // dynamic getter value.
  Object.defineProperty(this.webviewNode_, 'contentWindow', {
    get: function() {
      if (browserPluginNode.contentWindow)
        return browserPluginNode.contentWindow;
      window.console.error(ERROR_MSG_CONTENTWINDOW_NOT_AVAILABLE);
    },
    // No setter.
    enumerable: true
  });
};

/**
 * @private
 */
WebViewInternal.prototype.setupWebviewNodeAttributes_ = function() {
  Object.defineProperty(this.webviewNode_, 'internal_', {
    enumerable: false,
    writable: false,
    value: function(key) {
      if (key !== secret) {
        return null;
      }
      return this;
    }.bind(this)
  });
  this.setupWebViewSrcAttributeMutationObserver_();
};

/**
 * @private
 */
WebViewInternal.prototype.setupWebViewSrcAttributeMutationObserver_ =
    function() {
  // The purpose of this mutation observer is to catch assignment to the src
  // attribute without any changes to its value. This is useful in the case
  // where the webview guest has crashed and navigating to the same address
  // spawns off a new process.
  var self = this;
  this.srcObserver_ = new MutationObserver(function(mutations) {
    $Array.forEach(mutations, function(mutation) {
      var oldValue = mutation.oldValue;
      var newValue = self.webviewNode_.getAttribute(mutation.attributeName);
      if (oldValue != newValue) {
        return;
      }
      self.handleWebviewAttributeMutation_(
          mutation.attributeName, oldValue, newValue);
    });
  });
  var params = {
    attributes: true,
    attributeOldValue: true,
    attributeFilter: ['src']
  };
  this.srcObserver_.observe(this.webviewNode_, params);
};

/**
 * @private
 */
WebViewInternal.prototype.handleWebviewAttributeMutation_ =
      function(name, oldValue, newValue) {
  // This observer monitors mutations to attributes of the <webview> and
  // updates the BrowserPlugin properties accordingly. In turn, updating
  // a BrowserPlugin property will update the corresponding BrowserPlugin
  // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
  // details.
  if (name == 'src') {
    // We treat null attribute (attribute removed) and the empty string as
    // one case.
    oldValue = oldValue || '';
    newValue = newValue || '';
    // Once we have navigated, we don't allow clearing the src attribute.
    // Once <webview> enters a navigated state, it cannot be return back to a
    // placeholder state.
    if (newValue == '' && oldValue != '') {
      // src attribute changes normally initiate a navigation. We suppress
      // the next src attribute handler call to avoid reloading the page
      // on every guest-initiated navigation.
      this.ignoreNextSrcAttributeChange_ = true;
      this.webviewNode_.setAttribute('src', oldValue);
      return;
    }
    this.src_ = newValue;
    if (this.ignoreNextSrcAttributeChange_) {
      // Don't allow the src mutation observer to see this change.
      this.srcObserver_.takeRecords();
      this.ignoreNextSrcAttributeChange_ = false;
      return;
    }
  }
  if (this.browserPluginNode_.hasOwnProperty(name)) {
    this.browserPluginNode_[name] = newValue;
  } else {
    this.browserPluginNode_.setAttribute(name, newValue);
  }
};

/**
 * @private
 */
WebViewInternal.prototype.handleBrowserPluginAttributeMutation_ =
    function(name, newValue) {
  // This observer monitors mutations to attributes of the BrowserPlugin and
  // updates the <webview> attributes accordingly.
  // |newValue| is null if the attribute |name| has been removed.
  if (newValue != null) {
    // Update the <webview> attribute to match the BrowserPlugin attribute.
    // Note: Calling setAttribute on <webview> will trigger its mutation
    // observer which will then propagate that attribute to BrowserPlugin. In
    // cases where we permit assigning a BrowserPlugin attribute the same value
    // again (such as navigation when crashed), this could end up in an infinite
    // loop. Thus, we avoid this loop by only updating the <webview> attribute
    // if the BrowserPlugin attributes differs from it.
    if (newValue != this.webviewNode_.getAttribute(name)) {
      this.webviewNode_.setAttribute(name, newValue);
    }
  } else {
    // If an attribute is removed from the BrowserPlugin, then remove it
    // from the <webview> as well.
    this.webviewNode_.removeAttribute(name);
  }
};

/**
 * @private
 */
WebViewInternal.prototype.getEvents_ = function() {
  var experimentalEvents = this.maybeGetExperimentalEvents_();
  for (var eventName in experimentalEvents) {
    WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName];
  }
  return WEB_VIEW_EVENTS;
};

WebViewInternal.prototype.handleSizeChangedEvent_ =
    function(event, webViewEvent) {
  var node = this.webviewNode_;

  var width = node.offsetWidth;
  var height = node.offsetHeight;

  // Check the current bounds to make sure we do not resize <webview>
  // outside of current constraints.
  var maxWidth;
  if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXWIDTH) &&
      node[WEB_VIEW_ATTRIBUTE_MAXWIDTH]) {
    maxWidth = node[WEB_VIEW_ATTRIBUTE_MAXWIDTH];
  } else {
    maxWidth = width;
  }

  var minWidth;
  if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINWIDTH) &&
      node[WEB_VIEW_ATTRIBUTE_MINWIDTH]) {
    minWidth = node[WEB_VIEW_ATTRIBUTE_MINWIDTH];
  } else {
    minWidth = width;
  }
  if (minWidth > maxWidth) {
    minWidth = maxWidth;
  }

  var maxHeight;
  if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MAXHEIGHT) &&
      node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT]) {
    maxHeight = node[WEB_VIEW_ATTRIBUTE_MAXHEIGHT];
  } else {
    maxHeight = height;
  }
  var minHeight;
  if (node.hasAttribute(WEB_VIEW_ATTRIBUTE_MINHEIGHT) &&
      node[WEB_VIEW_ATTRIBUTE_MINHEIGHT]) {
    minHeight = node[WEB_VIEW_ATTRIBUTE_MINHEIGHT];
  } else {
    minHeight = height;
  }
  if (minHeight > maxHeight) {
    minHeight = maxHeight;
  }

  if (webViewEvent.newWidth >= minWidth &&
      webViewEvent.newWidth <= maxWidth &&
      webViewEvent.newHeight >= minHeight &&
      webViewEvent.newHeight <= maxHeight) {
    node.style.width = webViewEvent.newWidth + 'px';
    node.style.height = webViewEvent.newHeight + 'px';
  }
  node.dispatchEvent(webViewEvent);
};

/**
 * @private
 */
WebViewInternal.prototype.setupWebviewNodeEvents_ = function() {
  var self = this;
  this.viewInstanceId_ = IdGenerator.GetNextId();
  var onInstanceIdAllocated = function(e) {
    var detail = e.detail ? JSON.parse(e.detail) : {};
    self.instanceId_ = detail.windowId;
    var params = {
      'api': 'webview',
      'instanceId': self.viewInstanceId_
    };
    if (self.userAgentOverride_) {
      params['userAgentOverride'] = self.userAgentOverride_;
    }
    self.browserPluginNode_['-internal-attach'](params);

    var events = self.getEvents_();
    for (var eventName in events) {
      self.setupEvent_(eventName, events[eventName]);
    }
  };
  this.browserPluginNode_.addEventListener('-internal-instanceid-allocated',
                                           onInstanceIdAllocated);
  this.setupWebRequestEvents_();

  this.on_ = {};
  var events = self.getEvents_();
  for (var eventName in events) {
    this.setupEventProperty_(eventName);
  }
};

/**
 * @private
 */
WebViewInternal.prototype.setupEvent_ = function(eventName, eventInfo) {
  var self = this;
  var webviewNode = this.webviewNode_;
  eventInfo.evt.addListener(function(event) {
    var details = {bubbles:true};
    if (eventInfo.cancelable)
      details.cancelable = true;
    var webViewEvent = new Event(eventName, details);
    $Array.forEach(eventInfo.fields, function(field) {
      if (event[field] !== undefined) {
        webViewEvent[field] = event[field];
      }
    });
    if (eventInfo.customHandler) {
      eventInfo.customHandler(self, event, webViewEvent);
      return;
    }
    webviewNode.dispatchEvent(webViewEvent);
  }, {instanceId: self.instanceId_});
};

/**
 * Adds an 'on<event>' property on the webview, which can be used to set/unset
 * an event handler.
 * @private
 */
WebViewInternal.prototype.setupEventProperty_ = function(eventName) {
  var propertyName = 'on' + eventName.toLowerCase();
  var self = this;
  var webviewNode = this.webviewNode_;
  Object.defineProperty(webviewNode, propertyName, {
    get: function() {
      return self.on_[propertyName];
    },
    set: function(value) {
      if (self.on_[propertyName])
        webviewNode.removeEventListener(eventName, self.on_[propertyName]);
      self.on_[propertyName] = value;
      if (value)
        webviewNode.addEventListener(eventName, value);
    },
    enumerable: true
  });
};

/**
 * @private
 */
WebViewInternal.prototype.getPermissionTypes_ = function() {
  return ['media', 'geolocation', 'pointerLock', 'download', 'loadplugin'];
};

/**
 * @private
 */
WebViewInternal.prototype.handleLoadAbortEvent_ =
    function(event, webViewEvent) {
  var showWarningMessage = function(reason) {
    var WARNING_MSG_LOAD_ABORTED = '<webview>: ' +
        'The load has aborted with reason "%1".';
    window.console.warn(WARNING_MSG_LOAD_ABORTED.replace('%1', reason));
  };
  if (this.webviewNode_.dispatchEvent(webViewEvent)) {
    showWarningMessage(event.reason);
  }
};

/**
 * @private
 */
WebViewInternal.prototype.handleLoadCommitEvent_ =
    function(event, webViewEvent) {
  this.currentEntryIndex_ = event.currentEntryIndex;
  this.entryCount_ = event.entryCount;
  this.processId_ = event.processId;
  var oldValue = this.webviewNode_.getAttribute('src');
  var newValue = event.url;
  if (event.isTopLevel && (oldValue != newValue)) {
    // Touching the src attribute triggers a navigation. To avoid
    // triggering a page reload on every guest-initiated navigation,
    // we use the flag ignoreNextSrcAttributeChange_ here.
    this.ignoreNextSrcAttributeChange_ = true;
    this.webviewNode_.setAttribute('src', newValue);
  }
  this.webviewNode_.dispatchEvent(webViewEvent);
}

/**
 * @private
 */
WebViewInternal.prototype.handleNewWindowEvent_ =
    function(event, webViewEvent) {
  var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' +
      'An action has already been taken for this "newwindow" event.';

  var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' +
      'Unable to attach the new window to the provided webview.';

  var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.';

  var showWarningMessage = function() {
    var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.';
    window.console.warn(WARNING_MSG_NEWWINDOW_BLOCKED);
  };

  var self = this;
  var browserPluginNode = this.browserPluginNode_;
  var webviewNode = this.webviewNode_;

  var requestId = event.requestId;
  var actionTaken = false;

  var validateCall = function () {
    if (actionTaken) {
      throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN);
    }
    actionTaken = true;
  };

  var windowObj = {
    attach: function(webview) {
      validateCall();
      if (!webview)
        throw new Error(ERROR_MSG_WEBVIEW_EXPECTED);
      // Attach happens asynchronously to give the tagWatcher an opportunity
      // to pick up the new webview before attach operates on it, if it hasn't
      // been attached to the DOM already.
      // Note: Any subsequent errors cannot be exceptions because they happen
      // asynchronously.
      setTimeout(function() {
        var attached =
            browserPluginNode['-internal-attachWindowTo'](webview,
                                                          event.windowId);
        if (!attached) {
          window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH);
        }
        // If the object being passed into attach is not a valid <webview>
        // then we will fail and it will be treated as if the new window
        // was rejected. The permission API plumbing is used here to clean
        // up the state created for the new window if attaching fails.
        WebView.setPermission(
            self.instanceId_, requestId, attached ? 'allow' : 'deny');
      }, 0);
    },
    discard: function() {
      validateCall();
      WebView.setPermission(self.instanceId_, requestId, 'deny');
    }
  };
  webViewEvent.window = windowObj;

  var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent);
  if (actionTaken) {
    return;
  }

  if (defaultPrevented) {
    // Make browser plugin track lifetime of |windowObj|.
    MessagingNatives.BindToGC(windowObj, function() {
      // Avoid showing a warning message if the decision has already been made.
      if (actionTaken) {
        return;
      }
      WebView.setPermission(
          self.instanceId_, requestId, 'default', '', function(allowed) {
        if (allowed) {
          return;
        }
        showWarningMessage();
      });
    });
  } else {
    actionTaken = true;
    // The default action is to discard the window.
    WebView.setPermission(
        self.instanceId_, requestId, 'default', '', function(allowed) {
      if (allowed) {
        return;
      }
      showWarningMessage();
    });
  }
};

WebViewInternal.prototype.handlePermissionEvent_ =
    function(event, webViewEvent) {
  var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' +
      'Permission has already been decided for this "permissionrequest" event.';

  var showWarningMessage = function(permission) {
    var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' +
        'The permission request for "%1" has been denied.';
    window.console.warn(
        WARNING_MSG_PERMISSION_DENIED.replace('%1', permission));
  };

  var requestId = event.requestId;
  var self = this;

  var PERMISSION_TYPES = this.getPermissionTypes_().concat(
                             this.maybeGetExperimentalPermissions_());
  if (PERMISSION_TYPES.indexOf(event.permission) < 0) {
    // The permission type is not allowed. Trigger the default response.
    WebView.setPermission(
        self.instanceId_, requestId, 'default', '', function(allowed) {
      if (allowed) {
        return;
      }
      showWarningMessage(event.permission);
    });
    return;
  }

  var browserPluginNode = this.browserPluginNode_;
  var webviewNode = this.webviewNode_;

  var decisionMade = false;

  var validateCall = function() {
    if (decisionMade) {
      throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED);
    }
    decisionMade = true;
  };

  // Construct the event.request object.
  var request = {
    allow: function() {
      validateCall();
      WebView.setPermission(self.instanceId_, requestId, 'allow');
    },
    deny: function() {
      validateCall();
      WebView.setPermission(self.instanceId_, requestId, 'deny');
    }
  };
  webViewEvent.request = request;

  var defaultPrevented = !webviewNode.dispatchEvent(webViewEvent);
  if (decisionMade) {
    return;
  }

  if (defaultPrevented) {
    // Make browser plugin track lifetime of |request|.
    MessagingNatives.BindToGC(request, function() {
      // Avoid showing a warning message if the decision has already been made.
      if (decisionMade) {
        return;
      }
      WebView.setPermission(
          self.instanceId_, requestId, 'default', '', function(allowed) {
        if (allowed) {
          return;
        }
        showWarningMessage(event.permission);
      });
    });
  } else {
    decisionMade = true;
    WebView.setPermission(
        self.instanceId_, requestId, 'default', '', function(allowed) {
      if (allowed) {
        return;
      }
      showWarningMessage(event.permission);
    });
  }
};

/**
 * @private
 */
WebViewInternal.prototype.setupWebRequestEvents_ = function() {
  var self = this;
  var request = {};
  var createWebRequestEvent = function(webRequestEvent) {
    return function() {
      if (!self[webRequestEvent.name + '_']) {
        self[webRequestEvent.name + '_'] =
            new WebRequestEvent(
                'webview.' + webRequestEvent.name,
                webRequestEvent.parameters,
                webRequestEvent.extraParameters, webRequestEvent.options,
                self.viewInstanceId_);
      }
      return self[webRequestEvent.name + '_'];
    };
  };

  for (var i = 0; i < DeclarativeWebRequestSchema.events.length; ++i) {
    var eventSchema = DeclarativeWebRequestSchema.events[i];
    var webRequestEvent = createWebRequestEvent(eventSchema);
    this.maybeAttachWebRequestEventToObject_(request,
                                             eventSchema.name,
                                             webRequestEvent);
  }

  // Populate the WebRequest events from the API definition.
  for (var i = 0; i < WebRequestSchema.events.length; ++i) {
    var webRequestEvent = createWebRequestEvent(WebRequestSchema.events[i]);
    Object.defineProperty(
        request,
        WebRequestSchema.events[i].name,
        {
          get: webRequestEvent,
          enumerable: true
        }
    );
    this.maybeAttachWebRequestEventToObject_(this.webviewNode_,
                                             WebRequestSchema.events[i].name,
                                             webRequestEvent);
  }
  Object.defineProperty(
      this.webviewNode_,
      'request',
      {
        value: request,
        enumerable: true,
        writable: false
      }
  );
};

// Registers browser plugin <object> custom element.
function registerBrowserPluginElement() {
  var proto = Object.create(HTMLObjectElement.prototype);

  proto.createdCallback = function() {
    this.setAttribute('type', 'application/browser-plugin');
    // The <object> node fills in the <webview> container.
    this.style.width = '100%';
    this.style.height = '100%';
  };

  proto.attributeChangedCallback = function(name, oldValue, newValue) {
    if (!this.internal_) {
      return;
    }
    var internal = this.internal_(secret);
    internal.handleBrowserPluginAttributeMutation_(name, newValue);
  };

  proto.attachedCallback = function() {
    // Load the plugin immediately.
    var unused = this.nonExistentAttribute;
  };

  // TODO(dominicc): Remove this line once Custom Elements renames
  // enteredViewCallback to attachedCallback
  proto.enteredViewCallback = proto.attachedCallback;

  WebViewInternal.BrowserPlugin =
      DocumentNatives.RegisterElement('browser-plugin', {extends: 'object',
                                                         prototype: proto});

  delete proto.createdCallback;
  delete proto.attachedCallback;
  delete proto.detachedCallback;
  delete proto.attributeChangedCallback;

  // TODO(dominicc): Remove these lines once Custom Elements renames
  // enteredView, leftView callbacks to attached, detached
  // respectively.
  delete proto.enteredViewCallback;
  delete proto.leftViewCallback;
}

// Registers <webview> custom element.
function registerWebViewElement() {
  var proto = Object.create(HTMLElement.prototype);

  proto.createdCallback = function() {
    new WebViewInternal(this);
  };

  proto.attributeChangedCallback = function(name, oldValue, newValue) {
    var internal = this.internal_(secret);
    internal.handleWebviewAttributeMutation_(name, oldValue, newValue);
  };

  proto.back = function() {
    this.go(-1);
  };

  proto.forward = function() {
    this.go(1);
  };

  proto.canGoBack = function() {
    return this.internal_(secret).canGoBack_();
  };

  proto.canGoForward = function() {
    return this.internal_(secret).canGoForward_();
  };

  proto.getProcessId = function() {
    return this.internal_(secret).getProcessId_();
  };

  proto.go = function(relativeIndex) {
    this.internal_(secret).go_(relativeIndex);
  };

  proto.reload = function() {
    this.internal_(secret).reload_();
  };

  proto.stop = function() {
    this.internal_(secret).stop_();
  };

  proto.terminate = function() {
    this.internal_(secret).terminate_();
  };

  proto.executeScript = function(var_args) {
    var internal = this.internal_(secret);
    $Function.apply(internal.executeScript_, internal, arguments);
  };

  proto.insertCSS = function(var_args) {
    var internal = this.internal_(secret);
    $Function.apply(internal.insertCSS_, internal, arguments);
  };
  WebViewInternal.maybeRegisterExperimentalAPIs(proto, secret);

  window.WebView =
      DocumentNatives.RegisterElement('webview', {prototype: proto});

  // Delete the callbacks so developers cannot call them and produce unexpected
  // behavior.
  delete proto.createdCallback;
  delete proto.attachedCallback;
  delete proto.detachedCallback;
  delete proto.attributeChangedCallback;

  // TODO(dominicc): Remove these lines once Custom Elements renames
  // enteredView, leftView callbacks to attached, detached
  // respectively.
  delete proto.enteredViewCallback;
  delete proto.leftViewCallback;
}

var useCapture = true;
window.addEventListener('readystatechange', function listener(event) {
  if (document.readyState == 'loading')
    return;

  registerBrowserPluginElement();
  registerWebViewElement();
  window.removeEventListener(event.type, listener, useCapture);
}, useCapture);

/**
 * Implemented when the experimental API is available.
 * @private
 */
WebViewInternal.prototype.maybeGetExperimentalEvents_ = function() {};

/**
 * Implemented when the experimental API is available.
 * @private
 */
WebViewInternal.prototype.maybeAttachWebRequestEventToObject_ = function() {};

/**
 * Implemented when the experimental API is available.
 * @private
 */
WebViewInternal.prototype.maybeGetExperimentalPermissions_ = function() {
  return [];
};

exports.WebView = WebView;
exports.WebViewInternal = WebViewInternal;
exports.CreateEvent = CreateEvent;