tor-browser

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

aboutSessionRestore.js (12228B)


      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 "use strict";
      6 
      7 const { AppConstants } = ChromeUtils.importESModule(
      8  "resource://gre/modules/AppConstants.sys.mjs"
      9 );
     10 ChromeUtils.defineESModuleGetters(this, {
     11  PlacesUIUtils: "moz-src:///browser/components/places/PlacesUIUtils.sys.mjs",
     12  SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
     13 });
     14 
     15 var gStateObject;
     16 var gTreeData;
     17 var gTreeInitialized = false;
     18 
     19 // Page initialization
     20 
     21 window.onload = function () {
     22  let toggleTabs = document.getElementById("tabsToggle");
     23  if (toggleTabs) {
     24    let tabList = document.getElementById("tabList");
     25 
     26    let toggleHiddenTabs = () => {
     27      toggleTabs.classList.toggle("tabs-hidden");
     28      tabList.hidden = toggleTabs.classList.contains("tabs-hidden");
     29      initTreeView();
     30    };
     31    toggleTabs.onclick = toggleHiddenTabs;
     32  }
     33 
     34  // wire up click handlers for the radio buttons if they exist.
     35  for (let radioId of ["radioRestoreAll", "radioRestoreChoose"]) {
     36    let button = document.getElementById(radioId);
     37    if (button) {
     38      button.addEventListener("click", updateTabListVisibility);
     39    }
     40  }
     41 
     42  var tabListTree = document.getElementById("tabList");
     43  tabListTree.addEventListener("click", onListClick);
     44  tabListTree.addEventListener("keydown", onListKeyDown);
     45 
     46  var errorCancelButton = document.getElementById("errorCancel");
     47  // aboutSessionRestore.js is included aboutSessionRestore.xhtml
     48  // and aboutWelcomeBack.xhtml, but the latter does not have an
     49  // errorCancel button.
     50  if (errorCancelButton) {
     51    errorCancelButton.addEventListener("command", startNewSession);
     52  }
     53 
     54  var errorTryAgainButton = document.getElementById("errorTryAgain");
     55  errorTryAgainButton.addEventListener("command", restoreSession);
     56 
     57  // the crashed session state is kept inside a textbox so that SessionStore picks it up
     58  // (for when the tab is closed or the session crashes right again)
     59  var sessionData = document.getElementById("sessionData");
     60  if (!sessionData.value) {
     61    errorTryAgainButton.disabled = true;
     62    return;
     63  }
     64 
     65  gStateObject = JSON.parse(sessionData.value);
     66 
     67  // make sure the data is tracked to be restored in case of a subsequent crash
     68  var event = document.createEvent("UIEvents");
     69  event.initUIEvent("input", true, true, window, 0);
     70  sessionData.dispatchEvent(event);
     71 
     72  initTreeView();
     73 
     74  errorTryAgainButton.focus({ focusVisible: false });
     75 };
     76 
     77 function isTreeViewVisible() {
     78  return !document.getElementById("tabList").hidden;
     79 }
     80 
     81 async function initTreeView() {
     82  if (gTreeInitialized || !isTreeViewVisible()) {
     83    return;
     84  }
     85 
     86  var tabList = document.getElementById("tabList");
     87  let l10nIds = [];
     88  for (
     89    let labelIndex = 0;
     90    labelIndex < gStateObject.windows.length;
     91    labelIndex++
     92  ) {
     93    l10nIds.push({
     94      id: "restore-page-window-label",
     95      args: { windowNumber: labelIndex + 1 },
     96    });
     97  }
     98  let winLabels = await document.l10n.formatValues(l10nIds);
     99  gTreeData = [];
    100  gStateObject.windows.forEach(function (aWinData, aIx) {
    101    var winState = {
    102      label: winLabels[aIx],
    103      open: true,
    104      checked: true,
    105      ix: aIx,
    106    };
    107    winState.tabs = aWinData.tabs.map(function (aTabData) {
    108      var entry = aTabData.entries[aTabData.index - 1] || {
    109        url: "about:blank",
    110      };
    111      // don't initiate a connection just to fetch a favicon (see bug 462863)
    112      return {
    113        label: entry.title || entry.url,
    114        checked: true,
    115        src: PlacesUIUtils.getImageURL(aTabData.image),
    116        parent: winState,
    117      };
    118    });
    119    gTreeData.push(winState);
    120    for (let tab of winState.tabs) {
    121      gTreeData.push(tab);
    122    }
    123  }, this);
    124 
    125  tabList.view = treeView;
    126  tabList.view.selection.select(0);
    127  gTreeInitialized = true;
    128 }
    129 
    130 // User actions
    131 function updateTabListVisibility() {
    132  document.getElementById("tabList").hidden =
    133    !document.getElementById("radioRestoreChoose").checked;
    134  initTreeView();
    135 }
    136 
    137 function restoreSession() {
    138  Services.obs.notifyObservers(null, "sessionstore-initiating-manual-restore");
    139  document.getElementById("errorTryAgain").disabled = true;
    140 
    141  if (isTreeViewVisible()) {
    142    if (!gTreeData.some(aItem => aItem.checked)) {
    143      // This should only be possible when we have no "cancel" button, and thus
    144      // the "Restore session" button always remains enabled.  In that case and
    145      // when nothing is selected, we just want a new session.
    146      startNewSession();
    147      return;
    148    }
    149 
    150    // remove all unselected tabs from the state before restoring it
    151    var ix = gStateObject.windows.length - 1;
    152    for (var t = gTreeData.length - 1; t >= 0; t--) {
    153      if (treeView.isContainer(t)) {
    154        if (gTreeData[t].checked === 0) {
    155          // this window will be restored partially
    156          gStateObject.windows[ix].tabs = gStateObject.windows[ix].tabs.filter(
    157            (aTabData, aIx) => gTreeData[t].tabs[aIx].checked
    158          );
    159        } else if (!gTreeData[t].checked) {
    160          // this window won't be restored at all
    161          gStateObject.windows.splice(ix, 1);
    162        }
    163        ix--;
    164      }
    165    }
    166  }
    167  var stateString = JSON.stringify(gStateObject);
    168 
    169  var top = getBrowserWindow();
    170 
    171  // if there's only this page open, reuse the window for restoring the session
    172  if (top.gBrowser.tabs.length == 1) {
    173    SessionStore.setWindowState(top, stateString, true);
    174    return;
    175  }
    176 
    177  // restore the session into a new window and close the current tab
    178  var newWindow = top.openDialog(
    179    top.location,
    180    "_blank",
    181    "chrome,dialog=no,all"
    182  );
    183 
    184  Services.obs.addObserver(function observe(win, topic) {
    185    if (win != newWindow) {
    186      return;
    187    }
    188 
    189    Services.obs.removeObserver(observe, topic);
    190    SessionStore.setWindowState(newWindow, stateString, true);
    191 
    192    let tabbrowser = top.gBrowser;
    193    let browser = window.docShell.chromeEventHandler;
    194    let tab = tabbrowser.getTabForBrowser(browser);
    195    tabbrowser.removeTab(tab);
    196  }, "browser-delayed-startup-finished");
    197 }
    198 
    199 function startNewSession() {
    200  if (Services.prefs.getIntPref("browser.startup.page") == 0) {
    201    getBrowserWindow().gBrowser.loadURI(Services.io.newURI("about:blank"), {
    202      triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
    203        {}
    204      ),
    205    });
    206  } else {
    207    getBrowserWindow().BrowserCommands.home();
    208  }
    209 }
    210 
    211 function onListClick(aEvent) {
    212  // don't react to right-clicks
    213  if (aEvent.button == 2) {
    214    return;
    215  }
    216 
    217  var cell = treeView.treeBox.getCellAt(aEvent.clientX, aEvent.clientY);
    218  if (cell.col) {
    219    // Restore this specific tab in the same window for middle/double/accel clicking
    220    // on a tab's title.
    221    let accelKey =
    222      AppConstants.platform == "macosx" ? aEvent.metaKey : aEvent.ctrlKey;
    223    if (
    224      (aEvent.button == 1 ||
    225        (aEvent.button == 0 && aEvent.detail == 2) ||
    226        accelKey) &&
    227      cell.col.id == "title" &&
    228      !treeView.isContainer(cell.row)
    229    ) {
    230      restoreSingleTab(cell.row, aEvent.shiftKey);
    231      aEvent.stopPropagation();
    232    } else if (cell.col.id == "restore") {
    233      toggleRowChecked(cell.row);
    234    }
    235  }
    236 }
    237 
    238 function onListKeyDown(aEvent) {
    239  switch (aEvent.keyCode) {
    240    case KeyEvent.DOM_VK_SPACE:
    241      toggleRowChecked(document.getElementById("tabList").currentIndex);
    242      // Prevent page from scrolling on the space key.
    243      aEvent.preventDefault();
    244      break;
    245    case KeyEvent.DOM_VK_RETURN:
    246      var ix = document.getElementById("tabList").currentIndex;
    247      if (aEvent.ctrlKey && !treeView.isContainer(ix)) {
    248        restoreSingleTab(ix, aEvent.shiftKey);
    249      }
    250      break;
    251  }
    252 }
    253 
    254 // Helper functions
    255 
    256 function getBrowserWindow() {
    257  return window.browsingContext.topChromeWindow;
    258 }
    259 
    260 function toggleRowChecked(aIx) {
    261  function isChecked(aItem) {
    262    return aItem.checked;
    263  }
    264 
    265  var item = gTreeData[aIx];
    266  item.checked = !item.checked;
    267  treeView.treeBox.invalidateRow(aIx);
    268 
    269  if (treeView.isContainer(aIx)) {
    270    // (un)check all tabs of this window as well
    271    for (let tab of item.tabs) {
    272      tab.checked = item.checked;
    273      treeView.treeBox.invalidateRow(gTreeData.indexOf(tab));
    274    }
    275  } else {
    276    // Update the window's checkmark as well (0 means "partially checked").
    277    let state = false;
    278    if (item.parent.tabs.every(isChecked)) {
    279      state = true;
    280    } else if (item.parent.tabs.some(isChecked)) {
    281      state = 0;
    282    }
    283    item.parent.checked = state;
    284 
    285    treeView.treeBox.invalidateRow(gTreeData.indexOf(item.parent));
    286  }
    287 
    288  // we only disable the button when there's no cancel button.
    289  if (document.getElementById("errorCancel")) {
    290    document.getElementById("errorTryAgain").disabled =
    291      !gTreeData.some(isChecked);
    292  }
    293 }
    294 
    295 function restoreSingleTab(aIx, aShifted) {
    296  var tabbrowser = getBrowserWindow().gBrowser;
    297  var newTab = tabbrowser.addWebTab();
    298  var item = gTreeData[aIx];
    299 
    300  var tabState =
    301    gStateObject.windows[item.parent.ix].tabs[
    302      aIx - gTreeData.indexOf(item.parent) - 1
    303    ];
    304  // ensure tab would be visible on the tabstrip.
    305  tabState.hidden = false;
    306  SessionStore.setTabState(newTab, JSON.stringify(tabState));
    307 
    308  // respect the preference as to whether to select the tab (the Shift key inverses)
    309  if (
    310    Services.prefs.getBoolPref("browser.tabs.loadInBackground") != !aShifted
    311  ) {
    312    tabbrowser.selectedTab = newTab;
    313  }
    314 }
    315 
    316 // Tree controller
    317 
    318 var treeView = {
    319  treeBox: null,
    320  selection: null,
    321 
    322  get rowCount() {
    323    return gTreeData.length;
    324  },
    325  setTree(treeBox) {
    326    this.treeBox = treeBox;
    327  },
    328  getCellText(idx) {
    329    return gTreeData[idx].label;
    330  },
    331  isContainer(idx) {
    332    return "open" in gTreeData[idx];
    333  },
    334  getCellValue(idx) {
    335    return gTreeData[idx].checked;
    336  },
    337  isContainerOpen(idx) {
    338    return gTreeData[idx].open;
    339  },
    340  isContainerEmpty() {
    341    return false;
    342  },
    343  isSeparator() {
    344    return false;
    345  },
    346  isSorted() {
    347    return false;
    348  },
    349  isEditable() {
    350    return false;
    351  },
    352  canDrop() {
    353    return false;
    354  },
    355  getLevel(idx) {
    356    return this.isContainer(idx) ? 0 : 1;
    357  },
    358 
    359  getParentIndex(idx) {
    360    if (!this.isContainer(idx)) {
    361      for (var t = idx - 1; t >= 0; t--) {
    362        if (this.isContainer(t)) {
    363          return t;
    364        }
    365      }
    366    }
    367    return -1;
    368  },
    369 
    370  hasNextSibling(idx, after) {
    371    var thisLevel = this.getLevel(idx);
    372    for (var t = after + 1; t < gTreeData.length; t++) {
    373      if (this.getLevel(t) <= thisLevel) {
    374        return this.getLevel(t) == thisLevel;
    375      }
    376    }
    377    return false;
    378  },
    379 
    380  toggleOpenState(idx) {
    381    if (!this.isContainer(idx)) {
    382      return;
    383    }
    384    var item = gTreeData[idx];
    385    if (item.open) {
    386      // remove this window's tab rows from the view
    387      var thisLevel = this.getLevel(idx);
    388      /* eslint-disable no-empty */
    389      for (
    390        var t = idx + 1;
    391        t < gTreeData.length && this.getLevel(t) > thisLevel;
    392        t++
    393      ) {}
    394      /* eslint-disable no-empty */
    395      var deletecount = t - idx - 1;
    396      gTreeData.splice(idx + 1, deletecount);
    397      this.treeBox.rowCountChanged(idx + 1, -deletecount);
    398    } else {
    399      // add this window's tab rows to the view
    400      var toinsert = gTreeData[idx].tabs;
    401      for (var i = 0; i < toinsert.length; i++) {
    402        gTreeData.splice(idx + i + 1, 0, toinsert[i]);
    403      }
    404      this.treeBox.rowCountChanged(idx + 1, toinsert.length);
    405    }
    406    item.open = !item.open;
    407    this.treeBox.invalidateRow(idx);
    408  },
    409 
    410  getCellProperties(idx, column) {
    411    if (
    412      column.id == "restore" &&
    413      this.isContainer(idx) &&
    414      gTreeData[idx].checked === 0
    415    ) {
    416      return "partial";
    417    }
    418    if (column.id == "title") {
    419      return this.getImageSrc(idx, column) ? "icon" : "noicon";
    420    }
    421 
    422    return "";
    423  },
    424 
    425  getRowProperties(idx) {
    426    var winState = gTreeData[idx].parent || gTreeData[idx];
    427    if (winState.ix % 2 != 0) {
    428      return "alternate";
    429    }
    430 
    431    return "";
    432  },
    433 
    434  getImageSrc(idx, column) {
    435    if (column.id == "title") {
    436      return gTreeData[idx].src || null;
    437    }
    438    return null;
    439  },
    440 
    441  cycleHeader() {},
    442  cycleCell() {},
    443  selectionChanged() {},
    444  getColumnProperties() {
    445    return "";
    446  },
    447 };