/* -*- 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", {}); }, }, }; } };