tor-browser

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

ext-find.js (8908B)


      1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* vim: set sts=2 sw=2 et tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
      5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 /* global tabTracker */
      8 "use strict";
      9 
     10 ChromeUtils.defineESModuleGetters(this, {
     11  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     12 });
     13 
     14 var { ExtensionError } = ExtensionUtils;
     15 
     16 // A mapping of top-level ExtFind actors to arrays of results in each subframe.
     17 let findResults = new WeakMap();
     18 
     19 function getActorForBrowsingContext(browsingContext) {
     20  let windowGlobal = browsingContext.currentWindowGlobal;
     21  return windowGlobal ? windowGlobal.getActor("ExtFind") : null;
     22 }
     23 
     24 function getTopLevelActor(browser) {
     25  return getActorForBrowsingContext(browser.browsingContext);
     26 }
     27 
     28 function gatherActors(browsingContext) {
     29  let list = [];
     30 
     31  let actor = getActorForBrowsingContext(browsingContext);
     32  if (actor) {
     33    list.push({ actor, result: null });
     34  }
     35 
     36  let children = browsingContext.children;
     37  for (let child of children) {
     38    list.push(...gatherActors(child));
     39  }
     40 
     41  return list;
     42 }
     43 
     44 function mergeFindResults(params, list) {
     45  let finalResult = {
     46    count: 0,
     47  };
     48 
     49  if (params.includeRangeData) {
     50    finalResult.rangeData = [];
     51  }
     52  if (params.includeRectData) {
     53    finalResult.rectData = [];
     54  }
     55 
     56  let currentFramePos = -1;
     57  for (let item of list) {
     58    if (item.result.count == 0) {
     59      continue;
     60    }
     61 
     62    // The framePos is incremented for each different document that has matches.
     63    currentFramePos++;
     64 
     65    finalResult.count += item.result.count;
     66    if (params.includeRangeData && item.result.rangeData) {
     67      for (let range of item.result.rangeData) {
     68        range.framePos = currentFramePos;
     69      }
     70 
     71      finalResult.rangeData.push(...item.result.rangeData);
     72    }
     73 
     74    if (params.includeRectData && item.result.rectData) {
     75      finalResult.rectData.push(...item.result.rectData);
     76    }
     77  }
     78 
     79  return finalResult;
     80 }
     81 
     82 function sendMessageToAllActors(browser, message, params) {
     83  for (let { actor } of gatherActors(browser.browsingContext)) {
     84    actor.sendAsyncMessage("ext-Finder:" + message, params);
     85  }
     86 }
     87 
     88 async function getFindResultsForActor(findContext, message, params) {
     89  findContext.result = await findContext.actor.sendQuery(
     90    "ext-Finder:" + message,
     91    params
     92  );
     93  return findContext;
     94 }
     95 
     96 function queryAllActors(browser, message, params) {
     97  let promises = [];
     98  for (let findContext of gatherActors(browser.browsingContext)) {
     99    promises.push(getFindResultsForActor(findContext, message, params));
    100  }
    101  return Promise.all(promises);
    102 }
    103 
    104 async function collectFindResults(browser, findResults, params) {
    105  let results = await queryAllActors(browser, "CollectResults", params);
    106  findResults.set(getTopLevelActor(browser), results);
    107  return mergeFindResults(params, results);
    108 }
    109 
    110 async function runHighlight(browser, params) {
    111  let hasResults = false;
    112  let foundResults = false;
    113  let list = findResults.get(getTopLevelActor(browser));
    114  if (!list) {
    115    return Promise.reject({ message: "no search results to highlight" });
    116  }
    117 
    118  let highlightPromises = [];
    119 
    120  let index = params.rangeIndex;
    121  const highlightAll = typeof index != "number";
    122 
    123  for (let c = 0; c < list.length; c++) {
    124    if (list[c].result.count) {
    125      hasResults = true;
    126    }
    127 
    128    let actor = list[c].actor;
    129    if (highlightAll) {
    130      // Highlight all ranges.
    131      highlightPromises.push(
    132        actor.sendQuery("ext-Finder:HighlightResults", params)
    133      );
    134    } else if (!foundResults && index < list[c].result.count) {
    135      foundResults = true;
    136      params.rangeIndex = index;
    137      highlightPromises.push(
    138        actor.sendQuery("ext-Finder:HighlightResults", params)
    139      );
    140    } else {
    141      highlightPromises.push(
    142        actor.sendQuery("ext-Finder:ClearHighlighting", params)
    143      );
    144    }
    145 
    146    index -= list[c].result.count;
    147  }
    148 
    149  let responses = await Promise.all(highlightPromises);
    150  if (hasResults) {
    151    if (responses.includes("OutOfRange") || index >= 0) {
    152      return Promise.reject({ message: "index supplied was out of range" });
    153    } else if (responses.includes("Success")) {
    154      return;
    155    }
    156  }
    157 
    158  return Promise.reject({ message: "no search results to highlight" });
    159 }
    160 
    161 /**
    162 * runFindOperation
    163 * Utility for `find` and `highlightResults`.
    164 *
    165 * @param {BaseContext} context - context the find operation runs in.
    166 * @param {object} params - params to pass to message sender.
    167 * @param {string} message - identifying component of message name.
    168 *
    169 * @returns {Promise} a promise that will be resolved or rejected based on the
    170 *          data received by the message listener.
    171 */
    172 function runFindOperation(context, params, message) {
    173  let { tabId } = params;
    174  let tab = tabId ? tabTracker.getTab(tabId) : tabTracker.activeTab;
    175  let browser = tab.linkedBrowser;
    176  tabId = tabId || tabTracker.getId(tab);
    177  if (
    178    !context.privateBrowsingAllowed &&
    179    PrivateBrowsingUtils.isBrowserPrivate(browser)
    180  ) {
    181    return Promise.reject({ message: `Unable to search: ${tabId}` });
    182  }
    183  // We disallow find in about: urls.
    184  if (
    185    tab.linkedBrowser.contentPrincipal.isSystemPrincipal ||
    186    (["about", "chrome", "resource"].includes(
    187      tab.linkedBrowser.currentURI.scheme
    188    ) &&
    189      tab.linkedBrowser.currentURI.spec != "about:blank")
    190  ) {
    191    return Promise.reject({ message: `Unable to search: ${tabId}` });
    192  }
    193 
    194  if (message == "HighlightResults") {
    195    return runHighlight(browser, params);
    196  } else if (message == "CollectResults") {
    197    // Remove prior highlights before starting a new find operation.
    198    findResults.delete(getTopLevelActor(browser));
    199    return collectFindResults(browser, findResults, params);
    200  }
    201 }
    202 
    203 this.find = class extends ExtensionAPI {
    204  getAPI(context) {
    205    return {
    206      find: {
    207        /**
    208         * browser.find.find
    209         * Searches document and its frames for a given queryphrase and stores all found
    210         * Range objects in an array accessible by other browser.find methods.
    211         *
    212         * @param {string} queryphrase - The string to search for.
    213         * @param {object} params optional - may contain any of the following properties,
    214         *   all of which are optional:
    215         *   {number} tabId - Tab to query.  Defaults to the active tab.
    216         *   {boolean} caseSensitive - Highlight only ranges with case sensitive match.
    217         *   {boolean} entireWord - Highlight only ranges that match entire word.
    218         *   {boolean} includeRangeData - Whether to return range data.
    219         *   {boolean} includeRectData - Whether to return rectangle data.
    220         *
    221         * @returns {object} data received by the message listener that includes:
    222         *   {number} count - number of results found.
    223         *   {array} rangeData (if opted) - serialized representation of ranges found.
    224         *   {array} rectData (if opted) - rect data of ranges found.
    225         */
    226        find(queryphrase, params) {
    227          params = params || {};
    228          params.queryphrase = queryphrase;
    229          return runFindOperation(context, params, "CollectResults");
    230        },
    231 
    232        /**
    233         * browser.find.highlightResults
    234         * Highlights range(s) found in previous browser.find.find.
    235         *
    236         * @param {object} params optional - may contain any of the following properties,
    237         *   all of which are optional:
    238         *   {number} rangeIndex - Found range to be highlighted. Default highlights all ranges.
    239         *   {number} tabId - Tab to highlight.  Defaults to the active tab.
    240         *   {boolean} noScroll - Don't scroll to highlighted item.
    241         *
    242         * @returns {string} - data received by the message listener that may be:
    243         *   "Success" - Highlighting succeeded.
    244         *   "OutOfRange" - The index supplied was out of range.
    245         *   "NoResults" - There were no search results to highlight.
    246         */
    247        highlightResults(params) {
    248          params = params || {};
    249          return runFindOperation(context, params, "HighlightResults");
    250        },
    251 
    252        /**
    253         * browser.find.removeHighlighting
    254         * Removes all highlighting from previous search.
    255         *
    256         * @param {number} tabId optional
    257         *                 Tab to clear highlighting in.  Defaults to the active tab.
    258         */
    259        removeHighlighting(tabId) {
    260          let tab = tabId ? tabTracker.getTab(tabId) : tabTracker.activeTab;
    261          if (
    262            !context.privateBrowsingAllowed &&
    263            PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser)
    264          ) {
    265            throw new ExtensionError(`Invalid tab ID: ${tabId}`);
    266          }
    267          sendMessageToAllActors(tab.linkedBrowser, "ClearHighlighting", {});
    268        },
    269      },
    270    };
    271  }
    272 };