| // 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 EventBindings = require('event_bindings'); |
| var MessagingNatives = requireNative('messaging_natives'); |
| var WebView = require('webViewInternal').WebView; |
| |
| var CreateEvent = function(name) { |
| var eventOpts = {supportsListeners: true, supportsFilters: true}; |
| return new EventBindings.Event(name, undefined, eventOpts); |
| }; |
| |
| var FrameNameChangedEvent = CreateEvent('webViewInternal.onFrameNameChanged'); |
| var PluginDestroyedEvent = CreateEvent('webViewInternal.onPluginDestroyed'); |
| |
| // WEB_VIEW_EVENTS is a map of stable <webview> DOM event names to their |
| // 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 WEB_VIEW_EVENTS = { |
| 'close': { |
| evt: CreateEvent('webViewInternal.onClose'), |
| fields: [] |
| }, |
| 'consolemessage': { |
| evt: CreateEvent('webViewInternal.onConsoleMessage'), |
| fields: ['level', 'message', 'line', 'sourceId'] |
| }, |
| 'contentload': { |
| evt: CreateEvent('webViewInternal.onContentLoad'), |
| fields: [] |
| }, |
| 'dialog': { |
| cancelable: true, |
| customHandler: function(handler, event, webViewEvent) { |
| handler.handleDialogEvent(event, webViewEvent); |
| }, |
| evt: CreateEvent('webViewInternal.onDialog'), |
| fields: ['defaultPromptText', 'messageText', 'messageType', 'url'] |
| }, |
| 'exit': { |
| evt: CreateEvent('webViewInternal.onExit'), |
| fields: ['processId', 'reason'] |
| }, |
| 'findupdate': { |
| evt: CreateEvent('webViewInternal.onFindReply'), |
| fields: [ |
| 'searchText', |
| 'numberOfMatches', |
| 'activeMatchOrdinal', |
| 'selectionRect', |
| 'canceled', |
| 'finalUpdate' |
| ] |
| }, |
| 'loadabort': { |
| cancelable: true, |
| customHandler: function(handler, event, webViewEvent) { |
| handler.handleLoadAbortEvent(event, webViewEvent); |
| }, |
| evt: CreateEvent('webViewInternal.onLoadAbort'), |
| fields: ['url', 'isTopLevel', 'reason'] |
| }, |
| 'loadcommit': { |
| customHandler: function(handler, event, webViewEvent) { |
| handler.handleLoadCommitEvent(event, webViewEvent); |
| }, |
| evt: CreateEvent('webViewInternal.onLoadCommit'), |
| fields: ['url', 'isTopLevel'] |
| }, |
| 'loadprogress': { |
| evt: CreateEvent('webViewInternal.onLoadProgress'), |
| fields: ['url', 'progress'] |
| }, |
| 'loadredirect': { |
| evt: CreateEvent('webViewInternal.onLoadRedirect'), |
| fields: ['isTopLevel', 'oldUrl', 'newUrl'] |
| }, |
| 'loadstart': { |
| evt: CreateEvent('webViewInternal.onLoadStart'), |
| fields: ['url', 'isTopLevel'] |
| }, |
| 'loadstop': { |
| evt: CreateEvent('webViewInternal.onLoadStop'), |
| fields: [] |
| }, |
| 'newwindow': { |
| cancelable: true, |
| customHandler: function(handler, event, webViewEvent) { |
| handler.handleNewWindowEvent(event, webViewEvent); |
| }, |
| evt: CreateEvent('webViewInternal.onNewWindow'), |
| fields: [ |
| 'initialHeight', |
| 'initialWidth', |
| 'targetUrl', |
| 'windowOpenDisposition', |
| 'name' |
| ] |
| }, |
| 'permissionrequest': { |
| cancelable: true, |
| customHandler: function(handler, event, webViewEvent) { |
| handler.handlePermissionEvent(event, webViewEvent); |
| }, |
| evt: CreateEvent('webViewInternal.onPermissionRequest'), |
| fields: [ |
| 'identifier', |
| 'lastUnlockedBySelf', |
| 'name', |
| 'permission', |
| 'requestMethod', |
| 'url', |
| 'userGesture' |
| ] |
| }, |
| 'responsive': { |
| evt: CreateEvent('webViewInternal.onResponsive'), |
| fields: ['processId'] |
| }, |
| 'sizechanged': { |
| evt: CreateEvent('webViewInternal.onSizeChanged'), |
| customHandler: function(handler, event, webViewEvent) { |
| handler.handleSizeChangedEvent(event, webViewEvent); |
| }, |
| fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth'] |
| }, |
| 'unresponsive': { |
| evt: CreateEvent('webViewInternal.onUnresponsive'), |
| fields: ['processId'] |
| }, |
| 'zoomchange': { |
| evt: CreateEvent('webViewInternal.onZoomChange'), |
| fields: ['oldZoomFactor', 'newZoomFactor'] |
| } |
| }; |
| |
| // Constructor. |
| function WebViewEvents(webViewInternal, viewInstanceId) { |
| this.webViewInternal = webViewInternal; |
| this.viewInstanceId = viewInstanceId; |
| this.setup(); |
| } |
| |
| // Sets up events. |
| WebViewEvents.prototype.setup = function() { |
| this.setupFrameNameChangedEvent(); |
| this.setupPluginDestroyedEvent(); |
| this.webViewInternal.maybeSetupChromeWebViewEvents(); |
| this.webViewInternal.setupExperimentalContextMenus(); |
| |
| var events = this.getEvents(); |
| for (var eventName in events) { |
| this.setupEvent(eventName, events[eventName]); |
| } |
| }; |
| |
| WebViewEvents.prototype.setupFrameNameChangedEvent = function() { |
| FrameNameChangedEvent.addListener(function(e) { |
| this.webViewInternal.onFrameNameChanged(e.name); |
| }.bind(this), {instanceId: this.viewInstanceId}); |
| }; |
| |
| WebViewEvents.prototype.setupPluginDestroyedEvent = function() { |
| PluginDestroyedEvent.addListener(function(e) { |
| this.webViewInternal.onPluginDestroyed(); |
| }.bind(this), {instanceId: this.viewInstanceId}); |
| }; |
| |
| WebViewEvents.prototype.getEvents = function() { |
| var experimentalEvents = this.webViewInternal.maybeGetExperimentalEvents(); |
| for (var eventName in experimentalEvents) { |
| WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName]; |
| } |
| var chromeEvents = this.webViewInternal.maybeGetChromeWebViewEvents(); |
| for (var eventName in chromeEvents) { |
| WEB_VIEW_EVENTS[eventName] = chromeEvents[eventName]; |
| } |
| return WEB_VIEW_EVENTS; |
| }; |
| |
| WebViewEvents.prototype.setupEvent = function(name, info) { |
| 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]; |
| } |
| }.bind(this)); |
| if (info.customHandler) { |
| info.customHandler(this, e, webViewEvent); |
| return; |
| } |
| this.webViewInternal.dispatchEvent(webViewEvent); |
| }.bind(this), {instanceId: this.viewInstanceId}); |
| |
| this.webViewInternal.setupEventProperty(name); |
| }; |
| |
| |
| 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 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 getGuestInstanceId = function() { |
| return this.webViewInternal.getGuestInstanceId(); |
| }.bind(this); |
| |
| var dialog = { |
| ok: function(user_input) { |
| validateCall(); |
| user_input = user_input || ''; |
| WebView.setPermission(getGuestInstanceId(), requestId, 'allow', |
| user_input); |
| }, |
| cancel: function() { |
| validateCall(); |
| WebView.setPermission(getGuestInstanceId(), requestId, 'deny'); |
| } |
| }; |
| webViewEvent.dialog = dialog; |
| |
| var defaultPrevented = !this.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( |
| getGuestInstanceId(), requestId, 'default', '', function(allowed) { |
| if (allowed) { |
| return; |
| } |
| showWarningMessage(event.messageType); |
| }); |
| }); |
| } else { |
| actionTaken = true; |
| // The default action is equivalent to canceling the dialog. |
| WebView.setPermission( |
| getGuestInstanceId(), 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.baseUrlForDataUrl, |
| 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 webViewInternal.'; |
| |
| 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 getGuestInstanceId = function() { |
| return this.webViewInternal.getGuestInstanceId(); |
| }.bind(this); |
| |
| 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; |
| // Update the partition. |
| if (event.storagePartitionId) { |
| webViewInternal.onAttach(event.storagePartitionId); |
| } |
| |
| var attached = webViewInternal.attachWindow(event.windowId, true); |
| |
| if (!attached) { |
| window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH); |
| } |
| |
| var guestInstanceId = getGuestInstanceId(); |
| if (!guestInstanceId) { |
| // If the opener is already gone, then we won't have its |
| // guestInstanceId. |
| return; |
| } |
| |
| // 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( |
| guestInstanceId, requestId, attached ? 'allow' : 'deny'); |
| }, 0); |
| }, |
| discard: function() { |
| validateCall(); |
| var guestInstanceId = getGuestInstanceId(); |
| if (!guestInstanceId) { |
| // If the opener is already gone, then we won't have its |
| // guestInstanceId. |
| return; |
| } |
| WebView.setPermission(guestInstanceId, requestId, 'deny'); |
| } |
| }; |
| webViewEvent.window = windowObj; |
| |
| var defaultPrevented = !this.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; |
| } |
| |
| var guestInstanceId = getGuestInstanceId(); |
| if (!guestInstanceId) { |
| // If the opener is already gone, then we won't have its |
| // guestInstanceId. |
| return; |
| } |
| |
| WebView.setPermission( |
| guestInstanceId, requestId, 'default', '', function(allowed) { |
| if (allowed) { |
| return; |
| } |
| showWarningMessage(); |
| }); |
| }); |
| } else { |
| actionTaken = true; |
| // The default action is to discard the window. |
| WebView.setPermission( |
| getGuestInstanceId(), 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 getGuestInstanceId = function() { |
| return this.webViewInternal.getGuestInstanceId(); |
| }.bind(this); |
| |
| if (this.getPermissionTypes().indexOf(event.permission) < 0) { |
| // The permission type is not allowed. Trigger the default response. |
| WebView.setPermission( |
| getGuestInstanceId(), 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(getGuestInstanceId(), requestId, 'allow'); |
| }, |
| deny: function() { |
| validateCall(); |
| WebView.setPermission(getGuestInstanceId(), requestId, 'deny'); |
| } |
| }; |
| webViewEvent.request = request; |
| |
| var defaultPrevented = !this.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( |
| getGuestInstanceId(), requestId, 'default', '', function(allowed) { |
| if (allowed) { |
| return; |
| } |
| showWarningMessage(event.permission); |
| }); |
| }); |
| } else { |
| decisionMade = true; |
| WebView.setPermission( |
| getGuestInstanceId(), requestId, 'default', '', |
| function(allowed) { |
| if (allowed) { |
| return; |
| } |
| showWarningMessage(event.permission); |
| }); |
| } |
| }; |
| |
| WebViewEvents.prototype.handleSizeChangedEvent = function( |
| event, webViewEvent) { |
| this.webViewInternal.onSizeChanged(webViewEvent); |
| }; |
| |
| exports.WebViewEvents = WebViewEvents; |
| exports.CreateEvent = CreateEvent; |