// 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;