GeckoViewUtils.sys.mjs (17267B)
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 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 6 import { Log } from "resource://gre/modules/Log.sys.mjs"; 7 8 const lazy = {}; 9 10 ChromeUtils.defineESModuleGetters(lazy, { 11 EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", 12 clearTimeout: "resource://gre/modules/Timer.sys.mjs", 13 setTimeout: "resource://gre/modules/Timer.sys.mjs", 14 }); 15 16 if (AppConstants.platform == "android") { 17 ChromeUtils.defineESModuleGetters(lazy, { 18 AndroidAppender: "resource://gre/modules/AndroidLog.sys.mjs", 19 }); 20 } 21 22 export var GeckoViewUtils = { 23 /** 24 * Define a lazy getter that loads an object from external code, and 25 * optionally handles observer and/or message manager notifications for the 26 * object, so the object only loads when a notification is received. 27 * 28 * @param scope Scope for holding the loaded object. 29 * @param name Name of the object to load. 30 * @param service If specified, load the object from a JS component; the 31 * component must include the line 32 * "this.wrappedJSObject = this;" in its constructor. 33 * @param module If specified, load the object from a JS module. 34 * @param init Optional post-load initialization function. 35 * @param observers If specified, listen to specified observer notifications. 36 * @param ppmm If specified, listen to specified process messages. 37 * @param mm If specified, listen to specified frame messages. 38 * @param ged If specified, listen to specified global EventDispatcher events. 39 * @param once if true, only listen to the specified 40 * events/messages/notifications once. 41 */ 42 addLazyGetter( 43 scope, 44 name, 45 { service, module, handler, observers, ppmm, mm, ged, init, once } 46 ) { 47 ChromeUtils.defineLazyGetter(scope, name, _ => { 48 let ret = undefined; 49 if (module) { 50 ret = ChromeUtils.importESModule(module)[name]; 51 } else if (service) { 52 ret = Cc[service].getService(Ci.nsISupports).wrappedJSObject; 53 } else if (typeof handler === "function") { 54 ret = { 55 handleEvent: handler, 56 observe: handler, 57 onEvent: handler, 58 receiveMessage: handler, 59 }; 60 } else if (handler) { 61 ret = handler; 62 } 63 if (ret && init) { 64 init.call(scope, ret); 65 } 66 return ret; 67 }); 68 69 if (observers) { 70 const observer = (subject, topic, data) => { 71 Services.obs.removeObserver(observer, topic); 72 if (!once) { 73 Services.obs.addObserver(scope[name], topic); 74 } 75 scope[name].observe(subject, topic, data); // Explicitly notify new observer 76 }; 77 observers.forEach(topic => Services.obs.addObserver(observer, topic)); 78 } 79 80 if (!this.IS_PARENT_PROCESS) { 81 // ppmm, mm, and ged are only available in the parent process. 82 return; 83 } 84 85 const addMMListener = (target, names) => { 86 const listener = msg => { 87 target.removeMessageListener(msg.name, listener); 88 if (!once) { 89 target.addMessageListener(msg.name, scope[name]); 90 } 91 scope[name].receiveMessage(msg); 92 }; 93 names.forEach(msg => target.addMessageListener(msg, listener)); 94 }; 95 if (ppmm) { 96 addMMListener(Services.ppmm, ppmm); 97 } 98 if (mm) { 99 addMMListener(Services.mm, mm); 100 } 101 102 if (ged) { 103 const listener = (event, data, callback) => { 104 lazy.EventDispatcher.instance.unregisterListener(listener, event); 105 if (!once) { 106 lazy.EventDispatcher.instance.registerListener(scope[name], event); 107 } 108 scope[name].onEvent(event, data, callback); 109 }; 110 lazy.EventDispatcher.instance.registerListener(listener, ged); 111 } 112 }, 113 114 _addLazyListeners(events, handler, scope, name, addFn, handleFn) { 115 if (!handler) { 116 handler = _ => 117 Array.isArray(name) ? name.map(n => scope[n]) : scope[name]; 118 } 119 const listener = (...args) => { 120 let handlers = handler(...args); 121 if (!handlers) { 122 return; 123 } 124 if (!Array.isArray(handlers)) { 125 handlers = [handlers]; 126 } 127 handleFn(handlers, listener, args); 128 }; 129 if (Array.isArray(events)) { 130 addFn(events, listener); 131 } else { 132 addFn([events], listener); 133 } 134 }, 135 136 /** 137 * Add lazy event listeners that only load the actual handler when an event 138 * is being handled. 139 * 140 * @param target Event target for the event listeners. 141 * @param events Event name as a string or array. 142 * @param handler If specified, function that, for a given event, returns the 143 * actual event handler as an object or an array of objects. 144 * If handler is not specified, the actual event handler is 145 * specified using the scope and name pair. 146 * @param scope See handler. 147 * @param name See handler. 148 * @param options Options for addEventListener. 149 */ 150 addLazyEventListener(target, events, { handler, scope, name, options }) { 151 this._addLazyListeners( 152 events, 153 handler, 154 scope, 155 name, 156 (events, listener) => { 157 events.forEach(event => 158 target.addEventListener(event, listener, options) 159 ); 160 }, 161 (handlers, listener, args) => { 162 if (!options || !options.once) { 163 target.removeEventListener(args[0].type, listener, options); 164 handlers.forEach(handler => 165 target.addEventListener(args[0].type, handler, options) 166 ); 167 } 168 handlers.forEach(handler => handler.handleEvent(args[0])); 169 } 170 ); 171 }, 172 173 /** 174 * Add lazy pref observers, and only load the actual handler once the pref 175 * value changes from default, and every time the pref value changes 176 * afterwards. 177 * 178 * @param aPrefs Prefs as an object or array. Each pref object has fields 179 * "name" and "default", indicating the name and default value 180 * of the pref, respectively. 181 * @param handler If specified, function that, for a given pref, returns the 182 * actual event handler as an object or an array of objects. 183 * If handler is not specified, the actual event handler is 184 * specified using the scope and name pair. 185 * @param scope See handler. 186 * @param name See handler. 187 * @param once If true, only observe the specified prefs once. 188 */ 189 addLazyPrefObserver(aPrefs, { handler, scope, name, once }) { 190 this._addLazyListeners( 191 aPrefs, 192 handler, 193 scope, 194 name, 195 (prefs, observer) => { 196 prefs.forEach(pref => Services.prefs.addObserver(pref.name, observer)); 197 prefs.forEach(pref => { 198 if (pref.default === undefined) { 199 return; 200 } 201 let value; 202 switch (typeof pref.default) { 203 case "string": 204 value = Services.prefs.getCharPref(pref.name, pref.default); 205 break; 206 case "number": 207 value = Services.prefs.getIntPref(pref.name, pref.default); 208 break; 209 case "boolean": 210 value = Services.prefs.getBoolPref(pref.name, pref.default); 211 break; 212 } 213 if (pref.default !== value) { 214 // Notify observer if value already changed from default. 215 observer(Services.prefs, "nsPref:changed", pref.name); 216 } 217 }); 218 }, 219 (handlers, observer, args) => { 220 if (!once) { 221 Services.prefs.removeObserver(args[2], observer); 222 handlers.forEach(() => Services.prefs.addObserver(args[2], observer)); 223 } 224 handlers.forEach(handler => handler.observe(...args)); 225 } 226 ); 227 }, 228 229 getRootDocShell(aWin) { 230 if (!aWin) { 231 return null; 232 } 233 let docShell; 234 try { 235 docShell = aWin.QueryInterface(Ci.nsIDocShell); 236 } catch (e) { 237 docShell = aWin.docShell; 238 } 239 return docShell.rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor); 240 }, 241 242 /** 243 * Return the outermost chrome DOM window (the XUL window) for a given DOM 244 * window, in the parent process. 245 * 246 * @param aWin a DOM window. 247 */ 248 getChromeWindow(aWin) { 249 const docShell = this.getRootDocShell(aWin); 250 return docShell && docShell.domWindow; 251 }, 252 253 /** 254 * Return the content frame message manager (aka the frame script global 255 * object) for a given DOM window, in a child process. 256 * 257 * @param aWin a DOM window. 258 */ 259 getContentFrameMessageManager(aWin) { 260 const docShell = this.getRootDocShell(aWin); 261 return docShell && docShell.getInterface(Ci.nsIBrowserChild).messageManager; 262 }, 263 264 /** 265 * Return the per-nsWindow EventDispatcher for a given DOM window, in either 266 * the parent process or a child process. 267 * 268 * @param aWin a DOM window. 269 */ 270 getDispatcherForWindow(aWin) { 271 try { 272 if (!this.IS_PARENT_PROCESS) { 273 const mm = this.getContentFrameMessageManager(aWin.top || aWin); 274 return mm && lazy.EventDispatcher.forMessageManager(mm); 275 } 276 const win = this.getChromeWindow(aWin.top || aWin); 277 if (!win.closed) { 278 return win.WindowEventDispatcher || lazy.EventDispatcher.for(win); 279 } 280 } catch (e) {} 281 return null; 282 }, 283 284 /** 285 * Return promise for waiting for finishing PanZoomState. 286 * 287 * @param aWindow a DOM window. 288 * @return promise 289 */ 290 waitForPanZoomState(aWindow) { 291 return new Promise((resolve, reject) => { 292 if ( 293 !aWindow?.windowUtils.asyncPanZoomEnabled || 294 !Services.prefs.getBoolPref("apz.zoom-to-focused-input.enabled") 295 ) { 296 // No zoomToFocusedInput. 297 resolve(); 298 return; 299 } 300 301 let timerId = 0; 302 303 const panZoomState = (aSubject, aTopic, aData) => { 304 if (timerId != 0) { 305 // aWindow may be dead object now. 306 try { 307 lazy.clearTimeout(timerId); 308 } catch (e) {} 309 timerId = 0; 310 } 311 312 if (aData === "NOTHING") { 313 Services.obs.removeObserver(panZoomState, "PanZoom:StateChange"); 314 resolve(); 315 } 316 }; 317 318 Services.obs.addObserver(panZoomState, "PanZoom:StateChange"); 319 320 // "GeckoView:ZoomToInput" has the timeout as 500ms when window isn't 321 // resized (it means on-screen-keyboard is already shown). 322 // So after up to 500ms, APZ event is sent. So we need to wait for more 323 // 500ms. 324 timerId = lazy.setTimeout(() => { 325 // PanZoom state isn't changed. zoomToFocusedInput will return error. 326 Services.obs.removeObserver(panZoomState, "PanZoom:StateChange"); 327 reject(); 328 }, 600); 329 }); 330 }, 331 332 /** 333 * Add logging functions to the specified scope that forward to the given 334 * Log.sys.mjs logger. Currently "debug" and "warn" functions are supported. To 335 * log something, call the function through a template literal: 336 * 337 * function foo(bar, baz) { 338 * debug `hello world`; 339 * debug `foo called with ${bar} as bar`; 340 * warn `this is a warning for ${baz}`; 341 * } 342 * 343 * An inline format can also be used for logging: 344 * 345 * let bar = 42; 346 * do_something(bar); // No log. 347 * do_something(debug.foo = bar); // Output "foo = 42" to the log. 348 * 349 * @param aTag Name of the Log.sys.mjs logger to forward logs to. 350 * @param aScope Scope to add the logging functions to. 351 */ 352 initLogging(aTag, aScope) { 353 aScope = aScope || {}; 354 const tag = "GeckoView." + aTag.replace(/^GeckoView\.?/, ""); 355 356 // Only provide two levels for simplicity. 357 // For "info", use "debug" instead. 358 // For "error", throw an actual JS error instead. 359 for (const level of ["DEBUG", "WARN"]) { 360 const log = (strings, ...exprs) => 361 this._log(log.logger, level, strings, exprs); 362 363 ChromeUtils.defineLazyGetter(log, "logger", _ => { 364 const logger = Log.repository.getLogger(tag); 365 logger.parent = this.rootLogger; 366 return logger; 367 }); 368 369 aScope[level.toLowerCase()] = new Proxy(log, { 370 set: (obj, prop, value) => obj([prop + " = ", ""], value) || true, 371 }); 372 } 373 return aScope; 374 }, 375 376 get rootLogger() { 377 if (!this._rootLogger) { 378 this._rootLogger = Log.repository.getLogger("GeckoView"); 379 // On Android, we'll log to the native android logcat output using 380 // __android_log_write. On iOS, fall back to a dump appender. 381 if (AppConstants.platform == "android") { 382 this._rootLogger.addAppender(new lazy.AndroidAppender()); 383 } else { 384 this._rootLogger.addAppender(new Log.DumpAppender()); 385 } 386 this._rootLogger.manageLevelFromPref("geckoview.logging"); 387 } 388 return this._rootLogger; 389 }, 390 391 _log(aLogger, aLevel, aStrings, aExprs) { 392 if (!Array.isArray(aStrings)) { 393 const [, file, line] = new Error().stack.match(/.*\n.*\n.*@(.*):(\d+):/); 394 throw Error( 395 `Expecting template literal: ${aLevel} \`foo \${bar}\``, 396 file, 397 +line 398 ); 399 } 400 401 if (aLogger.level > Log.Level.Numbers[aLevel]) { 402 // Log disabled. 403 return; 404 } 405 406 // Do some GeckoView-specific formatting: 407 // * Remove newlines so long log lines can be put into multiple lines: 408 // debug `foo=${foo} 409 // bar=${bar}`; 410 const strs = Array.from(aStrings); 411 const regex = /\n\s*/g; 412 for (let i = 0; i < strs.length; i++) { 413 strs[i] = strs[i].replace(regex, " "); 414 } 415 416 // * Heuristically format flags as hex. 417 // * Heuristically format nsresult as string name or hex. 418 for (let i = 0; i < aExprs.length; i++) { 419 const expr = aExprs[i]; 420 switch (typeof expr) { 421 case "number": 422 if (expr > 0 && /\ba?[fF]lags?[\s=:]+$/.test(strs[i])) { 423 // Likely a flag; display in hex. 424 aExprs[i] = `0x${expr.toString(0x10)}`; 425 } else if (expr >= 0 && /\b(a?[sS]tatus|rv)[\s=:]+$/.test(strs[i])) { 426 // Likely an nsresult; display in name or hex. 427 aExprs[i] = `0x${expr.toString(0x10)}`; 428 for (const name in Cr) { 429 if (expr === Cr[name]) { 430 aExprs[i] = name; 431 break; 432 } 433 } 434 } 435 break; 436 } 437 } 438 439 aLogger[aLevel.toLowerCase()](strs, ...aExprs); 440 }, 441 442 /** 443 * Checks whether the principal is supported for permissions. 444 * 445 * @param {nsIPrincipal} principal 446 * The principal to check. 447 * 448 * @return {boolean} if the principal is supported. 449 */ 450 isSupportedPermissionsPrincipal(principal) { 451 if (!principal) { 452 return false; 453 } 454 if (!(principal instanceof Ci.nsIPrincipal)) { 455 throw new Error( 456 "Argument passed as principal is not an instance of Ci.nsIPrincipal" 457 ); 458 } 459 return this.isSupportedPermissionsScheme(principal.scheme); 460 }, 461 462 /** 463 * Checks whether we support managing permissions for a specific scheme. 464 * 465 * @param {string} scheme - Scheme to test. 466 * @returns {boolean} Whether the scheme is supported. 467 */ 468 isSupportedPermissionsScheme(scheme) { 469 return ["http", "https", "moz-extension", "file"].includes(scheme); 470 }, 471 472 /** 473 * Attach nsIOpenWindowInfo when opening GeckoSession 474 * 475 * @param {string} aSessionId A session id 476 * @param {nsIOpenWindowInfo} aOpenWindowInfo Attached nsIOpendWindowInfo 477 * @param {string} aName A window name 478 * @returns {Promise} resolved when nsIOpenWindowInfo is attached 479 */ 480 waitAndSetupWindow(aSessionId, aOpenWindowInfo, aName) { 481 if (!aSessionId) { 482 return Promise.reject(); 483 } 484 485 return new Promise((resolve, reject) => { 486 const handler = { 487 observe(aSubject, aTopic) { 488 if ( 489 aTopic === "geckoview-window-created" && 490 aSubject.name === aSessionId 491 ) { 492 // This value will be read by nsFrameLoader while it is being initialized. 493 aSubject.browser.openWindowInfo = aOpenWindowInfo; 494 495 // Gecko will use this attribute to set the name of the opened window. 496 if (aName) { 497 aSubject.browser.setAttribute("name", aName); 498 } 499 500 if ( 501 !aOpenWindowInfo.isRemote && 502 aSubject.browser.hasAttribute("remote") 503 ) { 504 // We cannot start in remote mode when we have an opener. 505 aSubject.browser.setAttribute("remote", "false"); 506 aSubject.browser.removeAttribute("remoteType"); 507 } 508 Services.obs.removeObserver(handler, "geckoview-window-created"); 509 if (!aSubject) { 510 reject(); 511 return; 512 } 513 resolve(aSubject); 514 } 515 }, 516 }; 517 518 // This event is emitted from createBrowser() in geckoview.js 519 Services.obs.addObserver(handler, "geckoview-window-created"); 520 }); 521 }, 522 }; 523 524 ChromeUtils.defineLazyGetter( 525 GeckoViewUtils, 526 "IS_PARENT_PROCESS", 527 _ => Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT 528 );