441 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			441 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| /* This Source Code Form is subject to the terms of the Mozilla Public
 | |
|  * License, v. 2.0. If a copy of the MPL was not distributed with this
 | |
|  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| const {Helper} = ChromeUtils.import('chrome://juggler/content/Helper.js');
 | |
| const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | |
| const {NetworkObserver, PageNetwork} = ChromeUtils.import('chrome://juggler/content/NetworkObserver.js');
 | |
| 
 | |
| const Cc = Components.classes;
 | |
| const Ci = Components.interfaces;
 | |
| const Cu = Components.utils;
 | |
| const XUL_NS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
 | |
| const helper = new Helper();
 | |
| 
 | |
| class WorkerHandler {
 | |
|   constructor(session, contentChannel, workerId) {
 | |
|     this._session = session;
 | |
|     this._contentWorker = contentChannel.connect(workerId);
 | |
|     this._workerId = workerId;
 | |
| 
 | |
|     const emitWrappedProtocolEvent = eventName => {
 | |
|       return params => {
 | |
|         this._session.emitEvent('Page.dispatchMessageFromWorker', {
 | |
|           workerId,
 | |
|           message: JSON.stringify({method: eventName, params}),
 | |
|         });
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this._eventListeners = [
 | |
|       contentChannel.register(workerId, {
 | |
|         runtimeConsole: emitWrappedProtocolEvent('Runtime.console'),
 | |
|         runtimeExecutionContextCreated: emitWrappedProtocolEvent('Runtime.executionContextCreated'),
 | |
|         runtimeExecutionContextDestroyed: emitWrappedProtocolEvent('Runtime.executionContextDestroyed'),
 | |
|       }),
 | |
|     ];
 | |
|   }
 | |
| 
 | |
|   async sendMessage(message) {
 | |
|     const [domain, method] = message.method.split('.');
 | |
|     if (domain !== 'Runtime')
 | |
|       throw new Error('ERROR: can only dispatch to Runtime domain inside worker');
 | |
|     const result = await this._contentWorker.send(method, message.params);
 | |
|     this._session.emitEvent('Page.dispatchMessageFromWorker', {
 | |
|       workerId: this._workerId,
 | |
|       message: JSON.stringify({result, id: message.id}),
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   dispose() {
 | |
|     this._contentWorker.dispose();
 | |
|     helper.removeListeners(this._eventListeners);
 | |
|   }
 | |
| }
 | |
| 
 | |
| class PageHandler {
 | |
|   constructor(target, session, contentChannel) {
 | |
|     this._session = session;
 | |
|     this._contentChannel = contentChannel;
 | |
|     this._contentPage = contentChannel.connect('page');
 | |
|     this._contentRuntime = contentChannel.connect('runtime');
 | |
|     this._workers = new Map();
 | |
| 
 | |
|     this._pageTarget = target;
 | |
|     this._browser = target.linkedBrowser();
 | |
|     this._dialogs = new Map();
 | |
|     this._pageNetwork = NetworkObserver.instance().pageNetworkForTarget(target);
 | |
| 
 | |
|     const emitProtocolEvent = eventName => {
 | |
|       return (...args) => this._session.emitEvent(eventName, ...args);
 | |
|     }
 | |
| 
 | |
|     this._reportedFrameIds = new Set();
 | |
|     this._networkEventsForUnreportedFrameIds = new Map();
 | |
| 
 | |
|     this._eventListeners = [
 | |
|       contentChannel.register('page', {
 | |
|         pageBindingCalled: emitProtocolEvent('Page.bindingCalled'),
 | |
|         pageDispatchMessageFromWorker: emitProtocolEvent('Page.dispatchMessageFromWorker'),
 | |
|         pageEventFired: emitProtocolEvent('Page.eventFired'),
 | |
|         pageFileChooserOpened: emitProtocolEvent('Page.fileChooserOpened'),
 | |
|         pageFrameAttached: this._onFrameAttached.bind(this),
 | |
|         pageFrameDetached: emitProtocolEvent('Page.frameDetached'),
 | |
|         pageLinkClicked: emitProtocolEvent('Page.linkClicked'),
 | |
|         pageWillOpenNewWindowAsynchronously: emitProtocolEvent('Page.willOpenNewWindowAsynchronously'),
 | |
|         pageNavigationAborted: emitProtocolEvent('Page.navigationAborted'),
 | |
|         pageNavigationCommitted: emitProtocolEvent('Page.navigationCommitted'),
 | |
|         pageNavigationStarted: emitProtocolEvent('Page.navigationStarted'),
 | |
|         pageReady: emitProtocolEvent('Page.ready'),
 | |
|         pageSameDocumentNavigation: emitProtocolEvent('Page.sameDocumentNavigation'),
 | |
|         pageUncaughtError: emitProtocolEvent('Page.uncaughtError'),
 | |
|         pageWorkerCreated: this._onWorkerCreated.bind(this),
 | |
|         pageWorkerDestroyed: this._onWorkerDestroyed.bind(this),
 | |
|       }),
 | |
|       contentChannel.register('runtime', {
 | |
|         runtimeConsole: emitProtocolEvent('Runtime.console'),
 | |
|         runtimeExecutionContextCreated: emitProtocolEvent('Runtime.executionContextCreated'),
 | |
|         runtimeExecutionContextDestroyed: emitProtocolEvent('Runtime.executionContextDestroyed'),
 | |
|       }),
 | |
|       helper.addEventListener(this._browser, 'DOMWillOpenModalDialog', async (event) => {
 | |
|         // wait for the dialog to be actually added to DOM.
 | |
|         await Promise.resolve();
 | |
|         this._updateModalDialogs();
 | |
|       }),
 | |
|       helper.addEventListener(this._browser, 'DOMModalDialogClosed', event => this._updateModalDialogs()),
 | |
|       helper.on(this._pageTarget, 'crashed', () => {
 | |
|         this._session.emitEvent('Page.crashed', {});
 | |
|       }),
 | |
|       helper.on(this._pageTarget, 'screencastStarted', () => {
 | |
|         const info = this._pageTarget.screencastInfo();
 | |
|         this._session.emitEvent('Page.screencastStarted', { screencastId: '' + info.videoSessionId, file: info.file });
 | |
|       }),
 | |
|       helper.on(this._pageNetwork, PageNetwork.Events.Request, this._handleNetworkEvent.bind(this, 'Network.requestWillBeSent')),
 | |
|       helper.on(this._pageNetwork, PageNetwork.Events.Response, this._handleNetworkEvent.bind(this, 'Network.responseReceived')),
 | |
|       helper.on(this._pageNetwork, PageNetwork.Events.RequestFinished, this._handleNetworkEvent.bind(this, 'Network.requestFinished')),
 | |
|       helper.on(this._pageNetwork, PageNetwork.Events.RequestFailed, this._handleNetworkEvent.bind(this, 'Network.requestFailed')),
 | |
|       this._pageNetwork.addSession(),
 | |
|     ];
 | |
| 
 | |
|     this._updateModalDialogs();
 | |
|     const options = this._pageTarget.browserContext().screencastOptions;
 | |
|     if (options)
 | |
|       this._pageTarget.startVideoRecording(options);
 | |
|   }
 | |
| 
 | |
|   _onWorkerCreated({workerId, frameId, url}) {
 | |
|     const worker = new WorkerHandler(this._session, this._contentChannel, workerId);
 | |
|     this._workers.set(workerId, worker);
 | |
|     this._session.emitEvent('Page.workerCreated', {workerId, frameId, url});
 | |
|   }
 | |
| 
 | |
|   _onWorkerDestroyed({workerId}) {
 | |
|     const worker = this._workers.get(workerId);
 | |
|     if (!worker)
 | |
|       return;
 | |
|     this._workers.delete(workerId);
 | |
|     worker.dispose();
 | |
|     this._session.emitEvent('Page.workerDestroyed', {workerId});
 | |
|   }
 | |
| 
 | |
|   _handleNetworkEvent(protocolEventName, eventDetails, frameId) {
 | |
|     if (!this._reportedFrameIds.has(frameId)) {
 | |
|       let events = this._networkEventsForUnreportedFrameIds.get(frameId);
 | |
|       if (!events) {
 | |
|         events = [];
 | |
|         this._networkEventsForUnreportedFrameIds.set(frameId, events);
 | |
|       }
 | |
|       events.push({eventName: protocolEventName, eventDetails});
 | |
|     } else {
 | |
|       this._session.emitEvent(protocolEventName, eventDetails);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   _onFrameAttached({frameId, parentFrameId}) {
 | |
|     this._session.emitEvent('Page.frameAttached', {frameId, parentFrameId});
 | |
|     this._reportedFrameIds.add(frameId);
 | |
|     const events = this._networkEventsForUnreportedFrameIds.get(frameId) || [];
 | |
|     this._networkEventsForUnreportedFrameIds.delete(frameId);
 | |
|     for (const {eventName, eventDetails} of events)
 | |
|       this._session.emitEvent(eventName, eventDetails);
 | |
|   }
 | |
| 
 | |
|   async ['Page.close']({runBeforeUnload}) {
 | |
|     // Postpone target close to deliver response in session.
 | |
|     Services.tm.dispatchToMainThread(() => {
 | |
|       this._pageTarget.close(runBeforeUnload);
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   async dispose() {
 | |
|     this._contentPage.dispose();
 | |
|     this._contentRuntime.dispose();
 | |
|     helper.removeListeners(this._eventListeners);
 | |
| 
 | |
|     if (this._pageTarget.screencastInfo())
 | |
|       await this._pageTarget.stopVideoRecording().catch(e => dump(`stopVideoRecording failed:\n${e}\n`));
 | |
|   }
 | |
| 
 | |
|   async ['Page.setViewportSize']({viewportSize}) {
 | |
|     await this._pageTarget.setViewportSize(viewportSize === null ? undefined : viewportSize);
 | |
|   }
 | |
| 
 | |
|   _updateModalDialogs() {
 | |
|     const prompts = new Set(this._browser.tabModalPromptBox ? this._browser.tabModalPromptBox.listPrompts() : []);
 | |
|     for (const dialog of this._dialogs.values()) {
 | |
|       if (!prompts.has(dialog.prompt())) {
 | |
|         this._dialogs.delete(dialog.id());
 | |
|         this._session.emitEvent('Page.dialogClosed', {
 | |
|           dialogId: dialog.id(),
 | |
|         });
 | |
|       } else {
 | |
|         prompts.delete(dialog.prompt());
 | |
|       }
 | |
|     }
 | |
|     for (const prompt of prompts) {
 | |
|       const dialog = Dialog.createIfSupported(prompt);
 | |
|       if (!dialog)
 | |
|         continue;
 | |
|       this._dialogs.set(dialog.id(), dialog);
 | |
|       this._session.emitEvent('Page.dialogOpened', {
 | |
|         dialogId: dialog.id(),
 | |
|         type: dialog.type(),
 | |
|         message: dialog.message(),
 | |
|         defaultValue: dialog.defaultValue(),
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async ['Runtime.evaluate'](options) {
 | |
|     return await this._contentRuntime.send('evaluate', options);
 | |
|   }
 | |
| 
 | |
|   async ['Runtime.callFunction'](options) {
 | |
|     return await this._contentRuntime.send('callFunction', options);
 | |
|   }
 | |
| 
 | |
|   async ['Runtime.getObjectProperties'](options) {
 | |
|     return await this._contentRuntime.send('getObjectProperties', options);
 | |
|   }
 | |
| 
 | |
|   async ['Runtime.disposeObject'](options) {
 | |
|     return await this._contentRuntime.send('disposeObject', options);
 | |
|   }
 | |
| 
 | |
|   async ['Network.getResponseBody']({requestId}) {
 | |
|     return this._pageNetwork.getResponseBody(requestId);
 | |
|   }
 | |
| 
 | |
|   async ['Network.setExtraHTTPHeaders']({headers}) {
 | |
|     this._pageNetwork.setExtraHTTPHeaders(headers);
 | |
|   }
 | |
| 
 | |
|   async ['Network.setRequestInterception']({enabled}) {
 | |
|     if (enabled)
 | |
|       this._pageNetwork.enableRequestInterception();
 | |
|     else
 | |
|       this._pageNetwork.disableRequestInterception();
 | |
|   }
 | |
| 
 | |
|   async ['Network.resumeInterceptedRequest']({requestId, method, headers, postData}) {
 | |
|     this._pageNetwork.resumeInterceptedRequest(requestId, method, headers, postData);
 | |
|   }
 | |
| 
 | |
|   async ['Network.abortInterceptedRequest']({requestId, errorCode}) {
 | |
|     this._pageNetwork.abortInterceptedRequest(requestId, errorCode);
 | |
|   }
 | |
| 
 | |
|   async ['Network.fulfillInterceptedRequest']({requestId, status, statusText, headers, base64body}) {
 | |
|     this._pageNetwork.fulfillInterceptedRequest(requestId, status, statusText, headers, base64body);
 | |
|   }
 | |
| 
 | |
|   async ['Accessibility.getFullAXTree'](params) {
 | |
|     return await this._contentPage.send('getFullAXTree', params);
 | |
|   }
 | |
| 
 | |
|   async ['Page.setFileInputFiles'](options) {
 | |
|     return await this._contentPage.send('setFileInputFiles', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.setEmulatedMedia'](options) {
 | |
|     return await this._contentPage.send('setEmulatedMedia', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.bringToFront'](options) {
 | |
|     this._pageTarget._window.focus();
 | |
|   }
 | |
| 
 | |
|   async ['Page.setCacheDisabled'](options) {
 | |
|     return await this._contentPage.send('setCacheDisabled', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.addBinding'](options) {
 | |
|     return await this._contentPage.send('addBinding', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.adoptNode'](options) {
 | |
|     return await this._contentPage.send('adoptNode', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.screenshot'](options) {
 | |
|     return await this._contentPage.send('screenshot', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.getBoundingBox'](options) {
 | |
|     return await this._contentPage.send('getBoundingBox', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.getContentQuads'](options) {
 | |
|     return await this._contentPage.send('getContentQuads', options);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {{frameId: string, url: string}} options
 | |
|    */
 | |
