NavigableManager.sys.mjs (9751B)
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 const lazy = {}; 6 7 ChromeUtils.defineESModuleGetters(lazy, { 8 BiMap: "chrome://remote/content/shared/BiMap.sys.mjs", 9 BrowsingContextListener: 10 "chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs", 11 generateUUID: "chrome://remote/content/shared/UUID.sys.mjs", 12 TabManager: "chrome://remote/content/shared/TabManager.sys.mjs", 13 }); 14 15 /** 16 * The navigable manager is intended to be used as a singleton and is 17 * responsible for tracking open browsing contexts by assigning each a 18 * unique identifier. This allows them to be referenced unambiguously. 19 * For top-level browsing contexts, the content browser instance itself 20 * is used as the anchor, since cross-origin navigations can result in 21 * browsing context replacements. Using the browser as a stable reference 22 * ensures that protocols like WebDriver BiDi and Marionette can reliably 23 * point to the intended "navigable" — a concept from the HTML specification 24 * that is not implemented in Firefox. 25 */ 26 class NavigableManagerClass { 27 #browserIds; 28 #chromeNavigables; 29 #contextListener; 30 #navigableIds; 31 #tracking; 32 33 constructor() { 34 this.#tracking = false; 35 36 // Maps browser's `permanentKey` to an uuid: WeakMap.<Object, string> 37 // 38 // It's required as a fallback, since in the case when a context was 39 // discarded embedderElement is gone, and we cannot retrieve the 40 // context id from the formerly known browser. 41 this.#browserIds = new WeakMap(); 42 43 // Maps canonical browsing contexts from the parent process 44 // to a uuid and vice versa. 45 this.#chromeNavigables = new lazy.BiMap(); 46 47 // Maps browsing contexts to uuid: WeakMap.<BrowsingContext, string>. 48 this.#navigableIds = new WeakMap(); 49 50 // Start tracking by default when the class gets instantiated. 51 this.startTracking(); 52 } 53 54 /** 55 * Retrieve the browser element corresponding to the provided unique id, 56 * previously generated via getIdForBrowser. 57 * 58 * TODO: To avoid creating strong references on browser elements and 59 * potentially leaking those elements, this method loops over all windows and 60 * all tabs. It should be replaced by a faster implementation in Bug 1750065. 61 * 62 * @param {string} id 63 * A browser unique id created by getIdForBrowser. 64 * 65 * @returns {XULBrowser} 66 * The <xul:browser> corresponding to the provided id. Will return 67 * `null` if no matching browser element is found. 68 */ 69 getBrowserById(id) { 70 for (const tab of lazy.TabManager.allTabs) { 71 const contentBrowser = lazy.TabManager.getBrowserForTab(tab); 72 if (this.getIdForBrowser(contentBrowser) == id) { 73 return contentBrowser; 74 } 75 } 76 77 return null; 78 } 79 80 /** 81 * Retrieve the browsing context corresponding to the provided navigabl id. 82 * 83 * @param {string} id 84 * A browsing context unique id (created by getIdForBrowsingContext). 85 * 86 * @returns {BrowsingContext|null} 87 * The browsing context found for this id, null if none was found or 88 * browsing context is discarded. 89 */ 90 getBrowsingContextById(id) { 91 let browsingContext; 92 93 if (this.#chromeNavigables.hasId(id)) { 94 // Chrome browsing context 95 browsingContext = this.#chromeNavigables.getObject(id); 96 } else { 97 // Content browsing context 98 const browser = this.getBrowserById(id); 99 if (browser) { 100 // Top-level browsing context 101 browsingContext = browser.browsingContext; 102 } else { 103 // Content child browsing contexts 104 const context = BrowsingContext.get(id); 105 if (context && context.isContent && context.parent) { 106 browsingContext = context; 107 } 108 } 109 } 110 111 if (!browsingContext || browsingContext.isDiscarded) { 112 return null; 113 } 114 115 return browsingContext; 116 } 117 118 /** 119 * Retrieve the unique id for the given xul browser element. The id is a 120 * dynamically generated uuid associated with the permanentKey property of the 121 * given browser element. This method is preferable over getIdForBrowsingContext 122 * in case of working with browser element of a tab, since we can not guarantee 123 * that browsing context is attached to it. 124 * 125 * @param {XULBrowser} browser 126 * The <xul:browser> for which we want to retrieve the id. 127 * 128 * @returns {string|null} 129 * The unique id for this browser or `null` if invalid. 130 */ 131 getIdForBrowser(browser) { 132 if (!(XULElement.isInstance(browser) && browser.permanentKey)) { 133 // Ignore those browsers that do not have a permanentKey 134 // attached like the print preview (bug 1990485), but which 135 // we need to uniquely identify a top-level browsing context. 136 return null; 137 } 138 139 return this.#browserIds.getOrInsertComputed( 140 browser.permanentKey, 141 lazy.generateUUID 142 ); 143 } 144 145 /** 146 * Retrieve the id of a Browsing Context. 147 * 148 * For a top-level browsing context a custom unique id will be returned. 149 * 150 * @param {BrowsingContext=} browsingContext 151 * The browsing context to get the id from. 152 * 153 * @returns {string|null} 154 * The unique id of the browsing context or `null` if invalid. 155 */ 156 getIdForBrowsingContext(browsingContext) { 157 if (!BrowsingContext.isInstance(browsingContext)) { 158 return null; 159 } 160 161 if (!browsingContext.isContent) { 162 // When the browsing context runs in the parent process we 163 // can use the browsing context as key because it's stable. 164 return this.#chromeNavigables.getOrInsert(browsingContext); 165 } 166 167 if (!browsingContext.parent) { 168 // For top-level browsing contexts always try to use the browser 169 // as navigable first because it survives a cross-process navigation. 170 const browser = this.#getBrowserForBrowsingContext(browsingContext); 171 if (browser) { 172 return this.getIdForBrowser(browser); 173 } 174 175 // If no browser can be found fallback to use the navigable id instead. 176 return this.#navigableIds.has(browsingContext) 177 ? this.#navigableIds.get(browsingContext) 178 : null; 179 } 180 181 // Child browsing context (frame) 182 return browsingContext.id.toString(); 183 } 184 185 /** 186 * Get the navigable for the given browsing context. 187 * 188 * Because Gecko doesn't support the Navigable concept in content 189 * scope the content browser could be used to uniquely identify 190 * top-level browsing contexts. 191 * 192 * @param {BrowsingContext} browsingContext 193 * 194 * @returns {BrowsingContext|XULBrowser} The navigable 195 * 196 * @throws {TypeError} 197 * If `browsingContext` is not a CanonicalBrowsingContext instance. 198 */ 199 getNavigableForBrowsingContext(browsingContext) { 200 if (!lazy.TabManager.isValidCanonicalBrowsingContext(browsingContext)) { 201 throw new TypeError( 202 `Expected browsingContext to be a CanonicalBrowsingContext, got ${browsingContext}` 203 ); 204 } 205 206 if (browsingContext.isContent && browsingContext.parent === null) { 207 return this.#getBrowserForBrowsingContext(browsingContext); 208 } 209 210 return browsingContext; 211 } 212 213 startTracking() { 214 if (this.#tracking) { 215 return; 216 } 217 218 this.#contextListener = new lazy.BrowsingContextListener(); 219 this.#contextListener.on("attached", this.#onContextAttached); 220 this.#contextListener.on("discarded", this.#onContextDiscarded); 221 this.#contextListener.startListening(); 222 223 // Register as well all browsing contexts from already open tabs. 224 lazy.TabManager.getBrowsers().forEach(browser => 225 this.#setIdForBrowsingContext(browser.browsingContext) 226 ); 227 228 this.#tracking = true; 229 } 230 231 stopTracking() { 232 if (!this.#tracking) { 233 return; 234 } 235 236 this.#contextListener.off("attached", this.#onContextAttached); 237 this.#contextListener.off("discarded", this.#onContextDiscarded); 238 this.#contextListener.stopListening(); 239 this.#contextListener = null; 240 241 this.#chromeNavigables.clear(); 242 this.#browserIds = new WeakMap(); 243 this.#navigableIds = new WeakMap(); 244 245 this.#tracking = false; 246 } 247 248 /** Private methods */ 249 250 /** 251 * Try to find the browser element to browsing context is attached to. 252 * 253 * @param {BrowsingContext} browsingContext 254 * The browsing context to find the related browser for. 255 * 256 * @returns {XULBrowser|null} 257 * The <xul:browser> element, or `null` if no browser exists. 258 */ 259 #getBrowserForBrowsingContext(browsingContext) { 260 return browsingContext.top.embedderElement 261 ? browsingContext.top.embedderElement 262 : null; 263 } 264 265 /** 266 * Update the internal maps for a new browsing context. 267 * 268 * @param {BrowsingContext} browsingContext 269 * The browsing context that needs to be observed. 270 */ 271 #setIdForBrowsingContext(browsingContext) { 272 const id = this.getIdForBrowsingContext(browsingContext); 273 274 // Add a fallback to the navigable weak map so that an id can 275 // also be retrieved when the related browser was closed. 276 this.#navigableIds.set(browsingContext, id); 277 } 278 279 /** Event handlers */ 280 281 #onContextAttached = (_, data = {}) => { 282 const { browsingContext } = data; 283 284 if (!browsingContext.isContent) { 285 this.#chromeNavigables.getOrInsert(browsingContext); 286 return; 287 } 288 289 this.#setIdForBrowsingContext(browsingContext); 290 }; 291 292 #onContextDiscarded = (_, data = {}) => { 293 const { browsingContext } = data; 294 295 if (!browsingContext.isContent) { 296 this.#chromeNavigables.deleteByObject(browsingContext); 297 } 298 }; 299 } 300 301 // Expose a shared singleton. 302 export const NavigableManager = new NavigableManagerClass();