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 };