tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

WindowsJumpLists.sys.mjs (16575B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      7 
      8 // Stop updating jumplists after some idle time.
      9 const IDLE_TIMEOUT_SECONDS = 5 * 60;
     10 
     11 // Prefs
     12 const PREF_TASKBAR_BRANCH = "browser.taskbar.lists.";
     13 const PREF_TASKBAR_ENABLED = "enabled";
     14 const PREF_TASKBAR_ITEMCOUNT = "maxListItemCount";
     15 const PREF_TASKBAR_FREQUENT = "frequent.enabled";
     16 const PREF_TASKBAR_RECENT = "recent.enabled";
     17 const PREF_TASKBAR_TASKS = "tasks.enabled";
     18 const PREF_TASKBAR_REFRESH = "refreshInSeconds";
     19 
     20 /**
     21 * Exports
     22 */
     23 
     24 const lazy = {};
     25 
     26 /**
     27 * Smart getters
     28 */
     29 
     30 ChromeUtils.defineLazyGetter(lazy, "_prefs", function () {
     31  return Services.prefs.getBranch(PREF_TASKBAR_BRANCH);
     32 });
     33 
     34 ChromeUtils.defineLazyGetter(lazy, "_stringBundle", function () {
     35  return Services.strings.createBundle(
     36    "chrome://browser/locale/taskbar.properties"
     37  );
     38 });
     39 
     40 ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
     41  return console.createInstance({
     42    prefix: "WindowsJumpLists",
     43    maxLogLevel: Services.prefs.getBoolPref("browser.taskbar.log", false)
     44      ? "Debug"
     45      : "Warn",
     46  });
     47 });
     48 
     49 XPCOMUtils.defineLazyServiceGetter(
     50  lazy,
     51  "_idle",
     52  "@mozilla.org/widget/useridleservice;1",
     53  Ci.nsIUserIdleService
     54 );
     55 XPCOMUtils.defineLazyServiceGetter(
     56  lazy,
     57  "_taskbarService",
     58  "@mozilla.org/windows-taskbar;1",
     59  Ci.nsIWinTaskbar
     60 );
     61 
     62 ChromeUtils.defineESModuleGetters(lazy, {
     63  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
     64  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     65 });
     66 
     67 /**
     68 * Global functions
     69 */
     70 
     71 function _getString(name) {
     72  return lazy._stringBundle.GetStringFromName(name);
     73 }
     74 
     75 // Task list configuration data object.
     76 
     77 var tasksCfg = [
     78  /**
     79   * Task configuration options: title, description, args, iconIndex, open, close.
     80   *
     81   * title       - Task title displayed in the list. (strings in the table are temp fillers.)
     82   * description - Tooltip description on the list item.
     83   * args        - Command line args to invoke the task.
     84   * iconIndex   - Optional win icon index into the main application for the
     85   *               list item.
     86   * open        - Boolean indicates if the command should be visible after the browser opens.
     87   * close       - Boolean indicates if the command should be visible after the browser closes.
     88   */
     89  // Open new tab
     90  {
     91    get title() {
     92      return _getString("taskbar.tasks.newTab.label");
     93    },
     94    get description() {
     95      return _getString("taskbar.tasks.newTab.description");
     96    },
     97    args: "-new-tab about:blank",
     98    iconIndex: 3, // New window icon
     99    open: true,
    100    close: true, // The jump list already has an app launch icon, but
    101    // we don't always update the list on shutdown.
    102    // Thus true for consistency.
    103  },
    104 
    105  // Open new window
    106  {
    107    get title() {
    108      return _getString("taskbar.tasks.newWindow.label");
    109    },
    110    get description() {
    111      return _getString("taskbar.tasks.newWindow.description");
    112    },
    113    args: "-browser",
    114    iconIndex: 2, // New tab icon
    115    open: true,
    116    close: true, // No point, but we don't always update the list on
    117    // shutdown. Thus true for consistency.
    118  },
    119 ];
    120 
    121 // Open new private window
    122 let privateWindowTask = {
    123  get title() {
    124    return _getString("taskbar.tasks.newPrivateWindow.label");
    125  },
    126  get description() {
    127    return _getString("taskbar.tasks.newPrivateWindow.description");
    128  },
    129  args: "-private-window",
    130  iconIndex: 4, // Private browsing mode icon
    131  open: true,
    132  close: true, // No point, but we don't always update the list on
    133  // shutdown. Thus true for consistency.
    134 };
    135 
    136 // Implementation
    137 
    138 var Builder = class {
    139  constructor(builder) {
    140    this._builder = builder;
    141    this._tasks = null;
    142    this._shuttingDown = false;
    143    // These are ultimately controlled by prefs, so we disable
    144    // everything until is read from there
    145    this._showTasks = false;
    146    this._showFrequent = false;
    147    this._showRecent = false;
    148    this._maxItemCount = 0;
    149    this._isBuilding = false;
    150  }
    151 
    152  refreshPrefs(showTasks, showFrequent, showRecent, maxItemCount) {
    153    this._showTasks = showTasks;
    154    this._showFrequent = showFrequent;
    155    this._showRecent = showRecent;
    156    this._maxItemCount = maxItemCount;
    157  }
    158 
    159  updateShutdownState(shuttingDown) {
    160    this._shuttingDown = shuttingDown;
    161  }
    162 
    163  delete() {
    164    delete this._builder;
    165  }
    166 
    167  /**
    168   * Constructs the tasks and recent history items to display in the JumpList,
    169   * and then sends those lists to the nsIJumpListBuilder to be written.
    170   *
    171   * @returns {Promise<undefined>}
    172   *   The Promise resolves once the JumpList has been written, and any
    173   *   items that the user remove from the recent history list have been
    174   *   removed from Places. The Promise may reject if any part of constructing
    175   *   the tasks or sending them to the builder thread failed.
    176   */
    177  async buildList() {
    178    if (!(this._builder instanceof Ci.nsIJumpListBuilder)) {
    179      console.error(
    180        "Expected nsIJumpListBuilder. The builder is of the wrong type."
    181      );
    182      return;
    183    }
    184 
    185    // anything to build?
    186    if (!this._showFrequent && !this._showRecent && !this._showTasks) {
    187      // don't leave the last list hanging on the taskbar.
    188      this._deleteActiveJumpList();
    189      return;
    190    }
    191 
    192    // Are we in the midst of building an earlier iteration of this list? If
    193    // so, bail out. Same if we're shutting down.
    194    if (this._isBuilding || this._shuttingDown) {
    195      return;
    196    }
    197 
    198    this._isBuilding = true;
    199 
    200    try {
    201      let removedURLs = await this._builder.checkForRemovals();
    202      if (removedURLs.length) {
    203        await this._clearHistory(removedURLs);
    204      }
    205 
    206      let selfPath = Services.dirsvc.get("XREExeF", Ci.nsIFile).path;
    207 
    208      let taskDescriptions = [];
    209 
    210      if (this._showTasks) {
    211        taskDescriptions = this._tasks.map(task => {
    212          return {
    213            title: task.title,
    214            description: task.description,
    215            path: selfPath,
    216            arguments: task.args,
    217            fallbackIconIndex: task.iconIndex,
    218          };
    219        });
    220      }
    221 
    222      let customTitle = "";
    223      let customDescriptions = [];
    224 
    225      if (this._showFrequent) {
    226        let conn = await lazy.PlacesUtils.promiseDBConnection();
    227        let rows = await conn.executeCached(
    228          "SELECT p.url, IFNULL(p.title, p.url) as title " +
    229            "FROM moz_places p WHERE p.hidden = 0 " +
    230            "AND EXISTS (" +
    231            "SELECT id FROM moz_historyvisits WHERE " +
    232            "place_id = p.id AND " +
    233            "visit_type NOT IN (" +
    234            "0, " +
    235            `${Ci.nsINavHistoryService.TRANSITION_EMBED}, ` +
    236            `${Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK}` +
    237            ")" +
    238            "LIMIT 1" +
    239            ") " +
    240            "ORDER BY p.visit_count DESC LIMIT :limit",
    241          {
    242            limit: this._maxItemCount,
    243          }
    244        );
    245 
    246        for (let row of rows) {
    247          let uri = Services.io.newURI(row.getResultByName("url"));
    248          let iconPath = "";
    249          try {
    250            iconPath = await this._builder.obtainAndCacheFaviconAsync(uri);
    251          } catch (e) {
    252            // obtainAndCacheFaviconAsync may throw NS_ERROR_NOT_AVAILABLE if
    253            // the icon doesn't yet exist on the disk, but has been requested.
    254            // It might also throw an exception if there was a problem fetching
    255            // the favicon from the database and writing it to the disk. Either
    256            // case is non-fatal, so we ignore them here.
    257            lazy.logConsole.warn("Failed to fetch favicon for ", uri.spec, e);
    258          }
    259 
    260          customDescriptions.push({
    261            title: row.getResultByName("title"),
    262            description: row.getResultByName("title"),
    263            path: selfPath,
    264            arguments: row.getResultByName("url"),
    265            fallbackIconIndex: 1,
    266            iconPath,
    267          });
    268        }
    269 
    270        customTitle = _getString("taskbar.frequent.label");
    271      }
    272 
    273      if (!this._shuttingDown) {
    274        await this._builder.populateJumpList(
    275          taskDescriptions,
    276          customTitle,
    277          customDescriptions
    278        );
    279      }
    280    } catch (e) {
    281      console.error("buildList failed: ", e);
    282    } finally {
    283      this._isBuilding = false;
    284    }
    285  }
    286 
    287  _deleteActiveJumpList() {
    288    this._builder.clearJumpList();
    289  }
    290 
    291  /**
    292   * Removes URLs from history in Places that the user has requested to clear
    293   * from their Jump List. We must do this before recomputing which history
    294   * to put into the Jump List, because if we ever include items that have
    295   * recently been removed, Windows will not allow us to proceed.
    296   * Please see
    297   * https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-icustomdestinationlist-beginlist
    298   * for more details.
    299   *
    300   * The returned Promise never rejects, but may report console errors in the
    301   * event of removal failure.
    302   *
    303   * @param {string[]} uriSpecsToRemove
    304   *   The URLs to be removed from Places history.
    305   * @returns {Promise<undefined>}
    306   */
    307  _clearHistory(uriSpecsToRemove) {
    308    let URIsToRemove = uriSpecsToRemove
    309      .map(spec => URL.parse(spec)?.URI)
    310      .filter(uri => !!uri);
    311 
    312    if (URIsToRemove.length) {
    313      return lazy.PlacesUtils.history.remove(URIsToRemove).catch(console.error);
    314    }
    315    return Promise.resolve();
    316  }
    317 };
    318 
    319 export var WinTaskbarJumpList = {
    320  // We build two separate jump lists -- one for the regular Firefox icon
    321  // and one for the Private Browsing icon
    322  _builder: null,
    323  _pbBuilder: null,
    324  _builtPb: false,
    325  // Is showing jump lists currently blocked, such as when waiting for the user
    326  // to interact with the preonboarding modal?
    327  _blocked: false,
    328  _shuttingDown: false,
    329 
    330  /**
    331   * Startup, shutdown, and update
    332   */
    333 
    334  startup: async function WTBJL_startup() {
    335    if (!lazy._taskbarService.available) {
    336      return;
    337    }
    338    // exit if initting the taskbar failed for some reason.
    339    if (!(await this._initTaskbar())) {
    340      return;
    341    }
    342 
    343    if (lazy.PrivateBrowsingUtils.enabled) {
    344      tasksCfg.push(privateWindowTask);
    345    }
    346    // Store our task list config data
    347    this._builder._tasks = tasksCfg;
    348    this._pbBuilder._tasks = tasksCfg;
    349 
    350    // retrieve taskbar related prefs.
    351    this._refreshPrefs();
    352 
    353    // observer for private browsing and our prefs branch
    354    this._initObs();
    355 
    356    // jump list refresh timer
    357    this._updateTimer();
    358 
    359    if (this._blocked) {
    360      this._builder._deleteActiveJumpList();
    361    }
    362  },
    363 
    364  update: function WTBJL_update() {
    365    // are we disabled via prefs or currently blocked? don't do anything!
    366    if (!this._enabled || this._blocked) {
    367      return;
    368    }
    369 
    370    if (this._shuttingDown) {
    371      return;
    372    }
    373 
    374    this._builder.buildList();
    375 
    376    // We only ever need to do this once because the private browsing window
    377    // jumplist only ever shows the static task list, which never changes,
    378    // so it doesn't need to be updated over time.
    379    if (!this._builtPb) {
    380      this._pbBuilder.buildList();
    381      this._builtPb = true;
    382    }
    383  },
    384 
    385  _shutdown: function WTBJL__shutdown() {
    386    this._builder.updateShutdownState(true);
    387    this._pbBuilder.updateShutdownState(true);
    388    this._shuttingDown = true;
    389    this._free();
    390  },
    391 
    392  /**
    393   * Prefs utilities
    394   */
    395 
    396  _refreshPrefs: function WTBJL__refreshPrefs() {
    397    this._enabled = lazy._prefs.getBoolPref(PREF_TASKBAR_ENABLED);
    398    var showTasks = lazy._prefs.getBoolPref(PREF_TASKBAR_TASKS);
    399    this._builder.refreshPrefs(
    400      showTasks,
    401      lazy._prefs.getBoolPref(PREF_TASKBAR_FREQUENT),
    402      lazy._prefs.getBoolPref(PREF_TASKBAR_RECENT),
    403      lazy._prefs.getIntPref(PREF_TASKBAR_ITEMCOUNT)
    404    );
    405    // showTasks is the only relevant pref for the Private Browsing Jump List
    406    // the others are are related to frequent/recent entries, which are
    407    // explicitly disabled for it
    408    this._pbBuilder.refreshPrefs(showTasks, false, false, 0);
    409  },
    410 
    411  /**
    412   * Init and shutdown utilities
    413   */
    414 
    415  _initTaskbar: async function WTBJL__initTaskbar() {
    416    let builder;
    417    let pbBuilder;
    418 
    419    builder = lazy._taskbarService.createJumpListBuilder(false);
    420    pbBuilder = lazy._taskbarService.createJumpListBuilder(true);
    421    if (!builder || !pbBuilder) {
    422      return false;
    423    }
    424    let [builderAvailable, pbBuilderAvailable] = await Promise.all([
    425      builder.isAvailable(),
    426      pbBuilder.isAvailable(),
    427    ]);
    428    if (!builderAvailable || !pbBuilderAvailable) {
    429      return false;
    430    }
    431 
    432    this._builder = new Builder(builder);
    433    this._pbBuilder = new Builder(pbBuilder);
    434 
    435    return true;
    436  },
    437 
    438  _initObs: function WTBJL__initObs() {
    439    // If the browser is closed while in private browsing mode, the "exit"
    440    // notification is fired on quit-application-granted.
    441    // History cleanup can happen at profile-change-teardown.
    442    Services.obs.addObserver(this, "profile-before-change");
    443    Services.obs.addObserver(this, "browser:purge-session-history");
    444    lazy._prefs.addObserver("", this);
    445    this._placesObserver = new PlacesWeakCallbackWrapper(
    446      this.update.bind(this)
    447    );
    448    lazy.PlacesUtils.observers.addListener(
    449      ["history-cleared"],
    450      this._placesObserver
    451    );
    452  },
    453 
    454  _freeObs: function WTBJL__freeObs() {
    455    Services.obs.removeObserver(this, "profile-before-change");
    456    Services.obs.removeObserver(this, "browser:purge-session-history");
    457    lazy._prefs.removeObserver("", this);
    458    if (this._placesObserver) {
    459      lazy.PlacesUtils.observers.removeListener(
    460        ["history-cleared"],
    461        this._placesObserver
    462      );
    463    }
    464  },
    465 
    466  _updateTimer: function WTBJL__updateTimer() {
    467    if (this._enabled && !this._shuttingDown && !this._timer) {
    468      this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    469      this._timer.initWithCallback(
    470        this,
    471        lazy._prefs.getIntPref(PREF_TASKBAR_REFRESH) * 1000,
    472        this._timer.TYPE_REPEATING_SLACK
    473      );
    474    } else if ((!this._enabled || this._shuttingDown) && this._timer) {
    475      this._timer.cancel();
    476      delete this._timer;
    477    }
    478  },
    479 
    480  _hasIdleObserver: false,
    481  _updateIdleObserver: function WTBJL__updateIdleObserver() {
    482    if (this._enabled && !this._shuttingDown && !this._hasIdleObserver) {
    483      lazy._idle.addIdleObserver(this, IDLE_TIMEOUT_SECONDS);
    484      this._hasIdleObserver = true;
    485    } else if (
    486      (!this._enabled || this._shuttingDown) &&
    487      this._hasIdleObserver
    488    ) {
    489      lazy._idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
    490      this._hasIdleObserver = false;
    491    }
    492  },
    493 
    494  _free: function WTBJL__free() {
    495    this._freeObs();
    496    this._updateTimer();
    497    this._updateIdleObserver();
    498    this._builder.delete();
    499    this._pbBuilder.delete();
    500  },
    501 
    502  QueryInterface: ChromeUtils.generateQI([
    503    "nsINamed",
    504    "nsIObserver",
    505    "nsITimerCallback",
    506  ]),
    507 
    508  name: "WinTaskbarJumpList",
    509 
    510  blockJumpList: async function WTBJL_clearJumpList(unblockPromise) {
    511    this._blocked = true;
    512    if (unblockPromise) {
    513      try {
    514        await unblockPromise;
    515      } catch (e) {
    516        console.error("Unblock promise error, reinstating jump list: ", e);
    517      }
    518    }
    519    this._unblockJumpList();
    520  },
    521 
    522  _unblockJumpList: function WTBJL_updateJumpList() {
    523    this._blocked = false;
    524    this.update();
    525  },
    526 
    527  notify: function WTBJL_notify() {
    528    // Add idle observer on the first notification so it doesn't hit startup.
    529    this._updateIdleObserver();
    530    Services.tm.idleDispatchToMainThread(() => {
    531      this.update();
    532    });
    533  },
    534 
    535  observe: function WTBJL_observe(aSubject, aTopic) {
    536    switch (aTopic) {
    537      case "nsPref:changed":
    538        if (this._enabled && !lazy._prefs.getBoolPref(PREF_TASKBAR_ENABLED)) {
    539          this._builder._deleteActiveJumpList();
    540        }
    541        this._refreshPrefs();
    542        this._updateTimer();
    543        this._updateIdleObserver();
    544        Services.tm.idleDispatchToMainThread(() => {
    545          this.update();
    546        });
    547        break;
    548 
    549      case "profile-before-change":
    550        this._shutdown();
    551        break;
    552 
    553      case "browser:purge-session-history":
    554        this.update();
    555        break;
    556      case "idle":
    557        if (this._timer) {
    558          this._timer.cancel();
    559          delete this._timer;
    560        }
    561        break;
    562 
    563      case "active":
    564        this._updateTimer();
    565        break;
    566    }
    567  },
    568 };