Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(183)

Side by Side Diff: chrome/renderer/resources/extensions/web_view_events.js

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

Powered by Google App Engine
This is Rietveld 408576698