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