summaryrefslogtreecommitdiffstats
path: root/browser/components/extensions/parent/ext-find.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/extensions/parent/ext-find.js')
-rw-r--r--browser/components/extensions/parent/ext-find.js272
1 files changed, 272 insertions, 0 deletions
diff --git a/browser/components/extensions/parent/ext-find.js b/browser/components/extensions/parent/ext-find.js
new file mode 100644
index 0000000000..5397caa85b
--- /dev/null
+++ b/browser/components/extensions/parent/ext-find.js
@@ -0,0 +1,272 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* global tabTracker */
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+});
+
+var { ExtensionError } = ExtensionUtils;
+
+// A mapping of top-level ExtFind actors to arrays of results in each subframe.
+let findResults = new WeakMap();
+
+function getActorForBrowsingContext(browsingContext) {
+ let windowGlobal = browsingContext.currentWindowGlobal;
+ return windowGlobal ? windowGlobal.getActor("ExtFind") : null;
+}
+
+function getTopLevelActor(browser) {
+ return getActorForBrowsingContext(browser.browsingContext);
+}
+
+function gatherActors(browsingContext) {
+ let list = [];
+
+ let actor = getActorForBrowsingContext(browsingContext);
+ if (actor) {
+ list.push({ actor, result: null });
+ }
+
+ let children = browsingContext.children;
+ for (let child of children) {
+ list.push(...gatherActors(child));
+ }
+
+ return list;
+}
+
+function mergeFindResults(params, list) {
+ let finalResult = {
+ count: 0,
+ };
+
+ if (params.includeRangeData) {
+ finalResult.rangeData = [];
+ }
+ if (params.includeRectData) {
+ finalResult.rectData = [];
+ }
+
+ let currentFramePos = -1;
+ for (let item of list) {
+ if (item.result.count == 0) {
+ continue;
+ }
+
+ // The framePos is incremented for each different document that has matches.
+ currentFramePos++;
+
+ finalResult.count += item.result.count;
+ if (params.includeRangeData && item.result.rangeData) {
+ for (let range of item.result.rangeData) {
+ range.framePos = currentFramePos;
+ }
+
+ finalResult.rangeData.push(...item.result.rangeData);
+ }
+
+ if (params.includeRectData && item.result.rectData) {
+ finalResult.rectData.push(...item.result.rectData);
+ }
+ }
+
+ return finalResult;
+}
+
+function sendMessageToAllActors(browser, message, params) {
+ for (let { actor } of gatherActors(browser.browsingContext)) {
+ actor.sendAsyncMessage("ext-Finder:" + message, params);
+ }
+}
+
+async function getFindResultsForActor(findContext, message, params) {
+ findContext.result = await findContext.actor.sendQuery(
+ "ext-Finder:" + message,
+ params
+ );
+ return findContext;
+}
+
+function queryAllActors(browser, message, params) {
+ let promises = [];
+ for (let findContext of gatherActors(browser.browsingContext)) {
+ promises.push(getFindResultsForActor(findContext, message, params));
+ }
+ return Promise.all(promises);
+}
+
+async function collectFindResults(browser, findResults, params) {
+ let results = await queryAllActors(browser, "CollectResults", params);
+ findResults.set(getTopLevelActor(browser), results);
+ return mergeFindResults(params, results);
+}
+
+async function runHighlight(browser, params) {
+ let hasResults = false;
+ let foundResults = false;
+ let list = findResults.get(getTopLevelActor(browser));
+ if (!list) {
+ return Promise.reject({ message: "no search results to highlight" });
+ }
+
+ let highlightPromises = [];
+
+ let index = params.rangeIndex;
+ const highlightAll = typeof index != "number";
+
+ for (let c = 0; c < list.length; c++) {
+ if (list[c].result.count) {
+ hasResults = true;
+ }
+
+ let actor = list[c].actor;
+ if (highlightAll) {
+ // Highlight all ranges.
+ highlightPromises.push(
+ actor.sendQuery("ext-Finder:HighlightResults", params)
+ );
+ } else if (!foundResults && index < list[c].result.count) {
+ foundResults = true;
+ params.rangeIndex = index;
+ highlightPromises.push(
+ actor.sendQuery("ext-Finder:HighlightResults", params)
+ );
+ } else {
+ highlightPromises.push(
+ actor.sendQuery("ext-Finder:ClearHighlighting", params)
+ );
+ }
+
+ index -= list[c].result.count;
+ }
+
+ let responses = await Promise.all(highlightPromises);
+ if (hasResults) {
+ if (responses.includes("OutOfRange") || index >= 0) {
+ return Promise.reject({ message: "index supplied was out of range" });
+ } else if (responses.includes("Success")) {
+ return;
+ }
+ }
+
+ return Promise.reject({ message: "no search results to highlight" });
+}
+
+/**
+ * runFindOperation
+ * Utility for `find` and `highlightResults`.
+ *
+ * @param {BaseContext} context - context the find operation runs in.
+ * @param {object} params - params to pass to message sender.
+ * @param {string} message - identifying component of message name.
+ *
+ * @returns {Promise} a promise that will be resolved or rejected based on the
+ * data received by the message listener.
+ */
+function runFindOperation(context, params, message) {
+ let { tabId } = params;
+ let tab = tabId ? tabTracker.getTab(tabId) : tabTracker.activeTab;
+ let browser = tab.linkedBrowser;
+ tabId = tabId || tabTracker.getId(tab);
+ if (
+ !context.privateBrowsingAllowed &&
+ PrivateBrowsingUtils.isBrowserPrivate(browser)
+ ) {
+ return Promise.reject({ message: `Unable to search: ${tabId}` });
+ }
+ // We disallow find in about: urls.
+ if (
+ tab.linkedBrowser.contentPrincipal.isSystemPrincipal ||
+ (["about", "chrome", "resource"].includes(
+ tab.linkedBrowser.currentURI.scheme
+ ) &&
+ tab.linkedBrowser.currentURI.spec != "about:blank")
+ ) {
+ return Promise.reject({ message: `Unable to search: ${tabId}` });
+ }
+
+ if (message == "HighlightResults") {
+ return runHighlight(browser, params);
+ } else if (message == "CollectResults") {
+ // Remove prior highlights before starting a new find operation.
+ findResults.delete(getTopLevelActor(browser));
+ return collectFindResults(browser, findResults, params);
+ }
+}
+
+this.find = class extends ExtensionAPI {
+ getAPI(context) {
+ return {
+ find: {
+ /**
+ * browser.find.find
+ * Searches document and its frames for a given queryphrase and stores all found
+ * Range objects in an array accessible by other browser.find methods.
+ *
+ * @param {string} queryphrase - The string to search for.
+ * @param {object} params optional - may contain any of the following properties,
+ * all of which are optional:
+ * {number} tabId - Tab to query. Defaults to the active tab.
+ * {boolean} caseSensitive - Highlight only ranges with case sensitive match.
+ * {boolean} entireWord - Highlight only ranges that match entire word.
+ * {boolean} includeRangeData - Whether to return range data.
+ * {boolean} includeRectData - Whether to return rectangle data.
+ *
+ * @returns {object} data received by the message listener that includes:
+ * {number} count - number of results found.
+ * {array} rangeData (if opted) - serialized representation of ranges found.
+ * {array} rectData (if opted) - rect data of ranges found.
+ */
+ find(queryphrase, params) {
+ params = params || {};
+ params.queryphrase = queryphrase;
+ return runFindOperation(context, params, "CollectResults");
+ },
+
+ /**
+ * browser.find.highlightResults
+ * Highlights range(s) found in previous browser.find.find.
+ *
+ * @param {object} params optional - may contain any of the following properties,
+ * all of which are optional:
+ * {number} rangeIndex - Found range to be highlighted. Default highlights all ranges.
+ * {number} tabId - Tab to highlight. Defaults to the active tab.
+ * {boolean} noScroll - Don't scroll to highlighted item.
+ *
+ * @returns {string} - data received by the message listener that may be:
+ * "Success" - Highlighting succeeded.
+ * "OutOfRange" - The index supplied was out of range.
+ * "NoResults" - There were no search results to highlight.
+ */
+ highlightResults(params) {
+ params = params || {};
+ return runFindOperation(context, params, "HighlightResults");
+ },
+
+ /**
+ * browser.find.removeHighlighting
+ * Removes all highlighting from previous search.
+ *
+ * @param {number} tabId optional
+ * Tab to clear highlighting in. Defaults to the active tab.
+ */
+ removeHighlighting(tabId) {
+ let tab = tabId ? tabTracker.getTab(tabId) : tabTracker.activeTab;
+ if (
+ !context.privateBrowsingAllowed &&
+ PrivateBrowsingUtils.isBrowserPrivate(tab.linkedBrowser)
+ ) {
+ throw new ExtensionError(`Invalid tab ID: ${tabId}`);
+ }
+ sendMessageToAllActors(tab.linkedBrowser, "ClearHighlighting", {});
+ },
+ },
+ };
+ }
+};