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