summaryrefslogtreecommitdiffstats
path: root/browser/components/urlbar/private/BlockedSuggestions.sys.mjs
blob: d74a0979d10fa4917eddfa15e12075b2e9ba724c (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
179
180
181
182
183
184
185
186
187
/* 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 { BaseFeature } from "resource:///modules/urlbar/private/BaseFeature.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  TaskQueue: "resource:///modules/UrlbarUtils.sys.mjs",
  UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
});

/**
 * A set of blocked suggestions for quick suggest.
 */
export class BlockedSuggestions extends BaseFeature {
  constructor() {
    super();
    this.#taskQueue = new lazy.TaskQueue();
    lazy.UrlbarPrefs.addObserver(this);
  }

  get shouldEnable() {
    // Return true so that we'll always load blocked digests when quick suggest
    // is enabled, even if blocking new suggestions is currently disabled.
    // Blocking may have been enabled previously, and blocked suggestions should
    // remain blocked as long as quick suggest as a whole remains enabled.
    return true;
  }

  enable(enabled) {
    if (enabled) {
      this.#loadDigests();
    }
  }

  /**
   * Blocks a suggestion.
   *
   * @param {string} originalUrl
   *   The suggestion's original URL with its unreplaced timestamp template.
   */
  async add(originalUrl) {
    this.logger.debug(`Queueing add: ${originalUrl}`);
    await this.#taskQueue.queue(async () => {
      this.logger.info(`Blocking suggestion: ${originalUrl}`);
      let digest = await this.#getDigest(originalUrl);
      this.logger.debug(`Got digest for '${originalUrl}': ${digest}`);
      this.#digests.add(digest);
      let json = JSON.stringify([...this.#digests]);
      this.#updatingDigests = true;
      try {
        lazy.UrlbarPrefs.set("quicksuggest.blockedDigests", json);
      } finally {
        this.#updatingDigests = false;
      }
      this.logger.debug(`All blocked suggestions: ${json}`);
    });
  }

  /**
   * Gets whether a suggestion is blocked.
   *
   * @param {string} originalUrl
   *   The suggestion's original URL with its unreplaced timestamp template.
   * @returns {boolean}
   *   Whether the suggestion is blocked.
   */
  async has(originalUrl) {
    this.logger.debug(`Queueing has: ${originalUrl}`);
    return this.#taskQueue.queue(async () => {
      this.logger.info(`Getting blocked status: ${originalUrl}`);
      let digest = await this.#getDigest(originalUrl);
      this.logger.debug(`Got digest for '${originalUrl}': ${digest}`);
      let isBlocked = this.#digests.has(digest);
      this.logger.info(`Blocked status for '${originalUrl}': ${isBlocked}`);
      return isBlocked;
    });
  }

  /**
   * Unblocks all suggestions.
   */
  async clear() {
    this.logger.debug(`Queueing clearBlockedSuggestions`);
    await this.#taskQueue.queue(() => {
      this.logger.info(`Clearing all blocked suggestions`);
      this.#digests.clear();
      lazy.UrlbarPrefs.clear("quicksuggest.blockedDigests");
    });
  }

  /**
   * Called when a urlbar pref changes.
   *
   * @param {string} pref
   *   The name of the pref relative to `browser.urlbar`.
   */
  onPrefChanged(pref) {
    switch (pref) {
      case "quicksuggest.blockedDigests":
        if (!this.#updatingDigests) {
          this.logger.info(
            "browser.urlbar.quicksuggest.blockedDigests changed"
          );
          this.#loadDigests();
        }
        break;
    }
  }

  /**
   * Loads blocked suggestion digests from the pref into `#digests`.
   */
  async #loadDigests() {
    this.logger.debug(`Queueing #loadDigests`);
    await this.#taskQueue.queue(() => {
      this.logger.info(`Loading blocked suggestion digests`);
      let json = lazy.UrlbarPrefs.get("quicksuggest.blockedDigests");
      this.logger.debug(
        `browser.urlbar.quicksuggest.blockedDigests value: ${json}`
      );
      if (!json) {
        this.logger.info(`There are no blocked suggestion digests`);
        this.#digests.clear();
      } else {
        try {
          this.#digests = new Set(JSON.parse(json));
          this.logger.info(`Successfully loaded blocked suggestion digests`);
        } catch (error) {
          this.logger.error(
            `Error loading blocked suggestion digests: ${error}`
          );
        }
      }
    });
  }

  /**
   * Returns the SHA-1 digest of a string as a 40-character hex-encoded string.
   *
   * @param {string} string
   *   The string to convert to SHA-1
   * @returns {string}
   *   The hex-encoded digest of the given string.
   */
  async #getDigest(string) {
    let stringArray = new TextEncoder().encode(string);
    let hashBuffer = await crypto.subtle.digest("SHA-1", stringArray);
    let hashArray = new Uint8Array(hashBuffer);
    return Array.from(hashArray, b => b.toString(16).padStart(2, "0")).join("");
  }

  get _test_readyPromise() {
    return this.#taskQueue.emptyPromise;
  }

  get _test_digests() {
    return this.#digests;
  }

  _test_getDigest(string) {
    return this.#getDigest(string);
  }

  // Set of digests of the original URLs of blocked suggestions. A suggestion's
  // "original URL" is its URL straight from the source with an unreplaced
  // timestamp template. For details on the digests, see `#getDigest()`.
  //
  // The only reason we use URL digests is that suggestions currently do not
  // have persistent IDs. We could use the URLs themselves but SHA-1 digests are
  // only 40 chars long, so they save a little space. This is also consistent
  // with how blocked tiles on the newtab page are stored, but they use MD5. We
  // do *not* store digests for any security or obfuscation reason.
  //
  // This value is serialized as a JSON'ed array to the
  // `browser.urlbar.quicksuggest.blockedDigests` pref.
  #digests = new Set();

  // Used to serialize access to blocked suggestions. This is only necessary
  // because getting a suggestion's URL digest is async.
  #taskQueue = null;

  // Whether blocked digests are currently being updated.
  #updatingDigests = false;
}