|   async ['Page.navigate'](options) {
 | |
|     return await this._contentPage.send('navigate', options);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {{frameId: string, url: string}} options
 | |
|    */
 | |
|   async ['Page.goBack'](options) {
 | |
|     return await this._contentPage.send('goBack', options);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {{frameId: string, url: string}} options
 | |
|    */
 | |
|   async ['Page.goForward'](options) {
 | |
|     return await this._contentPage.send('goForward', options);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param {{frameId: string, url: string}} options
 | |
|    */
 | |
|   async ['Page.reload'](options) {
 | |
|     return await this._contentPage.send('reload', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.describeNode'](options) {
 | |
|     return await this._contentPage.send('describeNode', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.scrollIntoViewIfNeeded'](options) {
 | |
|     return await this._contentPage.send('scrollIntoViewIfNeeded', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.addScriptToEvaluateOnNewDocument'](options) {
 | |
|     return await this._contentPage.send('addScriptToEvaluateOnNewDocument', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.removeScriptToEvaluateOnNewDocument'](options) {
 | |
|     return await this._contentPage.send('removeScriptToEvaluateOnNewDocument', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.dispatchKeyEvent'](options) {
 | |
|     return await this._contentPage.send('dispatchKeyEvent', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.dispatchTouchEvent'](options) {
 | |
|     return await this._contentPage.send('dispatchTouchEvent', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.dispatchMouseEvent'](options) {
 | |
|     return await this._contentPage.send('dispatchMouseEvent', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.insertText'](options) {
 | |
|     return await this._contentPage.send('insertText', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.crash'](options) {
 | |
|     return await this._contentPage.send('crash', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.handleDialog']({dialogId, accept, promptText}) {
 | |
|     const dialog = this._dialogs.get(dialogId);
 | |
|     if (!dialog)
 | |
|       throw new Error('Failed to find dialog with id = ' + dialogId);
 | |
|     if (accept)
 | |
|       dialog.accept(promptText);
 | |
|     else
 | |
|       dialog.dismiss();
 | |
|   }
 | |
| 
 | |
|   async ['Page.setInterceptFileChooserDialog'](options) {
 | |
|     return await this._contentPage.send('setInterceptFileChooserDialog', options);
 | |
|   }
 | |
| 
 | |
|   async ['Page.sendMessageToWorker']({workerId, message}) {
 | |
|     const worker = this._workers.get(workerId);
 | |
|     if (!worker)
 | |
|       throw new Error('ERROR: cannot find worker with id ' + workerId);
 | |
|     return await worker.sendMessage(JSON.parse(message));
 | |
|   }
 | |
| 
 | |
|   async ['Page.stopVideoRecording']() {
 | |
|     await this._pageTarget.stopVideoRecording();
 | |
|   }
 | |
| }
 | |
| 
 | |
| class Dialog {
 | |
|   static createIfSupported(prompt) {
 | |
|     const type = prompt.args.promptType;
 | |
|     switch (type) {
 | |
|       case 'alert':
 | |
|       case 'prompt':
 | |
|       case 'confirm':
 | |
|         return new Dialog(prompt, type);
 | |
|       case 'confirmEx':
 | |
|         return new Dialog(prompt, 'beforeunload');
 | |
|       default:
 | |
|         return null;
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   constructor(prompt, type) {
 | |
|     this._id = helper.generateId();
 | |
|     this._type = type;
 | |
|     this._prompt = prompt;
 | |
|   }
 | |
| 
 | |
|   id() {
 | |
|     return this._id;
 | |
|   }
 | |
| 
 | |
|   message() {
 | |
|     return this._prompt.ui.infoBody.textContent;
 | |
|   }
 | |
| 
 | |
|   type() {
 | |
|     return this._type;
 | |
|   }
 | |
| 
 | |
|   prompt() {
 | |
|     return this._prompt;
 | |
|   }
 | |
| 
 | |
|   dismiss() {
 | |
|     if (this._prompt.ui.button1)
 | |
|       this._prompt.ui.button1.click();
 | |
|     else
 | |
|       this._prompt.ui.button0.click();
 | |
|   }
 | |
| 
 | |
|   defaultValue() {
 | |
|     return this._prompt.ui.loginTextbox.value;
 | |
|   }
 | |
| 
 | |
|   accept(promptValue) {
 | |
|     if (typeof promptValue === 'string' && this._type === 'prompt')
 | |
|       this._prompt.ui.loginTextbox.value = promptValue;
 | |
|     this._prompt.ui.button0.click();
 | |
|   }
 | |
| }
 | |
| 
 | |
| var EXPORTED_SYMBOLS = ['PageHandler'];
 | |
| this.PageHandler = PageHandler;
 |