Bug 1700051: part 35) Reduce accessibility of `mSoftText.mDOMMapping` to `private...
[gecko.git] / services / automation / ServicesAutomation.jsm
blob52c734270bb6644e5cb324fad448750c94d18f8f
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 "use strict";
6 /*
7  * This module is used in automation to connect the browser to
8  * a specific FxA account and trigger FX Sync.
9  *
10  * To use it, you can call this sequence:
11  *
12  *    initConfig("https://accounts.stage.mozaws.net");
13  *    await Authentication.signIn(username, password);
14  *    await Sync.triggerSync();
15  *    await Authentication.signOut();
16  *
17  *
18  * Where username is your FxA e-mail. it will connect your browser
19  * to that account and trigger a Sync (on stage servers.)
20  *
21  * You can also use the convenience function that does everything:
22  *
23  *    await triggerSync(username, password, "https://accounts.stage.mozaws.net");
24  *
25  */
26 var EXPORTED_SYMBOLS = ["Sync", "Authentication", "initConfig", "triggerSync"];
28 const { XPCOMUtils } = ChromeUtils.import(
29   "resource://gre/modules/XPCOMUtils.jsm"
32 XPCOMUtils.defineLazyModuleGetters(this, {
33   Services: "resource://gre/modules/Services.jsm",
34   Log: "resource://gre/modules/Log.jsm",
35   Weave: "resource://services-sync/main.js",
36   Svc: "resource://services-sync/util.js",
37   fxAccounts: "resource://gre/modules/FxAccounts.jsm",
38   FxAccountsClient: "resource://gre/modules/FxAccountsClient.jsm",
39   FxAccountsConfig: "resource://gre/modules/FxAccountsConfig.jsm",
40   OS: "resource://gre/modules/osfile.jsm",
41   setTimeout: "resource://gre/modules/Timer.jsm",
42   clearTimeout: "resource://gre/modules/Timer.jsm",
43 });
45 XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
47 const AUTOCONFIG_PREF = "identity.fxaccounts.autoconfig.uri";
50  * Log helpers.
51  */
52 var _LOG = [];
54 function LOG(msg, error) {
55   console.debug(msg);
56   _LOG.push(msg);
57   if (error) {
58     console.debug(JSON.stringify(error));
59     _LOG.push(JSON.stringify(error));
60   }
63 function dumpLogs() {
64   let res = _LOG.join("\n");
65   _LOG = [];
66   return res;
69 function promiseObserver(aEventName) {
70   LOG("wait for " + aEventName);
71   return new Promise(resolve => {
72     let handler = () => {
73       Svc.Obs.remove(aEventName, handler);
74       resolve();
75     };
76     let handlerTimeout = () => {
77       Svc.Obs.remove(aEventName, handler);
78       LOG("handler timed out " + aEventName);
79       resolve();
80     };
81     Svc.Obs.add(aEventName, handler);
82     setTimeout(handlerTimeout, 3000);
83   });
87  *  Authentication
88  *
89  *  Used to sign in an FxA account, takes care of
90  *  the e-mail verification flow.
91  *
92  *  Usage:
93  *
94  *    await Authentication.signIn(username, password);
95  */
96 var Authentication = {
97   async isLoggedIn() {
98     return !!(await this.getSignedInUser());
99   },
101   async isReady() {
102     let user = await this.getSignedInUser();
103     if (user) {
104       LOG("current user " + JSON.stringify(user));
105     }
106     return user && user.verified;
107   },
109   async getSignedInUser() {
110     try {
111       return await fxAccounts.getSignedInUser();
112     } catch (error) {
113       LOG("getSignedInUser() failed", error);
114       throw error;
115     }
116   },
118   async shortWaitForVerification(ms) {
119     LOG("shortWaitForVerification");
120     let userData = await this.getSignedInUser();
121     let timeoutID;
122     LOG("set a timeout");
123     let timeoutPromise = new Promise(resolve => {
124       timeoutID = setTimeout(() => {
125         LOG(`Warning: no verification after ${ms}ms.`);
126         resolve();
127       }, ms);
128     });
129     LOG("set a fxAccounts.whenVerified");
130     await Promise.race([
131       fxAccounts.whenVerified(userData).finally(() => clearTimeout(timeoutID)),
132       timeoutPromise,
133     ]);
134     LOG("done");
135     return this.isReady();
136   },
138   async _confirmUser(uri) {
139     LOG("Open new tab and load verification page");
140     let mainWindow = Services.wm.getMostRecentWindow("navigator:browser");
141     let newtab = mainWindow.gBrowser.addWebTab(uri);
142     let win = mainWindow.gBrowser.getBrowserForTab(newtab);
143     win.addEventListener("load", function(e) {
144       LOG("load");
145     });
147     win.addEventListener("loadstart", function(e) {
148       LOG("loadstart");
149     });
151     win.addEventListener("error", function(msg, url, lineNo, columnNo, error) {
152       var string = msg.toLowerCase();
153       var substring = "script error";
154       if (string.indexOf(substring) > -1) {
155         LOG("Script Error: See Browser Console for Detail");
156       } else {
157         var message = [
158           "Message: " + msg,
159           "URL: " + url,
160           "Line: " + lineNo,
161           "Column: " + columnNo,
162           "Error object: " + JSON.stringify(error),
163         ].join(" - ");
165         LOG(message);
166       }
167     });
169     LOG("wait for page to load");
170     await new Promise(resolve => {
171       let handlerTimeout = () => {
172         LOG("timed out ");
173         resolve();
174       };
175       var timer = setTimeout(handlerTimeout, 10000);
176       win.addEventListener("loadend", function() {
177         resolve();
178         clearTimeout(timer);
179       });
180     });
181     LOG("Page Loaded");
182     let didVerify = await this.shortWaitForVerification(10000);
183     LOG("remove tab");
184     mainWindow.gBrowser.removeTab(newtab);
185     return didVerify;
186   },
188   /*
189    * This whole verification process may be bypassed if the
190    * account is whitelisted.
191    */
192   async _completeVerification(username) {
193     LOG("Fetching mail (from restmail) for user " + username);
194     let restmailURI = `https://www.restmail.net/mail/${encodeURIComponent(
195       username
196     )}`;
197     let triedAlready = new Set();
198     const tries = 10;
199     const normalWait = 4000;
200     for (let i = 0; i < tries; ++i) {
201       let resp = await fetch(restmailURI);
202       let messages = await resp.json();
203       // Sort so that the most recent emails are first.
204       messages.sort((a, b) => new Date(b.receivedAt) - new Date(a.receivedAt));
205       for (let m of messages) {
206         // We look for a link that has a x-link that we haven't yet tried.
207         if (!m.headers["x-link"] || triedAlready.has(m.headers["x-link"])) {
208           continue;
209         }
210         if (!m.headers["x-verify-code"]) {
211           continue;
212         }
213         let confirmLink = m.headers["x-link"];
214         triedAlready.add(confirmLink);
215         LOG("Trying confirmation link " + confirmLink);
216         try {
217           if (await this._confirmUser(confirmLink)) {
218             LOG("confirmation done");
219             return true;
220           }
221           LOG("confirmation failed");
222         } catch (e) {
223           LOG(
224             "Warning: Failed to follow confirmation link: " +
225               Log.exceptionStr(e)
226           );
227         }
228       }
229       if (i === 0) {
230         // first time through after failing we'll do this.
231         LOG("resendVerificationEmail");
232         await fxAccounts.resendVerificationEmail();
233       }
234       if (await this.shortWaitForVerification(normalWait)) {
235         return true;
236       }
237     }
238     // One last try.
239     return this.shortWaitForVerification(normalWait);
240   },
242   async signIn(username, password) {
243     LOG("Login user: " + username);
244     try {
245       // Required here since we don't go through the real login page
246       LOG("Calling FxAccountsConfig.ensureConfigured");
247       await FxAccountsConfig.ensureConfigured();
248       let client = new FxAccountsClient();
249       LOG("Signing in");
250       let credentials = await client.signIn(username, password, true);
251       LOG("Signed in, setting up the signed user in fxAccounts");
252       await fxAccounts._internal.setSignedInUser(credentials);
254       // If the account is not whitelisted for tests, we need to verify it
255       if (!credentials.verified) {
256         LOG("We need to verify the account");
257         await this._completeVerification(username);
258       } else {
259         LOG("Credentials already verified");
260       }
261       return true;
262     } catch (error) {
263       LOG("signIn() failed", error);
264       throw error;
265     }
266   },
268   async signOut() {
269     if (await Authentication.isLoggedIn()) {
270       // Note: This will clean up the device ID.
271       await fxAccounts.signOut();
272     }
273   },
277  * Sync
279  * Used to trigger sync.
281  * usage:
283  *   await Sync.triggerSync();
284  */
285 var Sync = {
286   getSyncLogsDirectory() {
287     return OS.Path.join(OS.Constants.Path.profileDir, ...["weave", "logs"]);
288   },
290   async init() {
291     Svc.Obs.add("weave:service:sync:error", this);
292     Svc.Obs.add("weave:service:setup-complete", this);
293     Svc.Obs.add("weave:service:tracking-started", this);
294     // Delay the automatic sync operations, so we can trigger it manually
295     Weave.Svc.Prefs.set("scheduler.immediateInterval", 7200);
296     Weave.Svc.Prefs.set("scheduler.idleInterval", 7200);
297     Weave.Svc.Prefs.set("scheduler.activeInterval", 7200);
298     Weave.Svc.Prefs.set("syncThreshold", 10000000);
299     // Wipe all the logs
300     await this.wipeLogs();
301   },
303   observe(subject, topic, data) {
304     LOG("Event received " + topic);
305   },
307   async configureSync() {
308     // todo, enable all sync engines here
309     // the addon engine requires kinto creds...
310     LOG("configuring sync");
311     console.assert(await Authentication.isReady(), "You are not connected");
312     await Weave.Service.configure();
313     if (!Weave.Status.ready) {
314       await promiseObserver("weave:service:ready");
315     }
316     if (Weave.Service.locked) {
317       await promiseObserver("weave:service:resyncs-finished");
318     }
319   },
321   /*
322    * triggerSync() runs the whole process of Syncing.
323    *
324    * returns 1 on success, 0 on failure.
325    */
326   async triggerSync() {
327     if (!(await Authentication.isLoggedIn())) {
328       LOG("Not connected");
329       return 1;
330     }
331     await this.init();
332     let result = 1;
333     try {
334       await this.configureSync();
335       LOG("Triggering a sync");
336       await Weave.Service.sync();
338       // wait a second for things to settle...
339       await new Promise(resolve => setTimeout(resolve, 1000));
341       LOG("Sync done");
342       result = 0;
343     } catch (error) {
344       LOG("triggerSync() failed", error);
345     }
347     return result;
348   },
350   async wipeLogs() {
351     let outputDirectory = this.getSyncLogsDirectory();
352     if (!(await OS.File.exists(outputDirectory))) {
353       return;
354     }
355     LOG("Wiping existing Sync logs");
356     try {
357       let iterator = new OS.File.DirectoryIterator(outputDirectory);
358       await iterator.forEach(async entry => {
359         try {
360           await OS.File.remove(entry.path);
361         } catch (error) {
362           LOG("wipeLogs() could not remove " + entry.path, error);
363         }
364       });
365     } catch (error) {
366       LOG("wipeLogs() failed", error);
367     }
368   },
370   async getLogs() {
371     let outputDirectory = this.getSyncLogsDirectory();
372     let entries = [];
374     if (await OS.File.exists(outputDirectory)) {
375       // Iterate through the directory
376       let iterator = new OS.File.DirectoryIterator(outputDirectory);
378       await iterator.forEach(async entry => {
379         let info = await OS.File.stat(entry.path);
380         entries.push({
381           path: entry.path,
382           name: entry.name,
383           lastModificationDate: info.lastModificationDate,
384         });
385       });
386       entries.sort(function(a, b) {
387         return b.lastModificationDate - a.lastModificationDate;
388       });
389     }
391     const promises = entries.map(async entry => {
392       let content = await OS.File.read(entry.path, {
393         encoding: "utf-8",
394       });
395       return {
396         name: entry.name,
397         content,
398       };
399     });
400     return Promise.all(promises);
401   },
404 function initConfig(autoconfig) {
405   Services.prefs.setCharPref(AUTOCONFIG_PREF, autoconfig);
408 async function triggerSync(username, password, autoconfig) {
409   initConfig(autoconfig);
410   await Authentication.signIn(username, password);
411   var result = await Sync.triggerSync();
412   await Authentication.signOut();
413   var logs = {
414     sync: await Sync.getLogs(),
415     condprof: [
416       {
417         name: "console.txt",
418         content: dumpLogs(),
419       },
420     ],
421   };
422   return {
423     result,
424     logs,
425   };