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