summaryrefslogtreecommitdiffstats
path: root/toolkit/components/cleardata/PrincipalsCollector.sys.mjs
blob: 2b5917c6ce44224d57988f929f3dbbbde05a352f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
/* 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/. */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "serviceWorkerManager",
  "@mozilla.org/serviceworkers/manager;1",
  "nsIServiceWorkerManager"
);

let logConsole;
function log(msg) {
  if (!logConsole) {
    logConsole = console.createInstance({
      prefix: "** PrincipalsCollector.jsm",
      maxLogLevelPref: "browser.sanitizer.loglevel",
    });
  }

  logConsole.log(msg);
}

/**
 * A helper module to collect all principals that have any of the following:
 *  * cookies
 *  * quota storage (indexedDB, localStorage)
 *  * service workers
 *
 *  Note that in case of cookies, because these are not strictly associated with a
 *  full origin (including scheme), the https:// scheme will be used as a convention,
 *  so when the cookie hostname is .example.com the principal will have the origin
 *  https://example.com. Origin Attributes from cookies are copied to the principal.
 *
 *  This class is not a singleton and needs to be instantiated using the constructor
 *  before usage. The class instance will cache the last list of principals.
 *
 *  There is currently no `refresh` method, though you are free to add one.
 */
export class PrincipalsCollector {
  // Indicating that we are in the process of collecting principals,
  // that might take some time
  #pendingCollection = null;
  /**
   * Creates a new PrincipalsCollector.
   */
  constructor() {
    this.principals = null;
  }

  /**
   * Checks whether the passed in principal has a scheme that is considered by the
   * PrincipalsCollector. This is used to avoid including principals for non-web
   * entities such as moz-extension.
   *
   * @param {nsIPrincipal} the principal to check
   * @returns {boolean}
   */
  static isSupportedPrincipal(principal) {
    return ["http", "https", "file"].some(scheme => principal.schemeIs(scheme));
  }

  /**
   * Fetches and collects all principals with cookies and/or site data (see module
   * description). Originally for usage in Sanitizer.jsm to compute principals to be
   * cleared on shutdown based on user settings.
   *
   * This operation might take a while to complete on big profiles.
   * DO NOT call or await this in a way that makes it block user interaction, or you
   * risk several painful seconds or possibly even minutes of lag.
   *
   * This function will cache its result and return the same list on second call,
   * even if the actual number of principals with cookies and site data changed.
   *
   * @param {Object} [optional] progress A Sanitizer.jsm progress object that will be
   *   updated to reflect the current step of fetching principals.
   * @returns {Array<nsIPrincipal>} the list of principals
   */
  async getAllPrincipals(progress = {}) {
    // Here is the list of principals with site data.
    if (this.principals) {
      return this.principals;
    }
    // Gathering all principals might take a while,
    // to ensure this process is only done once, we queue
    // the incomming calls here in case we are not yet done gathering principals
    if (!this.#pendingCollection) {
      this.#pendingCollection = this._getAllPrincipalsInternal(progress);
      this.principals = await this.#pendingCollection;
      this.#pendingCollection = null;
      return this.principals;
    }
    await this.#pendingCollection;
    return this.principals;
  }

  async _getAllPrincipalsInternal(progress = {}) {
    progress.step = "principals-quota-manager";
    let principals = await new Promise(resolve => {
      Services.qms.listOrigins().callback = request => {
        progress.step = "principals-quota-manager-listOrigins";
        if (request.resultCode != Cr.NS_OK) {
          // We are probably shutting down. We don't want to propagate the
          // error, rejecting the promise.
          resolve([]);
          return;
        }

        let principalsMap = new Map();
        for (const origin of request.result) {
          let principal =
            Services.scriptSecurityManager.createContentPrincipalFromOrigin(
              origin
            );
          if (PrincipalsCollector.isSupportedPrincipal(principal)) {
            principalsMap.set(principal.origin, principal);
          }
        }

        progress.step = "principals-quota-manager-completed";
        resolve(principalsMap);
      };
    }).catch(ex => {
      console.error("QuotaManagerService promise failed: ", ex);
      return [];
    });

    progress.step = "principals-service-workers";
    let serviceWorkers = lazy.serviceWorkerManager.getAllRegistrations();
    for (let i = 0; i < serviceWorkers.length; i++) {
      let sw = serviceWorkers.queryElementAt(
        i,
        Ci.nsIServiceWorkerRegistrationInfo
      );
      // We don't need to check the scheme. SW are just exposed to http/https URLs.
      principals.set(sw.principal.origin, sw.principal);
    }

    // Let's take the list of unique hosts+OA from cookies.
    progress.step = "principals-cookies";
    let cookies = Services.cookies.cookies;
    let hosts = new Set();
    for (let cookie of cookies) {
      hosts.add(
        cookie.rawHost +
          ChromeUtils.originAttributesToSuffix(cookie.originAttributes)
      );
    }

    progress.step = "principals-host-cookie";
    hosts.forEach(host => {
      // Cookies and permissions are handled by origin/host. Doesn't matter if we
      // use http: or https: schema here.
      let principal;
      try {
        principal =
          Services.scriptSecurityManager.createContentPrincipalFromOrigin(
            "https://" + host
          );
      } catch (e) {
        log(
          `ERROR: Could not create content principal for host '${host}' ${e.message}`
        );
      }
      if (principal) {
        principals.set(principal.origin, principal);
      }
    });

    principals = Array.from(principals.values());
    progress.step = "total-principals:" + principals.length;
    return principals;
  }
}