Chromium Code Reviews| Index: chrome/renderer/resources/extensions/web_view_events.js |
| diff --git a/chrome/renderer/resources/extensions/web_view_events.js b/chrome/renderer/resources/extensions/web_view_events.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..30747d2875b32cb3a704b2ca42b40c3f0362de38 |
| --- /dev/null |
| +++ b/chrome/renderer/resources/extensions/web_view_events.js |
| @@ -0,0 +1,593 @@ |
| +// Copyright 2014 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. |
| + |
| +// Event management for WebViewInternal. |
| + |
| +var DeclarativeWebRequestSchema = |
| + requireNative('schema_registry').GetSchema('declarativeWebRequest'); |
| +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 WebView = require('webview').WebView; |
| + |
| +var CreateEvent = function(name) { |
| + var eventOpts = {supportsListeners: true, supportsFilters: true}; |
| + return new EventBindings.Event(name, undefined, eventOpts); |
| +}; |
| + |
| +// WEB_VIEW_EVENTS is a map of stable <webview> DOM event names to their |
|
Fady Samuel
2014/06/19 14:25:26
Move this above WEB_VIEW_EVENTS directly.
|
| +// associated extension event descriptor objects. |
| +// An event listener will be attached to the extension event |evt| specified in |
| +// the descriptor. |
| +// |fields| specifies the public-facing fields in the DOM event that are |
| +// accessible to <webview> developers. |
| +// |customHandler| allows a handler function to be called each time an extension |
| +// event is caught by its event listener. The DOM event should be dispatched |
| +// within this handler function. With no handler function, the DOM event |
| +// will be dispatched by default each time the extension event is caught. |
| +// |cancelable| (default: false) specifies whether the event's default |
| +// behavior can be canceled. If the default action associated with the event |
| +// is prevented, then its dispatch function will return false in its event |
| +// handler. The event must have a custom handler for this to be meaningful. |
| + |
| +var FrameNameChangedEvent = CreateEvent('webview.onFrameNameChanged'); |
| +var WebRequestMessageEvent = CreateEvent('webview.onMessage'); |
| + |
| +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: [] |
| + }, |
| + 'contextmenu': { |
| + evt: CreateEvent('webview.contextmenu'), |
| + cancelable: true, |
| + customHandler: function(handler, event, webViewEvent) { |
| + handler.handleContextMenu(event, webViewEvent); |
| + }, |
| + fields: ['items'] |
| + }, |
| + 'dialog': { |
| + cancelable: true, |
| + customHandler: function(handler, event, webViewEvent) { |
| + handler.handleDialogEvent(event, webViewEvent); |
| + }, |
| + evt: CreateEvent('webview.onDialog'), |
| + fields: ['defaultPromptText', 'messageText', 'messageType', 'url'] |
| + }, |
| + 'exit': { |
| + evt: CreateEvent('webview.onExit'), |
| + fields: ['processId', 'reason'] |
| + }, |
| + 'loadabort': { |
| + cancelable: true, |
| + customHandler: function(handler, event, webViewEvent) { |
| + handler.handleLoadAbortEvent(event, webViewEvent); |
| + }, |
| + evt: CreateEvent('webview.onLoadAbort'), |
| + fields: ['url', 'isTopLevel', 'reason'] |
| + }, |
| + 'loadcommit': { |
| + customHandler: function(handler, event, webViewEvent) { |
| + handler.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(handler, event, webViewEvent) { |
| + handler.handleNewWindowEvent(event, webViewEvent); |
| + }, |
| + evt: CreateEvent('webview.onNewWindow'), |
| + fields: [ |
| + 'initialHeight', |
| + 'initialWidth', |
| + 'targetUrl', |
| + 'windowOpenDisposition', |
| + 'name' |
| + ] |
| + }, |
| + 'permissionrequest': { |
| + cancelable: true, |
| + customHandler: function(handler, event, webViewEvent) { |
| + handler.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(handler, event, webViewEvent) { |
| + handler.handleSizeChangedEvent(event, webViewEvent); |
| + }, |
| + fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth'] |
| + }, |
| + 'unresponsive': { |
| + evt: CreateEvent('webview.onUnresponsive'), |
| + fields: ['processId'] |
| + } |
| +}; |
| + |
| +function DeclarativeWebRequestEvent(opt_eventName, |
| + opt_argSchemas, |
| + opt_eventOptions, |
| + opt_webViewInstanceId) { |
| + var subEventName = opt_eventName + '/' + IdGenerator.GetNextId(); |
| + EventBindings.Event.call(this, subEventName, opt_argSchemas, opt_eventOptions, |
| + opt_webViewInstanceId); |
| + |
| + var self = this; |
| + // TODO(lazyboy): When do we dispose this listener? |
| + WebRequestMessageEvent.addListener(function() { |
| + // Re-dispatch to subEvent's listeners. |
| + $Function.apply(self.dispatch, self, $Array.slice(arguments)); |
| + }, {instanceId: opt_webViewInstanceId || 0}); |
| +} |
| + |
| +DeclarativeWebRequestEvent.prototype = { |
| + __proto__: EventBindings.Event.prototype |
| +}; |
| + |
| +// Constructor. |
| +function WebViewEvents(webViewInternal, viewInstanceId) { |
| + this.webViewInternal = webViewInternal; |
| + this.viewInstanceId = viewInstanceId; |
| + this.setup(); |
| +} |
| + |
| +// Sets up events. |
| +WebViewEvents.prototype.setup = function() { |
| + this.setupFrameNameChangedEvent(); |
| + this.setupWebRequestEvents(); |
| + this.webViewInternal.setupExperimentalContextMenus(); |
| + |
| + var events = this.getEvents(); |
| + for (var eventName in events) { |
| + this.setupEvent(eventName, events[eventName]); |
| + } |
| +}; |
| + |
| +WebViewEvents.prototype.setupFrameNameChangedEvent = function() { |
| + var self = this; |
| + FrameNameChangedEvent.addListener(function(e) { |
| + self.webViewInternal.onFrameNameChanged(e.name); |
| + }, {instanceId: self.viewInstanceId}); |
| +}; |
| + |
| +WebViewEvents.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]; |
| + }; |
| + }; |
| + |
| + var createDeclarativeWebRequestEvent = function(webRequestEvent) { |
| + return function() { |
| + if (!self[webRequestEvent.name]) { |
| + // The onMessage event gets a special event type because we want |
| + // the listener to fire only for messages targeted for this particular |
| + // <webview>. |
| + var EventClass = webRequestEvent.name === 'onMessage' ? |
| + DeclarativeWebRequestEvent : EventBindings.Event; |
| + self[webRequestEvent.name] = |
| + new EventClass( |
| + 'webview.' + webRequestEvent.name, |
| + webRequestEvent.parameters, |
| + webRequestEvent.options, |
| + self.viewInstanceId); |
| + } |
| + return self[webRequestEvent.name]; |
| + }; |
| + }; |
| + |
| + for (var i = 0; i < DeclarativeWebRequestSchema.events.length; ++i) { |
| + var eventSchema = DeclarativeWebRequestSchema.events[i]; |
| + var webRequestEvent = createDeclarativeWebRequestEvent(eventSchema); |
| + Object.defineProperty( |
| + request, |
| + eventSchema.name, |
| + { |
| + get: webRequestEvent, |
| + enumerable: true |
| + } |
| + ); |
| + } |
| + |
| + // 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.webViewInternal.setRequestPropertyOnWebViewNode(request); |
| +}; |
| + |
| +WebViewEvents.prototype.getEvents = function() { |
| + var experimentalEvents = this.webViewInternal.maybeGetExperimentalEvents(); |
| + for (var eventName in experimentalEvents) { |
| + WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName]; |
| + } |
| + return WEB_VIEW_EVENTS; |
| +}; |
| + |
| +WebViewEvents.prototype.setupEvent = function(name, info) { |
| + var self = this; |
| + info.evt.addListener(function(e) { |
| + var details = {bubbles:true}; |
| + if (info.cancelable) |
| + details.cancelable = true; |
| + var webViewEvent = new Event(name, details); |
| + $Array.forEach(info.fields, function(field) { |
| + if (e[field] !== undefined) { |
| + webViewEvent[field] = e[field]; |
| + } |
| + }); |
| + if (info.customHandler) { |
| + info.customHandler(self, e, webViewEvent); |
| + return; |
| + } |
| + self.webViewInternal.dispatchEvent(webViewEvent); |
| + }, {instanceId: self.viewInstanceId}); |
| + |
| + this.webViewInternal.setupEventProperty(name); |
| +}; |
| + |
| + |
| +// Event handlers. |
| +WebViewEvents.prototype.handleContextMenu = function(e, webViewEvent) { |
| + this.webViewInternal.maybeHandleContextMenu(); |
| +}; |
| + |
| +WebViewEvents.prototype.handleDialogEvent = function(event, webViewEvent) { |
| + var showWarningMessage = function(dialogType) { |
| + var VOWELS = ['a', 'e', 'i', 'o', 'u']; |
| + var WARNING_MSG_DIALOG_BLOCKED = '<webview>: %1 %2 dialog was blocked.'; |
| + var article = (VOWELS.indexOf(dialogType.charAt(0)) >= 0) ? 'An' : 'A'; |
| + var output = WARNING_MSG_DIALOG_BLOCKED.replace('%1', article); |
| + output = output.replace('%2', dialogType); |
| + window.console.warn(output); |
| + }; |
| + |
| + var self = this; |
| + var requestId = event.requestId; |
| + var actionTaken = false; |
| + |
| + var validateCall = function() { |
| + var ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN = '<webview>: ' + |
| + 'An action has already been taken for this "dialog" event.'; |
| + |
| + if (actionTaken) { |
| + throw new Error(ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN); |
| + } |
| + actionTaken = true; |
| + }; |
| + |
| + var getInstanceId = function() { |
| + return self.webViewInternal.getInstanceId(); |
| + }; |
| + |
| + var dialog = { |
| + ok: function(user_input) { |
| + validateCall(); |
| + user_input = user_input || ''; |
| + WebView.setPermission(getInstanceId(), requestId, 'allow', user_input); |
| + }, |
| + cancel: function() { |
| + validateCall(); |
| + WebView.setPermission(getInstanceId(), requestId, 'deny'); |
| + } |
| + }; |
| + webViewEvent.dialog = dialog; |
| + |
| + var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent); |
| + if (actionTaken) { |
| + return; |
| + } |
| + |
| + if (defaultPrevented) { |
| + // Tell the JavaScript garbage collector to track lifetime of |dialog| and |
| + // call back when the dialog object has been collected. |
| + MessagingNatives.BindToGC(dialog, function() { |
| + // Avoid showing a warning message if the decision has already been made. |
| + if (actionTaken) { |
| + return; |
| + } |
| + WebView.setPermission( |
| + getInstanceId(), requestId, 'default', '', function(allowed) { |
| + if (allowed) { |
| + return; |
| + } |
| + showWarningMessage(event.messageType); |
| + }); |
| + }); |
| + } else { |
| + actionTaken = true; |
| + // The default action is equivalent to canceling the dialog. |
| + WebView.setPermission( |
| + getInstanceId(), requestId, 'default', '', function(allowed) { |
| + if (allowed) { |
| + return; |
| + } |
| + showWarningMessage(event.messageType); |
| + }); |
| + } |
| +}; |
| + |
| +WebViewEvents.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.webViewInternal.dispatchEvent(webViewEvent)) { |
| + showWarningMessage(event.reason); |
| + } |
| +}; |
| + |
| +WebViewEvents.prototype.handleLoadCommitEvent = function(event, webViewEvent) { |
| + this.webViewInternal.onLoadCommit(event.currentEntryIndex, event.entryCount, |
| + event.processId, event.url, |
| + event.isTopLevel); |
| + this.webViewInternal.dispatchEvent(webViewEvent); |
| +}; |
| + |
| +WebViewEvents.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 requestId = event.requestId; |
| + var actionTaken = false; |
| + var self = this; |
| + var getInstanceId = function() { |
| + return self.webViewInternal.getInstanceId(); |
| + }; |
| + |
| + var validateCall = function () { |
| + if (actionTaken) { |
| + throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN); |
| + } |
| + actionTaken = true; |
| + }; |
| + |
| + var windowObj = { |
| + attach: function(webview) { |
| + validateCall(); |
| + if (!webview || !webview.tagName || webview.tagName != '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 webViewInternal = privates(webview).internal; |
| + if (event.storagePartitionId) { |
| + webViewInternal.onAttach(event.storagePartitionId); |
| + } |
| + |
| + var attached = |
| + webViewInternal.attachWindowAndSetUpEvents( |
| + event.windowId, undefined, event.storagePartitionId); |
| + |
| + 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( |
| + getInstanceId(), requestId, attached ? 'allow' : 'deny'); |
| + }, 0); |
| + }, |
| + discard: function() { |
| + validateCall(); |
| + WebView.setPermission(getInstanceId(), requestId, 'deny'); |
| + } |
| + }; |
| + webViewEvent.window = windowObj; |
| + |
| + var defaultPrevented = !self.webViewInternal.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( |
| + getInstanceId(), requestId, 'default', '', function(allowed) { |
| + if (allowed) { |
| + return; |
| + } |
| + showWarningMessage(); |
| + }); |
| + }); |
| + } else { |
| + actionTaken = true; |
| + // The default action is to discard the window. |
| + WebView.setPermission( |
| + getInstanceId(), requestId, 'default', '', function(allowed) { |
| + if (allowed) { |
| + return; |
| + } |
| + showWarningMessage(); |
| + }); |
| + } |
| +}; |
| + |
| +WebViewEvents.prototype.getPermissionTypes = function() { |
| + var permissions = |
| + ['media', |
| + 'geolocation', |
| + 'pointerLock', |
| + 'download', |
| + 'loadplugin', |
| + 'filesystem']; |
| + return permissions.concat( |
| + this.webViewInternal.maybeGetExperimentalPermissions()); |
| +}; |
| + |
| +WebViewEvents.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 getInstanceId = function() { |
| + return self.webViewInternal.getInstanceId(); |
| + }; |
| + |
| + if (this.getPermissionTypes().indexOf(event.permission) < 0) { |
| + // The permission type is not allowed. Trigger the default response. |
| + WebView.setPermission( |
| + getInstanceId(), requestId, 'default', '', function(allowed) { |
| + if (allowed) { |
| + return; |
| + } |
| + showWarningMessage(event.permission); |
| + }); |
| + return; |
| + } |
| + |
| + 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(getInstanceId(), requestId, 'allow'); |
| + }, |
| + deny: function() { |
| + validateCall(); |
| + WebView.setPermission(getInstanceId(), requestId, 'deny'); |
| + } |
| + }; |
| + webViewEvent.request = request; |
| + |
| + var defaultPrevented = !self.webViewInternal.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( |
| + getInstanceId(), requestId, 'default', '', function(allowed) { |
| + if (allowed) { |
| + return; |
| + } |
| + showWarningMessage(event.permission); |
| + }); |
| + }); |
| + } else { |
| + decisionMade = true; |
| + WebView.setPermission( |
| + getInstanceId(), requestId, 'default', '', function(allowed) { |
| + if (allowed) { |
| + return; |
| + } |
| + showWarningMessage(event.permission); |
| + }); |
| + } |
| +}; |
| + |
| +WebViewEvents.prototype.handleSizeChangedEvent = function( |
| + event, webViewEvent) { |
| + this.webViewInternal.onSizeChanged(webViewEvent.newWidth, |
| + webViewEvent.newHeight); |
| + this.webViewInternal.dispatchEvent(webViewEvent); |
| +}; |
| + |
| +exports.WebViewEvents = WebViewEvents; |
| +exports.CreateEvent = CreateEvent; |