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
5"use strict";
6
7const EXPORTED_SYMBOLS = ["AboutWelcomeParent"];
8const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
9
10const { XPCOMUtils } = ChromeUtils.import(
11  "resource://gre/modules/XPCOMUtils.jsm"
12);
13
14XPCOMUtils.defineLazyModuleGetters(this, {
15  FxAccounts: "resource://gre/modules/FxAccounts.jsm",
16  MigrationUtils: "resource:///modules/MigrationUtils.jsm",
17  OS: "resource://gre/modules/osfile.jsm",
18  SpecialMessageActions:
19    "resource://messaging-system/lib/SpecialMessageActions.jsm",
20  AboutWelcomeTelemetry:
21    "resource://activity-stream/aboutwelcome/lib/AboutWelcomeTelemetry.jsm",
22});
23
24XPCOMUtils.defineLazyGetter(this, "log", () => {
25  const { Logger } = ChromeUtils.import(
26    "resource://messaging-system/lib/Logger.jsm"
27  );
28  return new Logger("AboutWelcomeParent");
29});
30
31XPCOMUtils.defineLazyGetter(
32  this,
33  "Telemetry",
34  () => new AboutWelcomeTelemetry()
35);
36
37const DID_SEE_ABOUT_WELCOME_PREF = "trailhead.firstrun.didSeeAboutWelcome";
38const AWTerminate = {
39  UNKNOWN: "unknown",
40  WINDOW_CLOSED: "welcome-window-closed",
41  TAB_CLOSED: "welcome-tab-closed",
42  APP_SHUT_DOWN: "app-shut-down",
43  ADDRESS_BAR_NAVIGATED: "address-bar-navigated",
44};
45
46async function getImportableSites() {
47  const sites = [];
48
49  // Just handle these chromium-based browsers for now
50  for (const browserId of ["chrome", "chromium-edge", "chromium"]) {
51    // Skip if there's no profile data.
52    const migrator = await MigrationUtils.getMigrator(browserId);
53    if (!migrator) {
54      continue;
55    }
56
57    // Check each profile for top sites
58    const dataPath = await migrator.wrappedJSObject._getChromeUserDataPathIfExists();
59    for (const profile of await migrator.getSourceProfiles()) {
60      let path = OS.Path.join(dataPath, profile.id, "Top Sites");
61      // Skip if top sites data is missing
62      if (!(await OS.File.exists(path))) {
63        Cu.reportError(`Missing file at ${path}`);
64        continue;
65      }
66
67      try {
68        for (const row of await MigrationUtils.getRowsFromDBWithoutLocks(
69          path,
70          `Importable ${browserId} top sites`,
71          `SELECT url
72           FROM top_sites
73           ORDER BY url_rank`
74        )) {
75          sites.push(row.getString(0));
76        }
77      } catch (ex) {
78        Cu.reportError(
79          `Failed to get importable top sites from ${browserId} ${ex}`
80        );
81      }
82    }
83  }
84  return sites;
85}
86
87class AboutWelcomeObserver {
88  constructor() {
89    Services.obs.addObserver(this, "quit-application");
90
91    this.win = Services.focus.activeWindow;
92    if (!this.win) {
93      return;
94    }
95
96    this.terminateReason = AWTerminate.UNKNOWN;
97
98    this.onWindowClose = () => {
99      this.terminateReason = AWTerminate.WINDOW_CLOSED;
100    };
101
102    this.onTabClose = () => {
103      this.terminateReason = AWTerminate.TAB_CLOSED;
104    };
105
106    this.win.addEventListener("TabClose", this.onTabClose, { once: true });
107    this.win.addEventListener("unload", this.onWindowClose, { once: true });
108  }
109
110  observe(aSubject, aTopic, aData) {
111    switch (aTopic) {
112      case "quit-application":
113        this.terminateReason = AWTerminate.APP_SHUT_DOWN;
114        break;
115    }
116  }
117
118  // Added for testing
119  get AWTerminate() {
120    return AWTerminate;
121  }
122
123  stop() {
124    log.debug(`Terminate reason is ${this.terminateReason}`);
125    Services.obs.removeObserver(this, "quit-application");
126    if (!this.win) {
127      return;
128    }
129    this.win.removeEventListener("TabClose", this.onTabClose);
130    this.win.removeEventListener("unload", this.onWindowClose);
131    this.win = null;
132  }
133}
134
135class AboutWelcomeParent extends JSWindowActorParent {
136  constructor() {
137    super();
138    this.AboutWelcomeObserver = new AboutWelcomeObserver(this);
139  }
140
141  didDestroy() {
142    if (this.AboutWelcomeObserver) {
143      this.AboutWelcomeObserver.stop();
144    }
145
146    Telemetry.sendTelemetry({
147      event: "SESSION_END",
148      event_context: {
149        reason: this.AboutWelcomeObserver.terminateReason,
150        page: "about:welcome",
151      },
152      message_id: this.AWMessageId,
153      id: "ABOUT_WELCOME",
154    });
155  }
156
157  /**
158   * Handle messages from AboutWelcomeChild.jsm
159   *
160   * @param {string} type
161   * @param {any=} data
162   * @param {Browser} browser
163   * @param {Window} window
164   */
165  onContentMessage(type, data, browser, window) {
166    log.debug(`Received content event: ${type}`);
167    switch (type) {
168      case "AWPage:SET_WELCOME_MESSAGE_SEEN":
169        this.AWMessageId = data;
170        try {
171          Services.prefs.setBoolPref(DID_SEE_ABOUT_WELCOME_PREF, true);
172        } catch (e) {
173          log.debug(`Fails to set ${DID_SEE_ABOUT_WELCOME_PREF}.`);
174        }
175        break;
176      case "AWPage:SPECIAL_ACTION":
177        SpecialMessageActions.handleAction(data, browser);
178        break;
179      case "AWPage:FXA_METRICS_FLOW_URI":
180        return FxAccounts.config.promiseMetricsFlowURI("aboutwelcome");
181      case "AWPage:IMPORTABLE_SITES":
182        return getImportableSites();
183      case "AWPage:TELEMETRY_EVENT":
184        Telemetry.sendTelemetry(data);
185        break;
186      case "AWPage:LOCATION_CHANGED":
187        this.AboutWelcomeObserver.terminateReason =
188          AWTerminate.ADDRESS_BAR_NAVIGATED;
189        break;
190      case "AWPage:WAIT_FOR_MIGRATION_CLOSE":
191        return new Promise(resolve =>
192          Services.ww.registerNotification(function observer(subject, topic) {
193            if (
194              topic === "domwindowclosed" &&
195              subject.document.documentURI ===
196                "chrome://browser/content/migration/migration.xhtml"
197            ) {
198              Services.ww.unregisterNotification(observer);
199              resolve();
200            }
201          })
202        );
203      default:
204        log.debug(`Unexpected event ${type} was not handled.`);
205    }
206
207    return undefined;
208  }
209
210  /**
211   * @param {{name: string, data?: any}} message
212   * @override
213   */
214  receiveMessage(message) {
215    const { name, data } = message;
216    let browser;
217    let window;
218
219    if (this.manager.rootFrameLoader) {
220      browser = this.manager.rootFrameLoader.ownerElement;
221      window = browser.ownerGlobal;
222      return this.onContentMessage(name, data, browser, window);
223    }
224
225    log.warn(`Not handling ${name} because the browser doesn't exist.`);
226    return null;
227  }
228}
229