Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / chrome / renderer / resources / extensions / 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 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;
16
17 var CreateEvent = function(name) {
18   var eventOpts = {supportsListeners: true, supportsFilters: true};
19   return new EventBindings.Event(name, undefined, eventOpts);
20 };
21
22 var FrameNameChangedEvent = CreateEvent('webViewInternal.onFrameNameChanged');
23 var WebRequestMessageEvent = CreateEvent('webViewInternal.onMessage');
24
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   'findupdate': {
73     evt: CreateEvent('webViewInternal.onFindReply'),
74     fields: [
75       'searchText',
76       'numberOfMatches',
77       'activeMatchOrdinal',
78       'selectionRect',
79       'canceled',
80       'finalUpdate'
81     ]
82   },
83   'loadabort': {
84     cancelable: true,
85     customHandler: function(handler, event, webViewEvent) {
86       handler.handleLoadAbortEvent(event, webViewEvent);
87     },
88     evt: CreateEvent('webViewInternal.onLoadAbort'),
89     fields: ['url', 'isTopLevel', 'reason']
90   },
91   'loadcommit': {
92     customHandler: function(handler, event, webViewEvent) {
93       handler.handleLoadCommitEvent(event, webViewEvent);
94     },
95     evt: CreateEvent('webViewInternal.onLoadCommit'),
96     fields: ['url', 'isTopLevel']
97   },
98   'loadprogress': {
99     evt: CreateEvent('webViewInternal.onLoadProgress'),
100     fields: ['url', 'progress']
101   },
102   'loadredirect': {
103     evt: CreateEvent('webViewInternal.onLoadRedirect'),
104     fields: ['isTopLevel', 'oldUrl', 'newUrl']
105   },
106   'loadstart': {
107     evt: CreateEvent('webViewInternal.onLoadStart'),
108     fields: ['url', 'isTopLevel']
109   },
110   'loadstop': {
111     evt: CreateEvent('webViewInternal.onLoadStop'),
112     fields: []
113   },
114   'newwindow': {
115     cancelable: true,
116     customHandler: function(handler, event, webViewEvent) {
117       handler.handleNewWindowEvent(event, webViewEvent);
118     },
119     evt: CreateEvent('webViewInternal.onNewWindow'),
120     fields: [
121       'initialHeight',
122       'initialWidth',
123       'targetUrl',
124       'windowOpenDisposition',
125       'name'
126     ]
127   },
128   'permissionrequest': {
129     cancelable: true,
130     customHandler: function(handler, event, webViewEvent) {
131       handler.handlePermissionEvent(event, webViewEvent);
132     },
133     evt: CreateEvent('webViewInternal.onPermissionRequest'),
134     fields: [
135       'identifier',
136       'lastUnlockedBySelf',
137       'name',
138       'permission',
139       'requestMethod',
140       'url',
141       'userGesture'
142     ]
143   },
144   'responsive': {
145     evt: CreateEvent('webViewInternal.onResponsive'),
146     fields: ['processId']
147   },
148   'sizechanged': {
149     evt: CreateEvent('webViewInternal.onSizeChanged'),
150     customHandler: function(handler, event, webViewEvent) {
151       handler.handleSizeChangedEvent(event, webViewEvent);
152     },
153     fields: ['oldHeight', 'oldWidth', 'newHeight', 'newWidth']
154   },
155   'unresponsive': {
156     evt: CreateEvent('webViewInternal.onUnresponsive'),
157     fields: ['processId']
158   },
159   'zoomchange': {
160     evt: CreateEvent('webViewInternal.onZoomChange'),
161     fields: ['oldZoomFactor', 'newZoomFactor']
162   }
163 };
164
165 function DeclarativeWebRequestEvent(opt_eventName,
166                                     opt_argSchemas,
167                                     opt_eventOptions,
168                                     opt_webViewInstanceId) {
169   var subEventName = opt_eventName + '/' + IdGenerator.GetNextId();
170   EventBindings.Event.call(this, subEventName, opt_argSchemas, opt_eventOptions,
171       opt_webViewInstanceId);
172
173   var self = this;
174   // TODO(lazyboy): When do we dispose this listener?
175   WebRequestMessageEvent.addListener(function() {
176     // Re-dispatch to subEvent's listeners.
177     $Function.apply(self.dispatch, self, $Array.slice(arguments));
178   }, {instanceId: opt_webViewInstanceId || 0});
179 }
180
181 DeclarativeWebRequestEvent.prototype = {
182   __proto__: EventBindings.Event.prototype
183 };
184
185 // Constructor.
186 function WebViewEvents(webViewInternal, viewInstanceId) {
187   this.webViewInternal = webViewInternal;
188   this.viewInstanceId = viewInstanceId;
189   this.setup();
190 }
191
192 // Sets up events.
193 WebViewEvents.prototype.setup = function() {
194   this.setupFrameNameChangedEvent();
195   this.setupWebRequestEvents();
196   this.webViewInternal.setupExperimentalContextMenus();
197
198   var events = this.getEvents();
199   for (var eventName in events) {
200     this.setupEvent(eventName, events[eventName]);
201   }
202 };
203
204 WebViewEvents.prototype.setupFrameNameChangedEvent = function() {
205   var self = this;
206   FrameNameChangedEvent.addListener(function(e) {
207     self.webViewInternal.onFrameNameChanged(e.name);
208   }, {instanceId: self.viewInstanceId});
209 };
210
211 WebViewEvents.prototype.setupWebRequestEvents = function() {
212   var self = this;
213   var request = {};
214   var createWebRequestEvent = function(webRequestEvent) {
215     return function() {
216       if (!self[webRequestEvent.name]) {
217         self[webRequestEvent.name] =
218             new WebRequestEvent(
219                 'webViewInternal.' + webRequestEvent.name,
220                 webRequestEvent.parameters,
221                 webRequestEvent.extraParameters, webRequestEvent.options,
222                 self.viewInstanceId);
223       }
224       return self[webRequestEvent.name];
225     };
226   };
227
228   var createDeclarativeWebRequestEvent = function(webRequestEvent) {
229     return function() {
230       if (!self[webRequestEvent.name]) {
231         // The onMessage event gets a special event type because we want
232         // the listener to fire only for messages targeted for this particular
233         // <webview>.
234         var EventClass = webRequestEvent.name === 'onMessage' ?
235             DeclarativeWebRequestEvent : EventBindings.Event;
236         self[webRequestEvent.name] =
237             new EventClass(
238                 'webViewInternal.' + webRequestEvent.name,
239                 webRequestEvent.parameters,
240                 webRequestEvent.options,
241                 self.viewInstanceId);
242       }
243       return self[webRequestEvent.name];
244     };
245   };
246
247   for (var i = 0; i < DeclarativeWebRequestSchema.events.length; ++i) {
248     var eventSchema = DeclarativeWebRequestSchema.events[i];
249     var webRequestEvent = createDeclarativeWebRequestEvent(eventSchema);
250     Object.defineProperty(
251         request,
252         eventSchema.name,
253         {
254           get: webRequestEvent,
255           enumerable: true
256         }
257     );
258   }
259
260   // Populate the WebRequest events from the API definition.
261   for (var i = 0; i < WebRequestSchema.events.length; ++i) {
262     var webRequestEvent = createWebRequestEvent(WebRequestSchema.events[i]);
263     Object.defineProperty(
264         request,
265         WebRequestSchema.events[i].name,
266         {
267           get: webRequestEvent,
268           enumerable: true
269         }
270     );
271   }
272
273   this.webViewInternal.setRequestPropertyOnWebViewNode(request);
274 };
275
276 WebViewEvents.prototype.getEvents = function() {
277   var experimentalEvents = this.webViewInternal.maybeGetExperimentalEvents();
278   for (var eventName in experimentalEvents) {
279     WEB_VIEW_EVENTS[eventName] = experimentalEvents[eventName];
280   }
281   return WEB_VIEW_EVENTS;
282 };
283
284 WebViewEvents.prototype.setupEvent = function(name, info) {
285   var self = this;
286   info.evt.addListener(function(e) {
287     var details = {bubbles:true};
288     if (info.cancelable)
289       details.cancelable = true;
290     var webViewEvent = new Event(name, details);
291     $Array.forEach(info.fields, function(field) {
292       if (e[field] !== undefined) {
293         webViewEvent[field] = e[field];
294       }
295     });
296     if (info.customHandler) {
297       info.customHandler(self, e, webViewEvent);
298       return;
299     }
300     self.webViewInternal.dispatchEvent(webViewEvent);
301   }, {instanceId: self.viewInstanceId});
302
303   this.webViewInternal.setupEventProperty(name);
304 };
305
306
307 // Event handlers.
308 WebViewEvents.prototype.handleContextMenu = function(e, webViewEvent) {
309   this.webViewInternal.maybeHandleContextMenu(e, webViewEvent);
310 };
311
312 WebViewEvents.prototype.handleDialogEvent = function(event, webViewEvent) {
313   var showWarningMessage = function(dialogType) {
314     var VOWELS = ['a', 'e', 'i', 'o', 'u'];
315     var WARNING_MSG_DIALOG_BLOCKED = '<webview>: %1 %2 dialog was blocked.';
316     var article = (VOWELS.indexOf(dialogType.charAt(0)) >= 0) ? 'An' : 'A';
317     var output = WARNING_MSG_DIALOG_BLOCKED.replace('%1', article);
318     output = output.replace('%2', dialogType);
319     window.console.warn(output);
320   };
321
322   var self = this;
323   var requestId = event.requestId;
324   var actionTaken = false;
325
326   var validateCall = function() {
327     var ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN = '<webview>: ' +
328         'An action has already been taken for this "dialog" event.';
329
330     if (actionTaken) {
331       throw new Error(ERROR_MSG_DIALOG_ACTION_ALREADY_TAKEN);
332     }
333     actionTaken = true;
334   };
335
336   var getInstanceId = function() {
337     return self.webViewInternal.getInstanceId();
338   };
339
340   var dialog = {
341     ok: function(user_input) {
342       validateCall();
343       user_input = user_input || '';
344       WebView.setPermission(getInstanceId(), requestId, 'allow', user_input);
345     },
346     cancel: function() {
347       validateCall();
348       WebView.setPermission(getInstanceId(), requestId, 'deny');
349     }
350   };
351   webViewEvent.dialog = dialog;
352
353   var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent);
354   if (actionTaken) {
355     return;
356   }
357
358   if (defaultPrevented) {
359     // Tell the JavaScript garbage collector to track lifetime of |dialog| and
360     // call back when the dialog object has been collected.
361     MessagingNatives.BindToGC(dialog, function() {
362       // Avoid showing a warning message if the decision has already been made.
363       if (actionTaken) {
364         return;
365       }
366       WebView.setPermission(
367           getInstanceId(), requestId, 'default', '', function(allowed) {
368         if (allowed) {
369           return;
370         }
371         showWarningMessage(event.messageType);
372       });
373     });
374   } else {
375     actionTaken = true;
376     // The default action is equivalent to canceling the dialog.
377     WebView.setPermission(
378         getInstanceId(), requestId, 'default', '', function(allowed) {
379       if (allowed) {
380         return;
381       }
382       showWarningMessage(event.messageType);
383     });
384   }
385 };
386
387 WebViewEvents.prototype.handleLoadAbortEvent = function(event, webViewEvent) {
388   var showWarningMessage = function(reason) {
389     var WARNING_MSG_LOAD_ABORTED = '<webview>: ' +
390         'The load has aborted with reason "%1".';
391     window.console.warn(WARNING_MSG_LOAD_ABORTED.replace('%1', reason));
392   };
393   if (this.webViewInternal.dispatchEvent(webViewEvent)) {
394     showWarningMessage(event.reason);
395   }
396 };
397
398 WebViewEvents.prototype.handleLoadCommitEvent = function(event, webViewEvent) {
399   this.webViewInternal.onLoadCommit(event.currentEntryIndex, event.entryCount,
400                                     event.processId, event.url,
401                                     event.isTopLevel);
402   this.webViewInternal.dispatchEvent(webViewEvent);
403 };
404
405 WebViewEvents.prototype.handleNewWindowEvent = function(event, webViewEvent) {
406   var ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN = '<webview>: ' +
407       'An action has already been taken for this "newwindow" event.';
408
409   var ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH = '<webview>: ' +
410       'Unable to attach the new window to the provided webViewInternal.';
411
412   var ERROR_MSG_WEBVIEW_EXPECTED = '<webview> element expected.';
413
414   var showWarningMessage = function() {
415     var WARNING_MSG_NEWWINDOW_BLOCKED = '<webview>: A new window was blocked.';
416     window.console.warn(WARNING_MSG_NEWWINDOW_BLOCKED);
417   };
418
419   var requestId = event.requestId;
420   var actionTaken = false;
421   var self = this;
422   var getInstanceId = function() {
423     return self.webViewInternal.getInstanceId();
424   };
425
426   var validateCall = function () {
427     if (actionTaken) {
428       throw new Error(ERROR_MSG_NEWWINDOW_ACTION_ALREADY_TAKEN);
429     }
430     actionTaken = true;
431   };
432
433   var windowObj = {
434     attach: function(webview) {
435       validateCall();
436       if (!webview || !webview.tagName || webview.tagName != 'WEBVIEW')
437         throw new Error(ERROR_MSG_WEBVIEW_EXPECTED);
438       // Attach happens asynchronously to give the tagWatcher an opportunity
439       // to pick up the new webview before attach operates on it, if it hasn't
440       // been attached to the DOM already.
441       // Note: Any subsequent errors cannot be exceptions because they happen
442       // asynchronously.
443       setTimeout(function() {
444         var webViewInternal = privates(webview).internal;
445         // Update the partition.
446         if (event.storagePartitionId) {
447           webViewInternal.onAttach(event.storagePartitionId);
448         }
449
450         var attached = webViewInternal.attachWindow(event.windowId, true);
451
452         if (!attached) {
453           window.console.error(ERROR_MSG_NEWWINDOW_UNABLE_TO_ATTACH);
454         }
455         // If the object being passed into attach is not a valid <webview>
456         // then we will fail and it will be treated as if the new window
457         // was rejected. The permission API plumbing is used here to clean
458         // up the state created for the new window if attaching fails.
459         WebView.setPermission(
460             getInstanceId(), requestId, attached ? 'allow' : 'deny');
461       }, 0);
462     },
463     discard: function() {
464       validateCall();
465       WebView.setPermission(getInstanceId(), requestId, 'deny');
466     }
467   };
468   webViewEvent.window = windowObj;
469
470   var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent);
471   if (actionTaken) {
472     return;
473   }
474
475   if (defaultPrevented) {
476     // Make browser plugin track lifetime of |windowObj|.
477     MessagingNatives.BindToGC(windowObj, function() {
478       // Avoid showing a warning message if the decision has already been made.
479       if (actionTaken) {
480         return;
481       }
482       WebView.setPermission(
483           getInstanceId(), requestId, 'default', '', function(allowed) {
484         if (allowed) {
485           return;
486         }
487         showWarningMessage();
488       });
489     });
490   } else {
491     actionTaken = true;
492     // The default action is to discard the window.
493     WebView.setPermission(
494         getInstanceId(), requestId, 'default', '', function(allowed) {
495       if (allowed) {
496         return;
497       }
498       showWarningMessage();
499     });
500   }
501 };
502
503 WebViewEvents.prototype.getPermissionTypes = function() {
504   var permissions =
505       ['media',
506       'geolocation',
507       'pointerLock',
508       'download',
509       'loadplugin',
510       'filesystem'];
511   return permissions.concat(
512       this.webViewInternal.maybeGetExperimentalPermissions());
513 };
514
515 WebViewEvents.prototype.handlePermissionEvent =
516     function(event, webViewEvent) {
517   var ERROR_MSG_PERMISSION_ALREADY_DECIDED = '<webview>: ' +
518       'Permission has already been decided for this "permissionrequest" event.';
519
520   var showWarningMessage = function(permission) {
521     var WARNING_MSG_PERMISSION_DENIED = '<webview>: ' +
522         'The permission request for "%1" has been denied.';
523     window.console.warn(
524         WARNING_MSG_PERMISSION_DENIED.replace('%1', permission));
525   };
526
527   var requestId = event.requestId;
528   var self = this;
529   var getInstanceId = function() {
530     return self.webViewInternal.getInstanceId();
531   };
532
533   if (this.getPermissionTypes().indexOf(event.permission) < 0) {
534     // The permission type is not allowed. Trigger the default response.
535     WebView.setPermission(
536         getInstanceId(), requestId, 'default', '', function(allowed) {
537       if (allowed) {
538         return;
539       }
540       showWarningMessage(event.permission);
541     });
542     return;
543   }
544
545   var decisionMade = false;
546   var validateCall = function() {
547     if (decisionMade) {
548       throw new Error(ERROR_MSG_PERMISSION_ALREADY_DECIDED);
549     }
550     decisionMade = true;
551   };
552
553   // Construct the event.request object.
554   var request = {
555     allow: function() {
556       validateCall();
557       WebView.setPermission(getInstanceId(), requestId, 'allow');
558     },
559     deny: function() {
560       validateCall();
561       WebView.setPermission(getInstanceId(), requestId, 'deny');
562     }
563   };
564   webViewEvent.request = request;
565
566   var defaultPrevented = !self.webViewInternal.dispatchEvent(webViewEvent);
567   if (decisionMade) {
568     return;
569   }
570
571   if (defaultPrevented) {
572     // Make browser plugin track lifetime of |request|.
573     MessagingNatives.BindToGC(request, function() {
574       // Avoid showing a warning message if the decision has already been made.
575       if (decisionMade) {
576         return;
577       }
578       WebView.setPermission(
579           getInstanceId(), requestId, 'default', '', function(allowed) {
580         if (allowed) {
581           return;
582         }
583         showWarningMessage(event.permission);
584       });
585     });
586   } else {
587     decisionMade = true;
588     WebView.setPermission(
589         getInstanceId(), requestId, 'default', '', function(allowed) {
590       if (allowed) {
591         return;
592       }
593       showWarningMessage(event.permission);
594     });
595   }
596 };
597
598 WebViewEvents.prototype.handleSizeChangedEvent = function(
599     event, webViewEvent) {
600   this.webViewInternal.onSizeChanged(webViewEvent);
601 };
602
603 exports.WebViewEvents = WebViewEvents;
604 exports.CreateEvent = CreateEvent;