Bug 1944416: Restore individual tabs from closed groups in closed windows r=dao,sessi...
[gecko.git] / browser / components / urlbar / UrlbarProviderInterventions.sys.mjs
blob97a7c809cd8e3eaa1bd446e43566cc1db9a5fc58
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/. */
5 import {
6   UrlbarProvider,
7   UrlbarUtils,
8 } from "resource:///modules/UrlbarUtils.sys.mjs";
10 const lazy = {};
12 ChromeUtils.defineESModuleGetters(lazy, {
13   AppUpdater: "resource://gre/modules/AppUpdater.sys.mjs",
14   BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
15   NLP: "resource://gre/modules/NLP.sys.mjs",
16   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
17   ResetProfile: "resource://gre/modules/ResetProfile.sys.mjs",
18   Sanitizer: "resource:///modules/Sanitizer.sys.mjs",
19   UrlbarProviderGlobalActions:
20     "resource:///modules/UrlbarProviderGlobalActions.sys.mjs",
21   UrlbarResult: "resource:///modules/UrlbarResult.sys.mjs",
22   UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
23 });
25 ChromeUtils.defineLazyGetter(lazy, "appUpdater", () => new lazy.AppUpdater());
27 // The possible tips to show.
28 const TIPS = {
29   NONE: "",
30   CLEAR: "intervention_clear",
31   REFRESH: "intervention_refresh",
33   // There's an update available, but the user's pref says we should ask them to
34   // download and apply it.
35   UPDATE_ASK: "intervention_update_ask",
37   // The updater is currently checking.  We don't actually show a tip for this,
38   // but we use it to tell whether we should wait for the check to complete in
39   // startQuery.  See startQuery for details.
40   UPDATE_CHECKING: "intervention_update_checking",
42   // The user's browser is up to date, but they triggered the update
43   // intervention. We show this special refresh intervention instead.
44   UPDATE_REFRESH: "intervention_update_refresh",
46   // There's an update and it's been downloaded and applied. The user needs to
47   // restart to finish.
48   UPDATE_RESTART: "intervention_update_restart",
50   // We can't update the browser or possibly even check for updates for some
51   // reason, so the user should download the latest version from the web.
52   UPDATE_WEB: "intervention_update_web",
55 const EN_LOCALE_MATCH = /^en(-.*)$/;
57 // The search "documents" corresponding to each tip type.
58 const DOCUMENTS = {
59   clear: [
60     "cache firefox",
61     "clear cache firefox",
62     "clear cache in firefox",
63     "clear cookies firefox",
64     "clear firefox cache",
65     "clear history firefox",
66     "cookies firefox",
67     "delete cookies firefox",
68     "delete history firefox",
69     "firefox cache",
70     "firefox clear cache",
71     "firefox clear cookies",
72     "firefox clear history",
73     "firefox cookie",
74     "firefox cookies",
75     "firefox delete cookies",
76     "firefox delete history",
77     "firefox history",
78     "firefox not loading pages",
79     "history firefox",
80     "how to clear cache",
81     "how to clear history",
82   ],
83   refresh: [
84     "firefox crashing",
85     "firefox keeps crashing",
86     "firefox not responding",
87     "firefox not working",
88     "firefox refresh",
89     "firefox slow",
90     "how to reset firefox",
91     "refresh firefox",
92     "reset firefox",
93   ],
94   update: [
95     "download firefox",
96     "download mozilla",
97     "firefox browser",
98     "firefox download",
99     "firefox for mac",
100     "firefox for windows",
101     "firefox free download",
102     "firefox install",
103     "firefox installer",
104     "firefox latest version",
105     "firefox mac",
106     "firefox quantum",
107     "firefox update",
108     "firefox version",
109     "firefox windows",
110     "get firefox",
111     "how to update firefox",
112     "install firefox",
113     "mozilla download",
114     "mozilla firefox 2019",
115     "mozilla firefox 2020",
116     "mozilla firefox download",
117     "mozilla firefox for mac",
118     "mozilla firefox for windows",
119     "mozilla firefox free download",
120     "mozilla firefox mac",
121     "mozilla firefox update",
122     "mozilla firefox windows",
123     "mozilla update",
124     "update firefox",
125     "update mozilla",
126     "www.firefox.com",
127   ],
130 // In order to determine whether we should show an update tip, we check for app
131 // updates, but only once per this time period.
132 const UPDATE_CHECK_PERIOD_MS = 12 * 60 * 60 * 1000; // 12 hours
135  * A node in the QueryScorer's phrase tree.
136  */
137 // eslint-disable-next-line no-shadow
138 class Node {
139   constructor(word) {
140     this.word = word;
141     this.documents = new Set();
142     this.childrenByWord = new Map();
143   }
147  * This class scores a query string against sets of phrases.  To refer to a
148  * single set of phrases, we borrow the term "document" from search engine
149  * terminology.  To use this class, first add your documents with `addDocument`,
150  * and then call `score` with a query string.  `score` returns a sorted array of
151  * document-score pairs.
153  * The scoring method is fairly simple and is based on Levenshtein edit
154  * distance.  Therefore, lower scores indicate a better match than higher
155  * scores.  In summary, a query matches a phrase if the query starts with the
156  * phrase.  So a query "firefox update foo bar" matches the phrase "firefox
157  * update" for example.  A query matches a document if it matches any phrase in
158  * the document.  The query and phrases are compared word for word, and we allow
159  * fuzzy matching by computing the Levenshtein edit distance in each comparison.
160  * The amount of fuzziness allowed is controlled with `distanceThreshold`.  If
161  * the distance in a comparison is greater than this threshold, then the phrase
162  * does not match the query.  The final score for a document is the minimum edit
163  * distance between its phrases and the query.
165  * As mentioned, `score` returns a sorted array of document-score pairs.  It's
166  * up to you to filter the array to exclude scores above a certain threshold, or
167  * to take the top scorer, etc.
168  */
169 export class QueryScorer {
170   /**
171    * @param {object} options
172    *   Constructor options.
173    * @param {number} [options.distanceThreshold]
174    *   Edit distances no larger than this value are considered matches.
175    * @param {Map} [options.variations]
176    *   For convenience, the scorer can augment documents by replacing certain
177    *   words with other words and phrases. This mechanism is called variations.
178    *   This keys of this map are words that should be replaced, and the values
179    *   are the replacement words or phrases.  For example, if you add a document
180    *   whose only phrase is "firefox update", normally the scorer will register
181    *   only this single phrase for the document.  However, if you pass the value
182    *   `new Map(["firefox", ["fire fox", "fox fire", "foxfire"]])` for this
183    *   parameter, it will register 4 total phrases for the document: "fire fox
184    *   update", "fox fire update", "foxfire update", and the original "firefox
185    *   update".
186    */
187   constructor({ distanceThreshold = 1, variations = new Map() } = {}) {
188     this._distanceThreshold = distanceThreshold;
189     this._variations = variations;
190     this._documents = new Set();
191     this._rootNode = new Node();
192   }
194   /**
195    * Adds a document to the scorer.
196    *
197    * @param {object} doc
198    *   The document.
199    * @param {string} doc.id
200    *   The document's ID.
201    * @param {Array} doc.phrases
202    *   The set of phrases in the document.  Each phrase should be a string.
203    */
204   addDocument(doc) {
205     this._documents.add(doc);
207     for (let phraseStr of doc.phrases) {
208       // Split the phrase and lowercase the words.
209       let phrase = phraseStr
210         .trim()
211         .split(/\s+/)
212         .map(word => word.toLocaleLowerCase());
214       // Build a phrase list that contains the original phrase plus its
215       // variations, if any.
216       let phrases = [phrase];
217       for (let [triggerWord, variations] of this._variations) {
218         let index = phrase.indexOf(triggerWord);
219         if (index >= 0) {
220           for (let variation of variations) {
221             let variationPhrase = Array.from(phrase);
222             variationPhrase.splice(index, 1, ...variation.split(/\s+/));
223             phrases.push(variationPhrase);
224           }
225         }
226       }
228       // Finally, add the phrases to the phrase tree.
229       for (let completedPhrase of phrases) {
230         this._buildPhraseTree(this._rootNode, doc, completedPhrase, 0);
231       }
232     }
233   }
235   /**
236    * Scores a query string against the documents in the scorer.
237    *
238    * @param {string} queryString
239    *   The query string to score.
240    * @returns {Array}
241    *   An array of objects: { document, score }.  Each element in the array is a
242    *   a document and its score against the query string.  The elements are
243    *   ordered by score from low to high.  Scores represent edit distance, so
244    *   lower scores are better.
245    */
246   score(queryString) {
247     let queryWords = queryString
248       .trim()
249       .split(/\s+/)
250       .map(word => word.toLocaleLowerCase());
251     let minDistanceByDoc = this._traverse({ queryWords });
252     let results = [];
253     for (let doc of this._documents) {
254       let distance = minDistanceByDoc.get(doc);
255       results.push({
256         document: doc,
257         score: distance === undefined ? Infinity : distance,
258       });
259     }
260     results.sort((a, b) => a.score - b.score);
261     return results;
262   }
264   /**
265    * Builds the phrase tree based on the current documents.
266    *
267    * The phrase tree lets us efficiently match queries against phrases.  Each
268    * path through the tree starting from the root and ending at a leaf
269    * represents a complete phrase in a document (or more than one document, if
270    * the same phrase is present in multiple documents).  Each node in the path
271    * represents a word in the phrase.  To match a query, we start at the root,
272    * and in the root we look up the query's first word.  If the word matches the
273    * first word of any phrase, then the root will have a child node representing
274    * that word, and we move on to the child node.  Then we look up the query's
275    * second word in the child node, and so on, until either a lookup fails or we
276    * reach a leaf node.
277    *
278    * @param {Node} node
279    *   The current node being visited.
280    * @param {object} doc
281    *   The document whose phrases are being added to the tree.
282    * @param {Array} phrase
283    *   The phrase to add to the tree.
284    * @param {number} wordIndex
285    *   The index in the phrase of the current word.
286    */
287   _buildPhraseTree(node, doc, phrase, wordIndex) {
288     if (phrase.length == wordIndex) {
289       // We're done with this phrase.
290       return;
291     }
293     let word = phrase[wordIndex].toLocaleLowerCase();
294     let child = node.childrenByWord.get(word);
295     if (!child) {
296       child = new Node(word);
297       node.childrenByWord.set(word, child);
298     }
299     child.documents.add(doc);
301     // Recurse with the next word in the phrase.
302     this._buildPhraseTree(child, doc, phrase, wordIndex + 1);
303   }
305   /**
306    * Traverses a path in the phrase tree in order to score a query.  See
307    * `_buildPhraseTree` for a description of how this works.
308    *
309    * @param {object} options
310    *   Options.
311    * @param {Array} options.queryWords
312    *   The query being scored, split into words.
313    * @param {Node} [options.node]
314    *   The node currently being visited.
315    * @param {Map} [options.minDistanceByDoc]
316    *   Keeps track of the minimum edit distance for each document as the
317    *   traversal continues.
318    * @param {number} [options.queryWordsIndex]
319    *   The current index in the query words array.
320    * @param {number} [options.phraseDistance]
321    *   The total edit distance between the query and the path in the tree that's
322    *   been traversed so far.
323    * @returns {Map} minDistanceByDoc
324    */
325   _traverse({
326     queryWords,
327     node = this._rootNode,
328     minDistanceByDoc = new Map(),
329     queryWordsIndex = 0,
330     phraseDistance = 0,
331   } = {}) {
332     if (!node.childrenByWord.size) {
333       // We reached a leaf node.  The query has matched a phrase.  If the query
334       // and the phrase have the same number of words, then queryWordsIndex ==
335       // queryWords.length also.  Otherwise the query contains more words than
336       // the phrase.  We still count that as a match.
337       for (let doc of node.documents) {
338         minDistanceByDoc.set(
339           doc,
340           Math.min(
341             phraseDistance,
342             minDistanceByDoc.has(doc) ? minDistanceByDoc.get(doc) : Infinity
343           )
344         );
345       }
346       return minDistanceByDoc;
347     }
349     if (queryWordsIndex == queryWords.length) {
350       // We exhausted all the words in the query but have not reached a leaf
351       // node.  No match; the query has matched a phrase(s) up to this point,
352       // but it doesn't have enough words.
353       return minDistanceByDoc;
354     }
356     // Compare each word in the node to the current query word.
357     let queryWord = queryWords[queryWordsIndex];
358     for (let [childWord, child] of node.childrenByWord) {
359       let distance = lazy.NLP.levenshtein(queryWord, childWord);
360       if (distance <= this._distanceThreshold) {
361         // The word represented by this child node matches the current query
362         // word.  Recurse into the child node.
363         this._traverse({
364           node: child,
365           queryWords,
366           queryWordsIndex: queryWordsIndex + 1,
367           phraseDistance: phraseDistance + distance,
368           minDistanceByDoc,
369         });
370       }
371       // Else, the path that continues at the child node can't possibly match
372       // the query, so don't recurse into it.
373     }
375     return minDistanceByDoc;
376   }
380  * Gets appropriate values for each tip's payload.
382  * @param {string} tip a value from the TIPS enum
383  * @returns {object} Properties to include in the payload
384  */
385 function getPayloadForTip(tip) {
386   const baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
387   switch (tip) {
388     case TIPS.CLEAR:
389       return {
390         titleL10n: { id: "intervention-clear-data" },
391         buttons: [{ l10n: { id: "intervention-clear-data-confirm" } }],
392         helpUrl: baseURL + "delete-browsing-search-download-history-firefox",
393       };
394     case TIPS.REFRESH:
395       return {
396         titleL10n: { id: "intervention-refresh-profile" },
397         buttons: [{ l10n: { id: "intervention-refresh-profile-confirm" } }],
398         helpUrl: baseURL + "refresh-firefox-reset-add-ons-and-settings",
399       };
400     case TIPS.UPDATE_ASK:
401       return {
402         titleL10n: { id: "intervention-update-ask" },
403         buttons: [{ l10n: { id: "intervention-update-ask-confirm" } }],
404         helpUrl: baseURL + "update-firefox-latest-release",
405       };
406     case TIPS.UPDATE_REFRESH:
407       return {
408         titleL10n: { id: "intervention-update-refresh" },
409         buttons: [{ l10n: { id: "intervention-update-refresh-confirm" } }],
410         helpUrl: baseURL + "refresh-firefox-reset-add-ons-and-settings",
411       };
412     case TIPS.UPDATE_RESTART:
413       return {
414         titleL10n: { id: "intervention-update-restart" },
415         buttons: [{ l10n: { id: "intervention-update-restart-confirm" } }],
416         helpUrl: baseURL + "update-firefox-latest-release",
417       };
418     case TIPS.UPDATE_WEB:
419       return {
420         titleL10n: { id: "intervention-update-web" },
421         buttons: [{ l10n: { id: "intervention-update-web-confirm" } }],
422         helpUrl: baseURL + "update-firefox-latest-release",
423       };
424     default:
425       throw new Error("Unknown TIP type.");
426   }
430  * A provider that returns actionable tip results when the user is performing
431  * a search related to those actions.
432  */
433 class ProviderInterventions extends UrlbarProvider {
434   constructor() {
435     super();
436     // The tip we should currently show.
437     this.currentTip = TIPS.NONE;
439     // This object is used to match the user's queries to tips.
440     ChromeUtils.defineLazyGetter(this, "queryScorer", () => {
441       let queryScorer = new QueryScorer({
442         variations: new Map([
443           // Recognize "fire fox", "fox fire", and "foxfire" as "firefox".
444           ["firefox", ["fire fox", "fox fire", "foxfire"]],
445           // Recognize "mozila" as "mozilla".  This will catch common mispellings
446           // "mozila", "mozzila", and "mozzilla" (among others) due to the edit
447           // distance threshold of 1.
448           ["mozilla", ["mozila"]],
449         ]),
450       });
451       for (let [id, phrases] of Object.entries(DOCUMENTS)) {
452         queryScorer.addDocument({ id, phrases });
453       }
454       return queryScorer;
455     });
456   }
458   /**
459    * Enum of the types of intervention tips.
460    *
461    * @returns {{ NONE: string; CLEAR: string; REFRESH: string; UPDATE_ASK: string; UPDATE_CHECKING: string; UPDATE_REFRESH: string; UPDATE_RESTART: string; UPDATE_WEB: string; }}
462    */
463   get TIP_TYPE() {
464     return TIPS;
465   }
467   /**
468    * Unique name for the provider, used by the context to filter on providers.
469    *
470    * @returns {string}
471    */
472   get name() {
473     return "UrlbarProviderInterventions";
474   }
476   /**
477    * The type of the provider, must be one of UrlbarUtils.PROVIDER_TYPE.
478    *
479    * @returns {UrlbarUtils.PROVIDER_TYPE}
480    */
481   get type() {
482     return UrlbarUtils.PROVIDER_TYPE.PROFILE;
483   }
485   /**
486    * Whether this provider should be invoked for the given context.
487    * If this method returns false, the providers manager won't start a query
488    * with this provider, to save on resources.
489    *
490    * @param {UrlbarQueryContext} queryContext The query context object
491    * @returns {boolean} Whether this provider should be invoked for the search.
492    */
493   isActive(queryContext) {
494     if (
495       !queryContext.searchString ||
496       queryContext.searchString.length > UrlbarUtils.MAX_TEXT_LENGTH ||
497       lazy.UrlbarTokenizer.REGEXP_LIKE_PROTOCOL.test(
498         queryContext.searchString
499       ) ||
500       !EN_LOCALE_MATCH.test(Services.locale.appLocaleAsBCP47) ||
501       !Services.policies.isAllowed("urlbarinterventions") ||
502       lazy.UrlbarProviderGlobalActions.isActive(queryContext)
503     ) {
504       return false;
505     }
507     this.currentTip = TIPS.NONE;
509     // Get the scores and the top score.
510     let docScores = this.queryScorer.score(queryContext.searchString);
511     let topDocScore = docScores[0];
513     // Multiple docs may have the top score, so collect them all.
514     let topDocIDs = new Set();
515     if (topDocScore.score != Infinity) {
516       for (let { score, document } of docScores) {
517         if (score != topDocScore.score) {
518           break;
519         }
520         topDocIDs.add(document.id);
521       }
522     }
524     // Determine the tip to show, if any. If there are multiple top-score docs,
525     // prefer them in the following order.
526     if (topDocIDs.has("update")) {
527       this._setCurrentTipFromAppUpdaterStatus();
528     } else if (topDocIDs.has("clear")) {
529       let window = lazy.BrowserWindowTracker.getTopWindow();
530       if (!lazy.PrivateBrowsingUtils.isWindowPrivate(window)) {
531         this.currentTip = TIPS.CLEAR;
532       }
533     } else if (topDocIDs.has("refresh")) {
534       // Note that the "update" case can set currentTip to TIPS.REFRESH too.
535       this.currentTip = TIPS.REFRESH;
536     }
538     return (
539       this.currentTip != TIPS.NONE &&
540       (this.currentTip != TIPS.REFRESH ||
541         Services.policies.isAllowed("profileRefresh"))
542     );
543   }
545   async _setCurrentTipFromAppUpdaterStatus() {
546     // The update tips depend on the app's update status, so check for updates
547     // now (if we haven't already checked within the update-check period).  If
548     // we're running in an xpcshell test, then checkForBrowserUpdate's attempt
549     // to use appUpdater will throw an exception because it won't be available.
550     // In that case, return false to disable the provider.
551     //
552     // This causes synchronous IO within the updater the first time it's called
553     // (at least) so be careful not to do it the first time the urlbar is used.
554     try {
555       this.checkForBrowserUpdate();
556     } catch (ex) {
557       return;
558     }
560     // There are several update tips. Figure out which one to show.
561     switch (lazy.appUpdater.status) {
562       case lazy.AppUpdater.STATUS.READY_FOR_RESTART:
563         // Prompt the user to restart.
564         this.currentTip = TIPS.UPDATE_RESTART;
565         break;
566       case lazy.AppUpdater.STATUS.DOWNLOAD_AND_INSTALL:
567         // There's an update available, but the user's pref says we should ask
568         // them to download and apply it.
569         this.currentTip = TIPS.UPDATE_ASK;
570         break;
571       case lazy.AppUpdater.STATUS.NO_UPDATES_FOUND:
572         // We show a special refresh tip when the browser is up to date.
573         this.currentTip = TIPS.UPDATE_REFRESH;
574         break;
575       case lazy.AppUpdater.STATUS.CHECKING:
576         // This will be the case the first time we check.  See startQuery for
577         // how this special tip is handled.
578         this.currentTip = TIPS.UPDATE_CHECKING;
579         break;
580       case lazy.AppUpdater.STATUS.NO_UPDATER:
581       case lazy.AppUpdater.STATUS.UPDATE_DISABLED_BY_POLICY:
582         // If the updater is disabled at build time or at runtime, either by
583         // policy or because we're in a package, do not select any update tips.
584         this.currentTip = TIPS.NONE;
585         break;
586       default:
587         // Give up and ask the user to download the latest version from the
588         // web. We default to this case when the update is still downloading
589         // because an update doesn't actually occur if the user were to
590         // restart the browser. See bug 1625241.
591         this.currentTip = TIPS.UPDATE_WEB;
592         break;
593     }
594   }
596   /**
597    * Starts querying.
598    *
599    * @param {UrlbarQueryContext} queryContext The query context object
600    * @param {Function} addCallback Callback invoked by the provider to add a new
601    *        result. A UrlbarResult should be passed to it.
602    */
603   async startQuery(queryContext, addCallback) {
604     let instance = this.queryInstance;
606     // TIPS.UPDATE_CHECKING is special, and we never actually show a tip that
607     // reflects a "checking" status.  Instead it's handled like this.  We call
608     // appUpdater.check() to start an update check.  If we haven't called it
609     // before, then when it returns, appUpdater.status will be
610     // AppUpdater.STATUS.CHECKING, and it will remain CHECKING until the check
611     // finishes.  We can add a listener to appUpdater to be notified when the
612     // check finishes.  We don't want to wait for it to finish in isActive
613     // because that would block other providers from adding their results, so
614     // instead we wait here in startQuery.  The results from other providers
615     // will be added while we're waiting.  When the check finishes, we call
616     // addCallback and add our result.  It doesn't matter how long the check
617     // takes because if another query starts, the view is closed, or the user
618     // changes the selection, the query will be canceled.
619     if (this.currentTip == TIPS.UPDATE_CHECKING) {
620       // First check the status because it may have changed between the time
621       // isActive was called and now.
622       this._setCurrentTipFromAppUpdaterStatus();
623       if (this.currentTip == TIPS.UPDATE_CHECKING) {
624         // The updater is still checking, so wait for it to finish.
625         await new Promise(resolve => {
626           this._appUpdaterListener = () => {
627             lazy.appUpdater.removeListener(this._appUpdaterListener);
628             delete this._appUpdaterListener;
629             resolve();
630           };
631           lazy.appUpdater.addListener(this._appUpdaterListener);
632         });
633         if (instance != this.queryInstance) {
634           // The query was canceled before the check finished.
635           return;
636         }
637         // Finally, set the tip from the updater status.  The updater should no
638         // longer be checking, but guard against it just in case by returning
639         // early.
640         this._setCurrentTipFromAppUpdaterStatus();
641         if (this.currentTip == TIPS.UPDATE_CHECKING) {
642           return;
643         }
644       }
645     }
646     // At this point, this.currentTip != TIPS.UPDATE_CHECKING because we
647     // returned early above if it was.
649     let result = new lazy.UrlbarResult(
650       UrlbarUtils.RESULT_TYPE.TIP,
651       UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL,
652       {
653         ...getPayloadForTip(this.currentTip),
654         type: this.currentTip,
655         icon: UrlbarUtils.ICON.TIP,
656         helpL10n: {
657           id: "urlbar-result-menu-tip-get-help",
658         },
659       }
660     );
661     result.suggestedIndex = 1;
662     addCallback(this, result);
663   }
665   /**
666    * Cancels a running query,
667    */
668   cancelQuery() {
669     // If we're waiting for appUpdater to finish its update check,
670     // this._appUpdaterListener will be defined.  We can stop listening now.
671     if (this._appUpdaterListener) {
672       lazy.appUpdater.removeListener(this._appUpdaterListener);
673       delete this._appUpdaterListener;
674     }
675   }
677   #pickResult(result, window) {
678     let tip = result.payload.type;
680     // Do the tip action.
681     switch (tip) {
682       case TIPS.CLEAR:
683         openClearHistoryDialog(window);
684         break;
685       case TIPS.REFRESH:
686       case TIPS.UPDATE_REFRESH:
687         resetBrowser(window);
688         break;
689       case TIPS.UPDATE_ASK:
690         installBrowserUpdateAndRestart();
691         break;
692       case TIPS.UPDATE_RESTART:
693         restartBrowser();
694         break;
695       case TIPS.UPDATE_WEB:
696         window.gBrowser.selectedTab = window.gBrowser.addWebTab(
697           "https://www.mozilla.org/firefox/new/"
698         );
699         break;
700     }
701   }
703   onEngagement(queryContext, controller, details) {
704     // `selType` is "tip" when the tip's main button is picked. Ignore clicks on
705     // the help command ("tiphelp"), which is handled by UrlbarInput since we
706     // set `helpUrl` on the result payload. Currently there aren't any other
707     // buttons or commands but this will ignore clicks on them too.
708     if (details.selType == "tip") {
709       this.#pickResult(details.result, controller.browserWindow);
710     }
711   }
713   /**
714    * Checks for app updates.
715    *
716    * @param {boolean} force If false, this only checks for updates if we haven't
717    *        already checked within the update-check period.  If true, we check
718    *        regardless.
719    */
720   checkForBrowserUpdate(force = false) {
721     if (
722       force ||
723       !this._lastUpdateCheckTime ||
724       Date.now() - this._lastUpdateCheckTime >= UPDATE_CHECK_PERIOD_MS
725     ) {
726       this._lastUpdateCheckTime = Date.now();
727       lazy.appUpdater.check();
728     }
729   }
731   /**
732    * Resets the provider's app updater state by making a new app updater.  This
733    * is intended to be used by tests.
734    */
735   resetAppUpdater() {
736     // Reset only if the object has already been initialized.
737     if (!Object.getOwnPropertyDescriptor(lazy, "appUpdater").get) {
738       lazy.appUpdater = new lazy.AppUpdater();
739     }
740   }
743 export var UrlbarProviderInterventions = new ProviderInterventions();
746  * Tip callbacks follow.
747  */
749 function installBrowserUpdateAndRestart() {
750   if (lazy.appUpdater.status != lazy.AppUpdater.STATUS.DOWNLOAD_AND_INSTALL) {
751     return Promise.resolve();
752   }
753   return new Promise(resolve => {
754     let listener = () => {
755       // Once we call allowUpdateDownload, there are two possible end
756       // states: DOWNLOAD_FAILED and READY_FOR_RESTART.
757       if (
758         lazy.appUpdater.status != lazy.AppUpdater.STATUS.READY_FOR_RESTART &&
759         lazy.appUpdater.status != lazy.AppUpdater.STATUS.DOWNLOAD_FAILED
760       ) {
761         return;
762       }
763       lazy.appUpdater.removeListener(listener);
764       if (lazy.appUpdater.status == lazy.AppUpdater.STATUS.READY_FOR_RESTART) {
765         restartBrowser();
766       }
767       resolve();
768     };
769     lazy.appUpdater.addListener(listener);
770     lazy.appUpdater.allowUpdateDownload();
771   });
774 function openClearHistoryDialog(window) {
775   // The behaviour of the Clear Recent History dialog in PBM does
776   // not have the expected effect (bug 463607).
777   if (lazy.PrivateBrowsingUtils.isWindowPrivate(window)) {
778     return;
779   }
780   lazy.Sanitizer.showUI(window);
783 function restartBrowser() {
784   // Notify all windows that an application quit has been requested.
785   let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
786     Ci.nsISupportsPRBool
787   );
788   Services.obs.notifyObservers(
789     cancelQuit,
790     "quit-application-requested",
791     "restart"
792   );
793   // Something aborted the quit process.
794   if (cancelQuit.data) {
795     return;
796   }
797   // If already in safe mode restart in safe mode.
798   if (Services.appinfo.inSafeMode) {
799     Services.startup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit);
800   } else {
801     Services.startup.quit(
802       Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart
803     );
804   }
807 function resetBrowser(window) {
808   if (!lazy.ResetProfile.resetSupported()) {
809     return;
810   }
811   lazy.ResetProfile.openConfirmationDialog(window);