/* 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/. */ /* globals openAccountSetupTabWithAccount, openAccountProvisionerTab */ /** * This object takes care of intercepting page loads and creating the * corresponding account if the page load turns out to be a text/xml file from * one of our account providers. */ var { XPCOMUtils } = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ); var { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); var { JXON } = ChromeUtils.import("resource:///modules/JXON.jsm"); /** * This is an observer that watches all HTTP requests for one where the * response contentType contains text/xml. Once that observation is * made, we ensure that the associated window for that request matches * the window belonging to the content tab for the account order form. * If so, we attach an nsITraceableListener to read the contents of the * request response, and react accordingly if the contents can be turned * into an email account. * * @param aBrowser The XUL the request lives in. * @param aParams An object containing various bits of information. * @param aParams.realName The real name of the person * @param aParams.email The email address the person picked. * @param aParams.searchEngine The search engine associated to that provider. */ function httpRequestObserver(aBrowser, aParams) { this.browser = aBrowser; this.params = aParams; } httpRequestObserver.prototype = { observe(aSubject, aTopic, aData) { if ( aTopic != "http-on-examine-response" && aTopic != "http-on-examine-cached-response" ) { return; } if (!(aSubject instanceof Ci.nsIHttpChannel)) { console.error( "Failed to get a nsIHttpChannel when " + "observing http-on-examine-response" ); return; } // Helper function to get header values. let getHttpHeader = (httpChannel, header) => { // getResponseHeader throws when header is not set. try { return httpChannel.getResponseHeader(header); } catch (e) { return null; } }; let contentType = getHttpHeader(aSubject, "Content-Type"); if (!contentType || !contentType.toLowerCase().startsWith("text/xml")) { return; } // It's possible the account information changed during the setup at the // provider. Check some headers and set them if needed. let name = getHttpHeader(aSubject, "x-thunderbird-account-name"); if (name) { this.params.realName = name; } let email = getHttpHeader(aSubject, "x-thunderbird-account-email"); if (email) { this.params.email = email; } let requestWindow = this._getWindowForRequest(aSubject); if (!requestWindow || requestWindow !== this.browser.innerWindowID) { return; } // Ok, we've got a request that looks like a decent candidate. // Let's attach our TracingListener. if (aSubject instanceof Ci.nsITraceableChannel) { let newListener = new TracingListener(this.browser, this.params); newListener.oldListener = aSubject.setNewListener(newListener); } }, /** * _getWindowForRequest is an internal function that takes an nsIRequest, * and returns the associated window for that request. If it cannot find * an associated window, the function returns null. On exception, the * exception message is logged to the Error Console and null is returned. * * @param aRequest the nsIRequest to analyze */ _getWindowForRequest(aRequest) { try { if (aRequest && aRequest.notificationCallbacks) { return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext) .currentWindowContext.innerWindowId; } if ( aRequest && aRequest.loadGroup && aRequest.loadGroup.notificationCallbacks ) { return aRequest.loadGroup.notificationCallbacks.getInterface( Ci.nsILoadContext ).currentWindowContext.innerWindowId; } } catch (e) { console.error( "Could not find an associated window " + "for an HTTP request. Error: " + e ); } return null; }, QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), }; /** * TracingListener is an nsITracableChannel implementation that copies * an incoming stream of data from a request. The data flows through this * nsITracableChannel transparently to the original listener. Once the * response data is fully downloaded, an attempt is made to parse it * as XML, and derive email account data from it. * * @param aBrowser The XUL the request lives in. * @param aParams An object containing various bits of information. * @param aParams.realName The real name of the person * @param aParams.email The email address the person picked. * @param aParams.searchEngine The search engine associated to that provider. */ function TracingListener(aBrowser, aParams) { this.chunks = []; this.browser = aBrowser; this.params = aParams; this.oldListener = null; } TracingListener.prototype = { onStartRequest(/* nsIRequest */ aRequest) { this.oldListener.onStartRequest(aRequest); }, onStopRequest(/* nsIRequest */ aRequest, /* int */ aStatusCode) { const { CreateInBackend } = ChromeUtils.import( "resource:///modules/accountcreation/CreateInBackend.jsm" ); const { readFromXML } = ChromeUtils.import( "resource:///modules/accountcreation/readFromXML.jsm" ); const { AccountConfig } = ChromeUtils.import( "resource:///modules/accountcreation/AccountConfig.jsm" ); let newAccount; try { // Construct the downloaded data (we'll assume UTF-8 bytes) into XML. let xml = this.chunks.join(""); let bytes = new Uint8Array(xml.length); for (let i = 0; i < xml.length; i++) { bytes[i] = xml.charCodeAt(i); } xml = new TextDecoder().decode(bytes); // Attempt to derive email account information. let domParser = new DOMParser(); let accountConfig = readFromXML( JXON.build(domParser.parseFromString(xml, "text/xml")) ); AccountConfig.replaceVariables( accountConfig, this.params.realName, this.params.email ); let host = aRequest.getRequestHeader("Host"); let providerHostname = new URL("http://" + host).hostname; // Collect telemetry on which provider the new address was purchased from. Services.telemetry.keyedScalarAdd( "tb.account.new_account_from_provisioner", providerHostname, 1 ); // Create the new account in the back end. newAccount = CreateInBackend.createAccountInBackend(accountConfig); let tabmail = document.getElementById("tabmail"); // Find the tab associated with this browser, and close it. let myTabInfo = tabmail.tabInfo.filter( function (x) { return "browser" in x && x.browser == this.browser; }.bind(this) )[0]; tabmail.closeTab(myTabInfo); // Trigger the first login to download the folder structure and messages. newAccount.incomingServer.getNewMessages( newAccount.incomingServer.rootFolder, this._msgWindow, null ); } catch (e) { // Something went wrong with account set up. Dump the error out to the // error console, reopen the account provisioner tab, and show an error // dialog to the user. console.error("Problem interpreting provider XML:" + e); openAccountProvisionerTab(); Services.prompt.alert(window, null, e); this.oldListener.onStopRequest(aRequest, aStatusCode); return; } // Open the account setup tab and show the success view or an error if we // weren't able to create the new account. openAccountSetupTabWithAccount( newAccount, this.params.realName, this.params.email ); this.oldListener.onStopRequest(aRequest, aStatusCode); }, onDataAvailable( /* nsIRequest */ aRequest, /* nsIInputStream */ aStream, /* int */ aOffset, /* int */ aCount ) { // We want to read the stream of incoming data, but we also want // to make sure it gets passed to the original listener. We do this // by passing the input stream through an nsIStorageStream, writing // the data to that stream, and passing it along to the next listener. let binaryInputStream = Cc[ "@mozilla.org/binaryinputstream;1" ].createInstance(Ci.nsIBinaryInputStream); let storageStream = Cc["@mozilla.org/storagestream;1"].createInstance( Ci.nsIStorageStream ); let outStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( Ci.nsIBinaryOutputStream ); binaryInputStream.setInputStream(aStream); // The segment size of 8192 is a little magical - more or less // copied from nsITraceableChannel example code strewn about the // web. storageStream.init(8192, aCount, null); outStream.setOutputStream(storageStream.getOutputStream(0)); let data = binaryInputStream.readBytes(aCount); this.chunks.push(data); outStream.writeBytes(data, aCount); this.oldListener.onDataAvailable( aRequest, storageStream.newInputStream(0), aOffset, aCount ); }, QueryInterface: ChromeUtils.generateQI([ "nsIStreamListener", "nsIRequestObserver", ]), };