Rename webview API related files to web_view_internal.
[chromium-blink-merge.git] / chrome / renderer / resources / extensions / web_view_events.js
blob3729cff83d4a920e6536b02eeffbd671091694e7
1 // Copyright 2014 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.
5 // Event management for WebViewInternal.
7 var DeclarativeWebRequestSchema =
8     requireNative('schema_registry').GetSchema('declarativeWebRequest');
9 var EventBindings = require('event_bindings');
10 var IdGenerator = requireNative('id_generator');
11 var MessagingNatives = requireNative('messaging_natives');
12 var WebRequestEvent = require('webRequestInternal').WebRequestEvent;
13 var WebRequestSchema =
14     requireNative('schema_registry').GetSchema('webRequest');
15 var WebView = require('webViewInternal').WebView;
17 var CreateEvent = function(name) {
18   var eventOpts = {supportsListeners: true, supportsFilters: true};
19   return new EventBindings.Event(name, undefined, eventOpts);
22 var FrameNameChangedEvent = CreateEvent('webViewInternal.onFrameNameChanged');
23 var WebRequestMessageEvent = CreateEvent('webViewInternal.onMessage');
25 // WEB_VIEW_EVENTS is a map of stable <webview> DOM event names to their
26 //     associated extension event descriptor objects.
27 // An event listener will be attached to the extension event |evt| specified in
28 //     the descriptor.
29 // |fields| specifies the public-facing fields in the DOM event that are
30 //     accessible to <webview> developers.
31 // |customHandler| allows a handler function to be called each time an extension
32 //     event is caught by its event listener. The DOM event should be dispatched
33 //     within this handler function. With no handler function, the DOM event
34 //     will be dispatched by default each time the extension event is caught.
35 // |cancelable| (default: false) specifies whether the event's default
36 //     behavior can be canceled. If the default action associated with the event
37 //     is prevented, then its dispatch function will return false in its event
38 //     handler. The event must have a custom handler for this to be meaningful.
39 var WEB_VIEW_EVENTS = {
40   'close': {
41     evt: CreateEvent('webViewInternal.onClose'),
42     fields: []
43   },
44   'consolemessage': {
45     evt: CreateEvent('webViewInternal.onConsoleMessage'),
46     fields: ['level', 'message', 'line', 'sourceId']
47   },
48   'contentload': {
49     evt: CreateEvent('webViewInternal.onContentLoad'),
50     fields: []
51   },
52   'contextmenu': {
53     evt: CreateEvent('webViewInternal.contextmenu'),
54     cancelable: true,
55     customHandler: function(handler, event, webViewEvent) {
56       handler.handleContextMenu(event, webViewEvent);
57     },
58     fields: ['items']
59   },
60   'dialog': {
61     cancelable: true,
62     customHandler: function(handler, event, webViewEvent) {
63       handler.handleDialogEvent(event, webViewEvent);
64     },
65     evt: CreateEvent('webViewInternal.onDialog'),
66     fields: ['defaultPromptText', 'messageText', 'messageType', 'url']
67   },
68   'exit': {
69      evt: CreateEvent('webViewInternal.onExit'),
70      fields: ['processId', 'reason']
71   },
72   'loadabort': {
73     cancelable: true,
74     customHandler: function(handler, event, webViewEvent) {
75       handler.handleLoadAbortEvent(event, webViewEvent);
76     },
77     evt: CreateEvent('webViewInternal.onLoadAbort'),
78     fields: ['url', 'isTopLevel', 'reason']
79   },
80   'loadcommit': {
81     customHandler: function(handler, event, webViewEvent) {
82       handler.handleLoadCommitEvent(event, webViewEvent);
83     },
84     evt: CreateEvent('webViewInternal.onLoadCommit'),
85     fields: ['url', 'isTopLevel']
86   },
87   'loadprogress': {
88     evt: CreateEvent('webViewInternal.onLoadProgress'),
89     fields: ['url', 'progress']
90   },
91   'loadredirect': {
92     evt: CreateEvent('webViewInternal.onLoadRedirect'),
93     fields: ['isTopLevel', 'oldUrl', 'newUrl']
94   },
95   'loadstart': {
96     evt: CreateEvent('webViewInternal.onLoadStart'),
97     fields: ['url', 'isTopLevel']
98   },
99   'loadstop': {
100     evt: CreateEvent('webViewInternal.onLoadStop'),
101     fields: []
102   },
103   'newwindow': {
104     cancelable: true,
105     customHandler: function(handler, event, webViewEvent) {
106       handler.handleNewWindowEvent(event, webViewEvent);
107     },
108     evt: CreateEvent('webViewInternal.onNewWindow'),
109     fields: [
110       'initialHeight',
111       'initialWidth',
112       'targetUrl',
113       'windowOpenDisposition',
114       'name'
115     ]
116   },
117   'permissionrequest': {
118     cancelable: true,
119     customHandler: function(handler, event, webViewEvent) {
120       handler.handlePermissionEvent(event, webViewEvent);
121     },
122     evt: CreateEvent('webViewInternal.onPermissionRequest'),
123     fields: [
124       'identifier',
125       'lastUnlockedBySelf',
126       'name',
127       'permission',
128       'requestMethod',
129       'url',
130       'userGesture'
131     ]
132   },
133   'responsive': {
134     evt: CreateEvent('webViewInternal.onResponsive'),
135     fields: ['processId']
136   },
137   'sizechanged': {
138     evt: CreateEvent('webViewInternal.onSizeChanged'),
139     customHandler: function(handler, event, webViewEvent) {
140       handler.handleSizeChangedEvent(event, webViewEvent);
141     },
142     fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth']
143   },
144   'unresponsive': {
145     evt: CreateEvent('webViewInternal.onUnresponsive'),
146     fields: ['processId']
147   }
150 function DeclarativeWebRequestEvent(opt_eventName,
151                                     opt_argSchemas,
152                                     opt_eventOptions,
153                                     opt_webViewInstanceId) {
154   var subEventName = opt_eventName + '/' + IdGenerator.GetNextId();
155   EventBindings.Event.call(this, subEventName, opt_argSchemas, opt_eventOptions,
156       opt_webViewInstanceId);
158   var self = this;
159   // TODO(lazyboy): When do we dispose this listener?
160   WebRequestMessageEvent.addListener(function() {
161     // Re-dispatch to subEvent's listeners.
162     $Function.apply(self.dispatch, self, $Array.slice(arguments));
163   }, {instanceId: opt_webViewInstanceId || 0});
166 DeclarativeWebRequestEvent.prototype = {
167   __proto__: EventBindings.Event.prototype
170 // Constructor.
171 function WebViewEvents(webViewInternal, viewInstanceId) {
172   this.webViewInternal = webViewInternal;
173   this.viewInstanceId = viewInstanceId;
174   this.setup();
177 // Sets up events.
178 WebViewEvents.prototype.setup = function() {
179   this.setupFrameNameChangedEvent();
180   this.setupWebRequestEvents();
181   this.webViewInternal.setupExperimentalContextMenus();
183   var events = this.getEvents();
184   for (var eventName in events) {
185     this.setupEvent(eventName, events[eventName]);
186   }
189 WebViewEvents.prototype.setupFrameNameChangedEvent = function() {
190   var self = this;
191   FrameNameChangedEvent.addListener(function(e) {
192     self.webViewInternal.onFrameNameChanged(e.name);
193   }, {instanceId: self.viewInstanceId});
196 WebViewEvents.prototype.setupWebRequestEvents = function() {
197   var self = this;
198   var request = {};
199   var createWebRequestEvent = function(webRequestEvent) {
200     return function() {
201       if (!self[webRequestEvent.name]) {
202         self[webRequestEvent.name] =
203             new WebRequestEvent(
204                 'webViewInternal.' + webRequestEvent.name,
205                 webRequestEvent.parameters,
206                 webRequestEvent.extraParameters, webRequestEvent.options,
207                 self.viewInstanceId);
208       }
209       return self[webRequestEvent.name];
210     };
211   };
213   var createDeclarativeWebRequestEvent = function(webRequestEvent) {
214     return function() {
215       if (!self[webRequestEvent.name]) {
216         // The onMessage event gets a special event type because we want
217         // the listener to fire only for messages targeted for this particular
218         // <webview>.
219         var EventClass = webRequestEvent.name === 'onMessage' ?
220             DeclarativeWebRequestEvent : EventBindings.Event;
221         self[webRequestEvent.name] =
222             new EventClass(
223                 'webViewInternal.' + webRequestEvent.name,
224                 webRequestEvent.parameters,
225                 webRequestEvent.options,
226                 self.viewInstanceId);
227       }
228       return self[webRequestEvent.name];
229     };
230   };
232   for (var i = 0; i < DeclarativeWebRequestSchema.events.length; ++i) {
233     var eventSchema = DeclarativeWebRequestSchema.events[i];
234     var webRequestEvent = createDeclarativeWebRequestEvent(eventSchema);
235     Object.defineProperty(
236         request,
237         eventSchema.name,
238         {
239           get: webRequestEvent,
240           enumerable: true
241         }
242     );
243   }
245   // Populate the WebRequest events from the API definition.
246   for (var i = 0; i < WebRequestSchema.events.length; ++i) {
247     var webRequestEvent = createWebRequestEvent(WebRequestSchema.events[i]);
248     Object.defineProperty(
249         request,
250         WebRequestSchema.events[i].name,
251         {
252           get: webRequestEvent,
253           enumerable: true
254         }
255     );
256   }
258   this.webViewInternal.setRequestPropertyOnWebViewNode(request);
261 WebViewEvents.prototype.getEvents = function() {
262   var experimentalEvents = this.webViewInternal.maybeGetExperimentalEvents();
263   for (var eventName in experimentalEvents) {
264     WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName];
265   }
266   return WEB_VIEW_EVENTS;
269 WebViewEvents.prototype.setupEvent = function(name, info) {
270   var self = this;
271   info.evt.addListener(function(e) {
272     var details = {bubbles:true};
273     if (info.cancelable)
274       details.cancelable = true;
275     var webViewEvent = new Event(name, details);
276     $Array.forEach(info.fields, function(field) {
277       if (e[field] !== undefined) {
278         webViewEvent[field] = e[field];
279       }
280     });
281     if (info.customHandler) {
282       info.customHandler(self, e, webViewEvent);
283       return;
284     }
285     self.webViewInternal.dispatchEvent(webViewEvent);
286   }, {instanceId: self.viewInstanceId});
288   this.webViewInternal.setupEventProperty(name);
292 // Event handlers.
293 WebViewEvents.prototype.handleContextMenu = function(e, webViewEvent) {
294   this.webViewInternal.maybeHandleContextMenu(e, webViewEvent);
297 WebViewEvents.prototype.handleDialogEvent = function(event, webViewEvent) {
298   var showWarningMessage = function(dialogType) {
299     var VOWELS = ['a', 'e', 'i', 'o', 'u'];
300     var WARNING_MSG_DIALOG_BLOCKED = '<webview>: %1 %2 dialog was blocked.';
301     var article = (VOWELS.indexOf(dialogType.charAt(0)) >= 0) ? 'An' : 'A';
302     var output = WARNING_MSG_DIALOG_BLOCKED.replace('%1', article);
303     output = output.replace('%2', dialogType);
304     window.console.warn(output);
305   };
307   var self = this;
308   var requestId = event.requestId;
309   var actionTaken = false;
311   var validateCall = function() {
312     var ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN = '<webview>: ' +
313         'An action has already been taken for this "dialog" event.';
315     if (actionTaken) {
316       throw new Error(ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN);
317     }
318     actionTaken = true;
319   };
321   var getInstanceId = function() {
322     return self.webViewInternal.getInstanceId();
323   };
325   var dialog = {
326     ok: function(user_input) {
327       validateCall();
328       user_input = user_input || '';
329       WebView.setPermission(getInstanceId(), requestId, 'allow', user_input);
330     },
331     cancel: function() {
332       validateCall();
333       WebView.setPermission(getInstanceId(), requestId, 'deny');
334     }
335   };
336   webViewEvent.dialog = dialog;
338   var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent);
339   if (actionTaken) {
340     return;
341   }
343   if (defaultPrevented) {
344     // Tell the JavaScript garbage collector to track lifetime of |dialog| and
345     // call back when the dialog object has been collected.
346     MessagingNatives.BindToGC(dialog, function() {
347       // Avoid showing a warning message if the decision has already been made.
348       if (actionTaken) {
349         return;
350       }
351       WebView.setPermission(
352           getInstanceId(), requestId, 'default', '', function(allowed) {
353         if (allowed) {
354           return;
355         }
356         showWarningMessage(event.messageType);
357       });
358     });
359   } else {
360     actionTaken = true;
361     // The default action is equivalent to canceling the dialog.
362     WebView.setPermission(
363         getInstanceId(), requestId, 'default', '', function(allowed) {
364       if (allowed) {
365         return;
366       }
367       showWarningMessage(event.messageType);
368     });
369   }
372 WebViewEvents.prototype.handleLoadAbortEvent = function(event, webViewEvent) {
373   var showWarningMessage = function(reason) {
374     var WARNING_MSG_LOAD_ABORTED = '<webview>: ' +
375         'The load has aborted with reason "%1".';
376     window.console.warn(WARNING_MSG_LOAD_ABORTED.replace('%1', reason));
377   };
378   if (this.webViewInternal.dispatchEvent(webViewEvent)) {
379     showWarningMessage(event.reason);
380   }
383 WebViewEvents.prototype.handleLoadCommitEvent = function(event, webViewEvent) {
384   this.webViewInternal.onLoadCommit(event.currentEntryIndex, event.entryCount,
385                                     event.processId, event.url,
386                                     event.isTopLevel);
387   this.webViewInternal.dispatchEvent(webViewEvent);
390 WebViewEvents.prototype.handleNewWindowEvent = function(event, webViewEvent) {
391   var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' +
392       'An action has already been taken for this "newwindow" event.';
394   var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' +
395       'Unable to attach the new window to the provided webViewInternal.';
397   var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.';
399   var showWarningMessage = function() {
400     var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.';
401     window.console.warn(WARNING_MSG_NEWWINDOW_BLOCKED);
402   };
404   var requestId = event.requestId;
405   var actionTaken = false;
406   var self = this;
407   var getInstanceId = function() {
408     return self.webViewInternal.getInstanceId();
409   };
411   var validateCall = function () {
412     if (actionTaken) {
413       throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN);
414     }
415     actionTaken = true;
416   };
418   var windowObj = {
419     attach: function(webview) {
420       validateCall();
421       if (!webview || !webview.tagName || webview.tagName != 'WEBVIEW')
422         throw new Error(ERROR_MSG_WEBVIEW_EXPECTED);
423       // Attach happens asynchronously to give the tagWatcher an opportunity
424       // to pick up the new webview before attach operates on it, if it hasn't
425       // been attached to the DOM already.
426       // Note: Any subsequent errors cannot be exceptions because they happen
427       // asynchronously.
428       setTimeout(function() {
429         var webViewInternal = privates(webview).internal;
430         if (event.storagePartitionId) {
431           webViewInternal.onAttach(event.storagePartitionId);
432         }
434         var attached =
435             webViewInternal.attachWindowAndSetUpEvents(
436                 event.windowId, undefined, event.storagePartitionId);
438         if (!attached) {
439           window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH);
440         }
441         // If the object being passed into attach is not a valid <webview>
442         // then we will fail and it will be treated as if the new window
443         // was rejected. The permission API plumbing is used here to clean
444         // up the state created for the new window if attaching fails.
445         WebView.setPermission(
446             getInstanceId(), requestId, attached ? 'allow' : 'deny');
447       }, 0);
448     },
449     discard: function() {
450       validateCall();
451       WebView.setPermission(getInstanceId(), requestId, 'deny');
452     }
453   };
454   webViewEvent.window = windowObj;
456   var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent);
457   if (actionTaken) {
458     return;
459   }
461   if (defaultPrevented) {
462     // Make browser plugin track lifetime of |windowObj|.
463     MessagingNatives.BindToGC(windowObj, function() {
464       // Avoid showing a warning message if the decision has already been made.
465       if (actionTaken) {
466         return;
467       }
468       WebView.setPermission(
469           getInstanceId(), requestId, 'default', '', function(allowed) {
470         if (allowed) {
471           return;
472         }
473         showWarningMessage();
474       });
475     });
476   } else {
477     actionTaken = true;
478     // The default action is to discard the window.
479     WebView.setPermission(
480         getInstanceId(), requestId, 'default', '', function(allowed) {
481       if (allowed) {
482         return;
483       }
484       showWarningMessage();
485     });
486   }
489 WebViewEvents.prototype.getPermissionTypes = function() {
490   var permissions =
491       ['media',
492       'geolocation',
493       'pointerLock',
494       'download',
495       'loadplugin',
496       'filesystem'];
497   return permissions.concat(
498       this.webViewInternal.maybeGetExperimentalPermissions());
501 WebViewEvents.prototype.handlePermissionEvent =
502     function(event, webViewEvent) {
503   var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' +
504       'Permission has already been decided for this "permissionrequest" event.';
506   var showWarningMessage = function(permission) {
507     var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' +
508         'The permission request for "%1" has been denied.';
509     window.console.warn(
510         WARNING_MSG_PERMISSION_DENIED.replace('%1', permission));
511   };
513   var requestId = event.requestId;
514   var self = this;
515   var getInstanceId = function() {
516     return self.webViewInternal.getInstanceId();
517   };
519   if (this.getPermissionTypes().indexOf(event.permission) < 0) {
520     // The permission type is not allowed. Trigger the default response.
521     WebView.setPermission(
522         getInstanceId(), requestId, 'default', '', function(allowed) {
523       if (allowed) {
524         return;
525       }
526       showWarningMessage(event.permission);
527     });
528     return;
529   }
531   var decisionMade = false;
532   var validateCall = function() {
533     if (decisionMade) {
534       throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED);
535     }
536     decisionMade = true;
537   };
539   // Construct the event.request object.
540   var request = {
541     allow: function() {
542       validateCall();
543       WebView.setPermission(getInstanceId(), requestId, 'allow');
544     },
545     deny: function() {
546       validateCall();
547       WebView.setPermission(getInstanceId(), requestId, 'deny');
548     }
549   };
550   webViewEvent.request = request;
552   var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent);
553   if (decisionMade) {
554     return;
555   }
557   if (defaultPrevented) {
558     // Make browser plugin track lifetime of |request|.
559     MessagingNatives.BindToGC(request, function() {
560       // Avoid showing a warning message if the decision has already been made.
561       if (decisionMade) {
562         return;
563       }
564       WebView.setPermission(
565           getInstanceId(), requestId, 'default', '', function(allowed) {
566         if (allowed) {
567           return;
568         }
569         showWarningMessage(event.permission);
570       });
571     });
572   } else {
573     decisionMade = true;
574     WebView.setPermission(
575         getInstanceId(), requestId, 'default', '', function(allowed) {
576       if (allowed) {
577         return;
578       }
579       showWarningMessage(event.permission);
580     });
581   }
584 WebViewEvents.prototype.handleSizeChangedEvent = function(
585     event, webViewEvent) {
586   this.webViewInternal.onSizeChanged(webViewEvent.newWidth,
587                                      webViewEvent.newHeight);
588   this.webViewInternal.dispatchEvent(webViewEvent);
591 exports.WebViewEvents = WebViewEvents;
592 exports.CreateEvent = CreateEvent;