Upstream version 11.39.266.0
[platform/framework/web/crosswalk.git] / src / extensions / renderer / resources / web_view_events.js
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.
4
5 // Event management for WebViewInternal.
6
7 var EventBindings = require('event_bindings');
8 var MessagingNatives = requireNative('messaging_natives');
9 var WebView = require('webViewInternal').WebView;
10
11 var CreateEvent = function(name) {
12   var eventOpts = {supportsListeners: true, supportsFilters: true};
13   return new EventBindings.Event(name, undefined, eventOpts);
14 };
15
16 var FrameNameChangedEvent = CreateEvent('webViewInternal.onFrameNameChanged');
17 var PluginDestroyedEvent = CreateEvent('webViewInternal.onPluginDestroyed');
18
19 // WEB_VIEW_EVENTS is a map of stable <webview> DOM event names to their
20 //     associated extension event descriptor objects.
21 // An event listener will be attached to the extension event |evt| specified in
22 //     the descriptor.
23 // |fields| specifies the public-facing fields in the DOM event that are
24 //     accessible to <webview> developers.
25 // |customHandler| allows a handler function to be called each time an extension
26 //     event is caught by its event listener. The DOM event should be dispatched
27 //     within this handler function. With no handler function, the DOM event
28 //     will be dispatched by default each time the extension event is caught.
29 // |cancelable| (default: false) specifies whether the event's default
30 //     behavior can be canceled. If the default action associated with the event
31 //     is prevented, then its dispatch function will return false in its event
32 //     handler. The event must have a custom handler for this to be meaningful.
33 var WEB_VIEW_EVENTS = {
34   'close': {
35     evt: CreateEvent('webViewInternal.onClose'),
36     fields: []
37   },
38   'consolemessage': {
39     evt: CreateEvent('webViewInternal.onConsoleMessage'),
40     fields: ['level', 'message', 'line', 'sourceId']
41   },
42   'contentload': {
43     evt: CreateEvent('webViewInternal.onContentLoad'),
44     fields: []
45   },
46   'dialog': {
47     cancelable: true,
48     customHandler: function(handler, event, webViewEvent) {
49       handler.handleDialogEvent(event, webViewEvent);
50     },
51     evt: CreateEvent('webViewInternal.onDialog'),
52     fields: ['defaultPromptText', 'messageText', 'messageType', 'url']
53   },
54   'exit': {
55      evt: CreateEvent('webViewInternal.onExit'),
56      fields: ['processId', 'reason']
57   },
58   'findupdate': {
59     evt: CreateEvent('webViewInternal.onFindReply'),
60     fields: [
61       'searchText',
62       'numberOfMatches',
63       'activeMatchOrdinal',
64       'selectionRect',
65       'canceled',
66       'finalUpdate'
67     ]
68   },
69   'loadabort': {
70     cancelable: true,
71     customHandler: function(handler, event, webViewEvent) {
72       handler.handleLoadAbortEvent(event, webViewEvent);
73     },
74     evt: CreateEvent('webViewInternal.onLoadAbort'),
75     fields: ['url', 'isTopLevel', 'reason']
76   },
77   'loadcommit': {
78     customHandler: function(handler, event, webViewEvent) {
79       handler.handleLoadCommitEvent(event, webViewEvent);
80     },
81     evt: CreateEvent('webViewInternal.onLoadCommit'),
82     fields: ['url', 'isTopLevel']
83   },
84   'loadprogress': {
85     evt: CreateEvent('webViewInternal.onLoadProgress'),
86     fields: ['url', 'progress']
87   },
88   'loadredirect': {
89     evt: CreateEvent('webViewInternal.onLoadRedirect'),
90     fields: ['isTopLevel', 'oldUrl', 'newUrl']
91   },
92   'loadstart': {
93     evt: CreateEvent('webViewInternal.onLoadStart'),
94     fields: ['url', 'isTopLevel']
95   },
96   'loadstop': {
97     evt: CreateEvent('webViewInternal.onLoadStop'),
98     fields: []
99   },
100   'newwindow': {
101     cancelable: true,
102     customHandler: function(handler, event, webViewEvent) {
103       handler.handleNewWindowEvent(event, webViewEvent);
104     },
105     evt: CreateEvent('webViewInternal.onNewWindow'),
106     fields: [
107       'initialHeight',
108       'initialWidth',
109       'targetUrl',
110       'windowOpenDisposition',
111       'name'
112     ]
113   },
114   'permissionrequest': {
115     cancelable: true,
116     customHandler: function(handler, event, webViewEvent) {
117       handler.handlePermissionEvent(event, webViewEvent);
118     },
119     evt: CreateEvent('webViewInternal.onPermissionRequest'),
120     fields: [
121       'identifier',
122       'lastUnlockedBySelf',
123       'name',
124       'permission',
125       'requestMethod',
126       'url',
127       'userGesture'
128     ]
129   },
130   'responsive': {
131     evt: CreateEvent('webViewInternal.onResponsive'),
132     fields: ['processId']
133   },
134   'sizechanged': {
135     evt: CreateEvent('webViewInternal.onSizeChanged'),
136     customHandler: function(handler, event, webViewEvent) {
137       handler.handleSizeChangedEvent(event, webViewEvent);
138     },
139     fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth']
140   },
141   'unresponsive': {
142     evt: CreateEvent('webViewInternal.onUnresponsive'),
143     fields: ['processId']
144   },
145   'zoomchange': {
146     evt: CreateEvent('webViewInternal.onZoomChange'),
147     fields: ['oldZoomFactor', 'newZoomFactor']
148   }
149 };
150
151 // Constructor.
152 function WebViewEvents(webViewInternal, viewInstanceId) {
153   this.webViewInternal = webViewInternal;
154   this.viewInstanceId = viewInstanceId;
155   this.setup();
156 }
157
158 // Sets up events.
159 WebViewEvents.prototype.setup = function() {
160   this.setupFrameNameChangedEvent();
161   this.setupPluginDestroyedEvent();
162   this.webViewInternal.maybeSetupChromeWebViewEvents();
163   this.webViewInternal.setupExperimentalContextMenus();
164
165   var events = this.getEvents();
166   for (var eventName in events) {
167     this.setupEvent(eventName, events[eventName]);
168   }
169 };
170
171 WebViewEvents.prototype.setupFrameNameChangedEvent = function() {
172   FrameNameChangedEvent.addListener(function(e) {
173     this.webViewInternal.onFrameNameChanged(e.name);
174   }.bind(this), {instanceId: this.viewInstanceId});
175 };
176
177 WebViewEvents.prototype.setupPluginDestroyedEvent = function() {
178   PluginDestroyedEvent.addListener(function(e) {
179     this.webViewInternal.onPluginDestroyed();
180   }.bind(this), {instanceId: this.viewInstanceId});
181 };
182
183 WebViewEvents.prototype.getEvents = function() {
184   var experimentalEvents = this.webViewInternal.maybeGetExperimentalEvents();
185   for (var eventName in experimentalEvents) {
186     WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName];
187   }
188   var chromeEvents = this.webViewInternal.maybeGetChromeWebViewEvents();
189   for (var eventName in chromeEvents) {
190     WEB_VIEW_EVENTS[eventName] = chromeEvents[eventName];
191   }
192   return WEB_VIEW_EVENTS;
193 };
194
195 WebViewEvents.prototype.setupEvent = function(name, info) {
196   info.evt.addListener(function(e) {
197     var details = {bubbles:true};
198     if (info.cancelable) {
199       details.cancelable = true;
200     }
201     var webViewEvent = new Event(name, details);
202     $Array.forEach(info.fields, function(field) {
203       if (e[field] !== undefined) {
204         webViewEvent[field] = e[field];
205       }
206     }.bind(this));
207     if (info.customHandler) {
208       info.customHandler(this, e, webViewEvent);
209       return;
210     }
211     this.webViewInternal.dispatchEvent(webViewEvent);
212   }.bind(this), {instanceId: this.viewInstanceId});
213
214   this.webViewInternal.setupEventProperty(name);
215 };
216
217
218 WebViewEvents.prototype.handleDialogEvent = function(event, webViewEvent) {
219   var showWarningMessage = function(dialogType) {
220     var VOWELS = ['a', 'e', 'i', 'o', 'u'];
221     var WARNING_MSG_DIALOG_BLOCKED = '<webview>: %1 %2 dialog was blocked.';
222     var article = (VOWELS.indexOf(dialogType.charAt(0)) >= 0) ? 'An' : 'A';
223     var output = WARNING_MSG_DIALOG_BLOCKED.replace('%1', article);
224     output = output.replace('%2', dialogType);
225     window.console.warn(output);
226   };
227
228   var requestId = event.requestId;
229   var actionTaken = false;
230
231   var validateCall = function() {
232     var ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN = '<webview>: ' +
233         'An action has already been taken for this "dialog" event.';
234
235     if (actionTaken) {
236       throw new Error(ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN);
237     }
238     actionTaken = true;
239   };
240
241   var getGuestInstanceId = function() {
242     return this.webViewInternal.getGuestInstanceId();
243   }.bind(this);
244
245   var dialog = {
246     ok: function(user_input) {
247       validateCall();
248       user_input = user_input || '';
249       WebView.setPermission(getGuestInstanceId(), requestId, 'allow',
250                             user_input);
251     },
252     cancel: function() {
253       validateCall();
254       WebView.setPermission(getGuestInstanceId(), requestId, 'deny');
255     }
256   };
257   webViewEvent.dialog = dialog;
258
259   var defaultPrevented = !this.webViewInternal.dispatchEvent(webViewEvent);
260   if (actionTaken) {
261     return;
262   }
263
264   if (defaultPrevented) {
265     // Tell the JavaScript garbage collector to track lifetime of |dialog| and
266     // call back when the dialog object has been collected.
267     MessagingNatives.BindToGC(dialog, function() {
268       // Avoid showing a warning message if the decision has already been made.
269       if (actionTaken) {
270         return;
271       }
272       WebView.setPermission(
273           getGuestInstanceId(), requestId, 'default', '', function(allowed) {
274         if (allowed) {
275           return;
276         }
277         showWarningMessage(event.messageType);
278       });
279     });
280   } else {
281     actionTaken = true;
282     // The default action is equivalent to canceling the dialog.
283     WebView.setPermission(
284         getGuestInstanceId(), requestId, 'default', '', function(allowed) {
285       if (allowed) {
286         return;
287       }
288       showWarningMessage(event.messageType);
289     });
290   }
291 };
292
293 WebViewEvents.prototype.handleLoadAbortEvent = function(event, webViewEvent) {
294   var showWarningMessage = function(reason) {
295     var WARNING_MSG_LOAD_ABORTED = '<webview>: ' +
296         'The load has aborted with reason "%1".';
297     window.console.warn(WARNING_MSG_LOAD_ABORTED.replace('%1', reason));
298   };
299   if (this.webViewInternal.dispatchEvent(webViewEvent)) {
300     showWarningMessage(event.reason);
301   }
302 };
303
304 WebViewEvents.prototype.handleLoadCommitEvent = function(event, webViewEvent) {
305   this.webViewInternal.onLoadCommit(event.baseUrlForDataUrl,
306                                     event.currentEntryIndex, event.entryCount,
307                                     event.processId, event.url,
308                                     event.isTopLevel);
309   this.webViewInternal.dispatchEvent(webViewEvent);
310 };
311
312 WebViewEvents.prototype.handleNewWindowEvent = function(event, webViewEvent) {
313   var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' +
314       'An action has already been taken for this "newwindow" event.';
315
316   var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' +
317       'Unable to attach the new window to the provided webViewInternal.';
318
319   var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.';
320
321   var showWarningMessage = function() {
322     var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.';
323     window.console.warn(WARNING_MSG_NEWWINDOW_BLOCKED);
324   };
325
326   var requestId = event.requestId;
327   var actionTaken = false;
328   var getGuestInstanceId = function() {
329     return this.webViewInternal.getGuestInstanceId();
330   }.bind(this);
331
332   var validateCall = function () {
333     if (actionTaken) {
334       throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN);
335     }
336     actionTaken = true;
337   };
338
339   var windowObj = {
340     attach: function(webview) {
341       validateCall();
342       if (!webview || !webview.tagName || webview.tagName != 'WEBVIEW')
343         throw new Error(ERROR_MSG_WEBVIEW_EXPECTED);
344       // Attach happens asynchronously to give the tagWatcher an opportunity
345       // to pick up the new webview before attach operates on it, if it hasn't
346       // been attached to the DOM already.
347       // Note: Any subsequent errors cannot be exceptions because they happen
348       // asynchronously.
349       setTimeout(function() {
350         var webViewInternal = privates(webview).internal;
351         // Update the partition.
352         if (event.storagePartitionId) {
353           webViewInternal.onAttach(event.storagePartitionId);
354         }
355
356         var attached = webViewInternal.attachWindow(event.windowId, true);
357
358         if (!attached) {
359           window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH);
360         }
361
362         var guestInstanceId = getGuestInstanceId();
363         if (!guestInstanceId) {
364           // If the opener is already gone, then we won't have its
365           // guestInstanceId.
366           return;
367         }
368
369         // If the object being passed into attach is not a valid <webview>
370         // then we will fail and it will be treated as if the new window
371         // was rejected. The permission API plumbing is used here to clean
372         // up the state created for the new window if attaching fails.
373         WebView.setPermission(
374             guestInstanceId, requestId, attached ? 'allow' : 'deny');
375       }, 0);
376     },
377     discard: function() {
378       validateCall();
379       var guestInstanceId = getGuestInstanceId();
380       if (!guestInstanceId) {
381         // If the opener is already gone, then we won't have its
382         // guestInstanceId.
383         return;
384       }
385       WebView.setPermission(guestInstanceId, requestId, 'deny');
386     }
387   };
388   webViewEvent.window = windowObj;
389
390   var defaultPrevented = !this.webViewInternal.dispatchEvent(webViewEvent);
391   if (actionTaken) {
392     return;
393   }
394
395   if (defaultPrevented) {
396     // Make browser plugin track lifetime of |windowObj|.
397     MessagingNatives.BindToGC(windowObj, function() {
398       // Avoid showing a warning message if the decision has already been made.
399       if (actionTaken) {
400         return;
401       }
402
403       var guestInstanceId = getGuestInstanceId();
404       if (!guestInstanceId) {
405         // If the opener is already gone, then we won't have its
406         // guestInstanceId.
407         return;
408       }
409
410       WebView.setPermission(
411           guestInstanceId, requestId, 'default', '', function(allowed) {
412             if (allowed) {
413               return;
414             }
415             showWarningMessage();
416           });
417     });
418   } else {
419     actionTaken = true;
420     // The default action is to discard the window.
421     WebView.setPermission(
422         getGuestInstanceId(), requestId, 'default', '', function(allowed) {
423       if (allowed) {
424         return;
425       }
426       showWarningMessage();
427     });
428   }
429 };
430
431 WebViewEvents.prototype.getPermissionTypes = function() {
432   var permissions =
433       ['media',
434       'geolocation',
435       'pointerLock',
436       'download',
437       'loadplugin',
438       'filesystem'];
439   return permissions.concat(
440       this.webViewInternal.maybeGetExperimentalPermissions());
441 };
442
443 WebViewEvents.prototype.handlePermissionEvent =
444     function(event, webViewEvent) {
445   var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' +
446       'Permission has already been decided for this "permissionrequest" event.';
447
448   var showWarningMessage = function(permission) {
449     var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' +
450         'The permission request for "%1" has been denied.';
451     window.console.warn(
452         WARNING_MSG_PERMISSION_DENIED.replace('%1', permission));
453   };
454
455   var requestId = event.requestId;
456   var getGuestInstanceId = function() {
457     return this.webViewInternal.getGuestInstanceId();
458   }.bind(this);
459
460   if (this.getPermissionTypes().indexOf(event.permission) < 0) {
461     // The permission type is not allowed. Trigger the default response.
462     WebView.setPermission(
463         getGuestInstanceId(), requestId, 'default', '', function(allowed) {
464       if (allowed) {
465         return;
466       }
467       showWarningMessage(event.permission);
468     });
469     return;
470   }
471
472   var decisionMade = false;
473   var validateCall = function() {
474     if (decisionMade) {
475       throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED);
476     }
477     decisionMade = true;
478   };
479
480   // Construct the event.request object.
481   var request = {
482     allow: function() {
483       validateCall();
484       WebView.setPermission(getGuestInstanceId(), requestId, 'allow');
485     },
486     deny: function() {
487       validateCall();
488       WebView.setPermission(getGuestInstanceId(), requestId, 'deny');
489     }
490   };
491   webViewEvent.request = request;
492
493   var defaultPrevented = !this.webViewInternal.dispatchEvent(webViewEvent);
494   if (decisionMade) {
495     return;
496   }
497
498   if (defaultPrevented) {
499     // Make browser plugin track lifetime of |request|.
500     MessagingNatives.BindToGC(request, function() {
501       // Avoid showing a warning message if the decision has already been made.
502       if (decisionMade) {
503         return;
504       }
505       WebView.setPermission(
506           getGuestInstanceId(), requestId, 'default', '', function(allowed) {
507         if (allowed) {
508           return;
509         }
510         showWarningMessage(event.permission);
511       });
512     });
513   } else {
514     decisionMade = true;
515     WebView.setPermission(
516         getGuestInstanceId(), requestId, 'default', '',
517         function(allowed) {
518           if (allowed) {
519             return;
520           }
521           showWarningMessage(event.permission);
522         });
523   }
524 };
525
526 WebViewEvents.prototype.handleSizeChangedEvent = function(
527     event, webViewEvent) {
528   this.webViewInternal.onSizeChanged(webViewEvent);
529 };
530
531 exports.WebViewEvents = WebViewEvents;
532 exports.CreateEvent = CreateEvent;