GeckoViewContent.sys.mjs (19590B)
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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs"; 6 7 const lazy = {}; 8 ChromeUtils.defineESModuleGetters(lazy, { 9 TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs", 10 }); 11 12 export class GeckoViewContent extends GeckoViewModule { 13 onInit() { 14 this.registerListener([ 15 "GeckoViewContent:ExitFullScreen", 16 "GeckoView:ClearMatches", 17 "GeckoView:DisplayMatches", 18 "GeckoView:FindInPage", 19 "GeckoView:HasCookieBannerRuleForBrowsingContextTree", 20 "GeckoView:RestoreState", 21 "GeckoView:ContainsFormData", 22 "GeckoView:ScrollBy", 23 "GeckoView:ScrollTo", 24 "GeckoView:SetActive", 25 "GeckoView:SetFocused", 26 "GeckoView:SetPriorityHint", 27 "GeckoView:UpdateInitData", 28 "GeckoView:ZoomToInput", 29 "GeckoView:IsPdfJs", 30 "GeckoView:GetWebCompatInfo", 31 "GeckoView:SendMoreWebCompatInfo", 32 "GeckoView:GetTorCircuit", 33 "GeckoView:NewTorCircuit", 34 ]); 35 } 36 37 onEnable() { 38 this.window.addEventListener( 39 "MozDOMFullscreen:Entered", 40 this, 41 /* capture */ true, 42 /* untrusted */ false 43 ); 44 this.window.addEventListener( 45 "MozDOMFullscreen:Exited", 46 this, 47 /* capture */ true, 48 /* untrusted */ false 49 ); 50 this.window.addEventListener( 51 "framefocusrequested", 52 this, 53 /* capture */ true, 54 /* untrusted */ false 55 ); 56 57 this.window.addEventListener("DOMWindowClose", this); 58 this.window.addEventListener("pagetitlechanged", this); 59 this.window.addEventListener("pageinfo", this); 60 61 this.window.addEventListener("cookiebannerdetected", this); 62 this.window.addEventListener("cookiebannerhandled", this); 63 64 Services.obs.addObserver(this, "oop-frameloader-crashed"); 65 Services.obs.addObserver(this, "ipc:content-shutdown"); 66 } 67 68 onDisable() { 69 this.window.removeEventListener( 70 "MozDOMFullscreen:Entered", 71 this, 72 /* capture */ true 73 ); 74 this.window.removeEventListener( 75 "MozDOMFullscreen:Exited", 76 this, 77 /* capture */ true 78 ); 79 this.window.removeEventListener( 80 "framefocusrequested", 81 this, 82 /* capture */ true 83 ); 84 85 this.window.removeEventListener("DOMWindowClose", this); 86 this.window.removeEventListener("pagetitlechanged", this); 87 this.window.removeEventListener("pageinfo", this); 88 89 this.window.removeEventListener("cookiebannerdetected", this); 90 this.window.removeEventListener("cookiebannerhandled", this); 91 92 Services.obs.removeObserver(this, "oop-frameloader-crashed"); 93 Services.obs.removeObserver(this, "ipc:content-shutdown"); 94 } 95 96 get actor() { 97 return this.getActor("GeckoViewContent"); 98 } 99 100 get isPdfJs() { 101 return ( 102 this.browser.contentPrincipal.spec === "resource://pdf.js/web/viewer.html" 103 ); 104 } 105 106 // Goes up the browsingContext chain and sends the message every time 107 // we cross the process boundary so that every process in the chain is 108 // notified. 109 sendToAllChildren(aEvent, aData) { 110 let { browsingContext } = this.actor; 111 112 while (browsingContext) { 113 if (!browsingContext.currentWindowGlobal) { 114 break; 115 } 116 117 const currentPid = browsingContext.currentWindowGlobal.osPid; 118 const parentPid = browsingContext.parent?.currentWindowGlobal.osPid; 119 120 if (currentPid != parentPid) { 121 const actor = 122 browsingContext.currentWindowGlobal.getActor("GeckoViewContent"); 123 actor.sendAsyncMessage(aEvent, aData); 124 } 125 126 browsingContext = browsingContext.parent; 127 } 128 } 129 130 #sendEnterDOMFullscreenEvent(aRequestOrigin) { 131 // Track the actors that are involved in the fullscreen request. And we will 132 // use them to send the exit message when the fullscreen is exited. 133 this._fullscreenRequest = { actors: [] }; 134 135 let currentBC = aRequestOrigin.browsingContext; 136 let currentPid = currentBC.currentWindowGlobal.osPid; 137 let parentBC = currentBC.parent; 138 139 while (parentBC) { 140 if (!parentBC.currentWindowGlobal) { 141 break; 142 } 143 144 const parentPid = parentBC.currentWindowGlobal.osPid; 145 if (currentPid != parentPid) { 146 const actor = parentBC.currentWindowGlobal.getActor("GeckoViewContent"); 147 actor.sendAsyncMessage("GeckoView:DOMFullscreenEntered", { 148 remoteFrameBC: currentBC, 149 }); 150 this._fullscreenRequest.actors.push(actor); 151 currentPid = parentPid; 152 } 153 154 currentBC = parentBC; 155 parentBC = parentBC.parent; 156 } 157 158 const actor = 159 aRequestOrigin.browsingContext.currentWindowGlobal.getActor( 160 "GeckoViewContent" 161 ); 162 actor.sendAsyncMessage("GeckoView:DOMFullscreenEntered", {}); 163 this._fullscreenRequest.actors.push(actor); 164 } 165 166 #sendExitDOMFullScreenEvent() { 167 if (!this._fullscreenRequest) { 168 return; 169 } 170 171 for (const actor of this._fullscreenRequest.actors) { 172 if ( 173 !actor.hasBeenDestroyed() && 174 actor.windowContext && 175 !actor.windowContext.isInBFCache 176 ) { 177 actor.sendAsyncMessage("GeckoView:DOMFullscreenExited", {}); 178 } 179 } 180 delete this._fullscreenRequest; 181 } 182 183 // Bundle event handler. 184 onEvent(aEvent, aData, aCallback) { 185 debug`onEvent: event=${aEvent}, data=${aData}`; 186 187 switch (aEvent) { 188 case "GeckoViewContent:ExitFullScreen": 189 this.browser.ownerDocument.exitFullscreen(); 190 break; 191 case "GeckoView:ClearMatches": { 192 if (!this.isPdfJs) { 193 this._clearMatches(); 194 } 195 break; 196 } 197 case "GeckoView:DisplayMatches": { 198 if (!this.isPdfJs) { 199 this._displayMatches(aData); 200 } 201 break; 202 } 203 case "GeckoView:FindInPage": { 204 if (!this.isPdfJs) { 205 this._findInPage(aData, aCallback); 206 } 207 break; 208 } 209 case "GeckoView:ZoomToInput": { 210 const sendZoomToFocusedInputMessage = function () { 211 // For ZoomToInput we just need to send the message to the current focused one. 212 const actor = 213 Services.focus.focusedContentBrowsingContext.currentWindowGlobal.getActor( 214 "GeckoViewContent" 215 ); 216 217 actor.sendAsyncMessage(aEvent, aData); 218 }; 219 220 const { force } = aData; 221 let gotResize = false; 222 const onResize = () => { 223 gotResize = true; 224 if (this.window.windowUtils.isMozAfterPaintPending) { 225 this.window.addEventListener( 226 "MozAfterPaint", 227 () => sendZoomToFocusedInputMessage(), 228 { capture: true, once: true } 229 ); 230 } else { 231 sendZoomToFocusedInputMessage(); 232 } 233 }; 234 235 this.window.addEventListener("resize", onResize, { capture: true }); 236 237 // When the keyboard is displayed, we can get one resize event, 238 // multiple resize events, or none at all. Try to handle all these 239 // cases by allowing resizing within a set interval, and still zoom to 240 // input if there is no resize event at the end of the interval. 241 this.window.setTimeout(() => { 242 this.window.removeEventListener("resize", onResize, { 243 capture: true, 244 }); 245 if (!gotResize && force) { 246 onResize(); 247 } 248 }, 500); 249 break; 250 } 251 case "GeckoView:ScrollBy": 252 // Unclear if that actually works with oop iframes? 253 this.sendToAllChildren(aEvent, aData); 254 break; 255 case "GeckoView:ScrollTo": 256 // Unclear if that actually works with oop iframes? 257 this.sendToAllChildren(aEvent, aData); 258 break; 259 case "GeckoView:UpdateInitData": 260 this.sendToAllChildren(aEvent, aData); 261 break; 262 case "GeckoView:SetActive": 263 this.browser.docShellIsActive = !!aData.active; 264 break; 265 case "GeckoView:SetFocused": 266 if (aData.focused) { 267 this.browser.focus(); 268 this.browser.setAttribute("primary", "true"); 269 } else { 270 this.browser.removeAttribute("primary"); 271 this.browser.blur(); 272 } 273 break; 274 case "GeckoView:SetPriorityHint": 275 if (this.browser.isRemoteBrowser) { 276 const remoteTab = this.browser.frameLoader?.remoteTab; 277 if (remoteTab) { 278 remoteTab.priorityHint = aData.priorityHint; 279 } 280 } 281 break; 282 case "GeckoView:RestoreState": 283 this.actor.restoreState(aData); 284 break; 285 case "GeckoView:ContainsFormData": 286 this._containsFormData(aCallback); 287 break; 288 case "GeckoView:GetWebCompatInfo": 289 this._getWebCompatInfo(aCallback); 290 break; 291 case "GeckoView:SendMoreWebCompatInfo": 292 this._sendMoreWebCompatInfo(aData, aCallback); 293 break; 294 case "GeckoView:IsPdfJs": 295 aCallback.onSuccess(this.isPdfJs); 296 break; 297 case "GeckoView:HasCookieBannerRuleForBrowsingContextTree": 298 this._hasCookieBannerRuleForBrowsingContextTree(aCallback); 299 break; 300 case "GeckoView:GetTorCircuits": 301 this._getTorCircuits(aCallback); 302 break; 303 case "GeckoView:NewTorCircuit": 304 this._newTorCircuit(aCallback); 305 break; 306 } 307 } 308 309 // DOM event handler 310 handleEvent(aEvent) { 311 debug`handleEvent: ${aEvent.type}`; 312 313 switch (aEvent.type) { 314 case "framefocusrequested": 315 if (this.browser != aEvent.target) { 316 return; 317 } 318 if (this.browser.hasAttribute("primary")) { 319 return; 320 } 321 this.eventDispatcher.sendRequest({ 322 type: "GeckoView:FocusRequest", 323 }); 324 aEvent.preventDefault(); 325 break; 326 case "MozDOMFullscreen:Entered": 327 if (this.browser == aEvent.target) { 328 const chromeWindow = this.browser.ownerGlobal; 329 const requestOrigin = 330 chromeWindow.browsingContext?.fullscreenRequestOrigin?.get(); 331 if (!requestOrigin) { 332 chromeWindow.document.exitFullscreen(); 333 return; 334 } 335 336 // Remote browser; dispatch to content process. 337 this.#sendEnterDOMFullscreenEvent(requestOrigin); 338 } 339 break; 340 case "MozDOMFullscreen:Exited": 341 this.#sendExitDOMFullScreenEvent(); 342 break; 343 case "pagetitlechanged": 344 this.eventDispatcher.sendRequest({ 345 type: "GeckoView:PageTitleChanged", 346 title: this.browser.contentTitle, 347 }); 348 break; 349 case "DOMWindowClose": 350 // We need this because we want to allow the app 351 // to close the window itself. If we don't preventDefault() 352 // here Gecko will close it immediately. 353 aEvent.preventDefault(); 354 355 this.eventDispatcher.sendRequest({ 356 type: "GeckoView:DOMWindowClose", 357 }); 358 break; 359 case "pageinfo": 360 if (aEvent.detail.previewImageURL) { 361 this.eventDispatcher.sendRequest({ 362 type: "GeckoView:PreviewImage", 363 previewImageUrl: aEvent.detail.previewImageURL, 364 }); 365 } 366 break; 367 case "cookiebannerdetected": 368 this.eventDispatcher.sendRequest({ 369 type: "GeckoView:CookieBannerEvent:Detected", 370 }); 371 break; 372 case "cookiebannerhandled": 373 this.eventDispatcher.sendRequest({ 374 type: "GeckoView:CookieBannerEvent:Handled", 375 }); 376 break; 377 } 378 } 379 380 // nsIObserver event handler 381 observe(aSubject, aTopic) { 382 debug`observe: ${aTopic}`; 383 this._contentCrashed = false; 384 const browser = aSubject.ownerElement; 385 386 switch (aTopic) { 387 case "oop-frameloader-crashed": { 388 if (!browser || browser != this.browser) { 389 return; 390 } 391 this.window.setTimeout(() => { 392 if (this._contentCrashed) { 393 this.eventDispatcher.sendRequest({ 394 type: "GeckoView:ContentCrash", 395 }); 396 } else { 397 this.eventDispatcher.sendRequest({ 398 type: "GeckoView:ContentKill", 399 }); 400 } 401 }, 250); 402 break; 403 } 404 case "ipc:content-shutdown": { 405 aSubject.QueryInterface(Ci.nsIPropertyBag2); 406 if (aSubject.get("dumpID")) { 407 if ( 408 browser && 409 aSubject.get("childID") != browser.frameLoader.childID 410 ) { 411 return; 412 } 413 this._contentCrashed = true; 414 } 415 break; 416 } 417 } 418 } 419 420 async _getWebCompatInfo(aCallback) { 421 if ( 422 Cu.isInAutomation && 423 Services.prefs.getBoolPref( 424 "browser.webcompat.geckoview.enableAllTestMocks", 425 false 426 ) 427 ) { 428 const mockResult = { 429 devicePixelRatio: 2.5, 430 antitracking: { hasTrackingContentBlocked: false }, 431 }; 432 aCallback.onSuccess(JSON.stringify(mockResult)); 433 return; 434 } 435 try { 436 const actor = 437 this.browser.browsingContext.currentWindowGlobal.getActor( 438 "ReportBrokenSite" 439 ); 440 const info = await actor.sendQuery("GetWebCompatInfo"); 441 442 // Stringify to convert potential non-ASCII 443 // characters in the returned web compat info map. 444 aCallback.onSuccess(JSON.stringify(info)); 445 } catch (error) { 446 aCallback.onError(`Cannot get web compat info, error: ${error}`); 447 } 448 } 449 450 async _sendMoreWebCompatInfo(aData, aCallback) { 451 if ( 452 Cu.isInAutomation && 453 Services.prefs.getBoolPref( 454 "browser.webcompat.geckoview.enableAllTestMocks", 455 false 456 ) 457 ) { 458 aCallback.onSuccess(); 459 return; 460 } 461 let infoObj = JSON.parse(aData.info); 462 try { 463 const actor = 464 this.browser.browsingContext.currentWindowGlobal.getActor( 465 "ReportBrokenSite" 466 ); 467 468 await actor.sendQuery("SendDataToWebcompatCom", infoObj); 469 aCallback.onSuccess(); 470 } catch (error) { 471 aCallback.onError(`Cannot send more web compat info, error: ${error}`); 472 } 473 } 474 475 _getTorCircuits(aCallback) { 476 if (this.browser && aCallback) { 477 const domain = lazy.TorDomainIsolator.getDomainForBrowser(this.browser); 478 const circuits = lazy.TorDomainIsolator.getCircuits( 479 this.browser, 480 domain, 481 this.browser.contentPrincipal.originAttributes.userContextId 482 ); 483 aCallback?.onSuccess({ domain, circuits }); 484 } else { 485 aCallback?.onSuccess(null); 486 } 487 } 488 489 _newTorCircuit(aCallback) { 490 lazy.TorDomainIsolator.newCircuitForBrowser(this.browser); 491 aCallback?.onSuccess(); 492 } 493 494 async _containsFormData(aCallback) { 495 aCallback.onSuccess(await this.actor.containsFormData()); 496 } 497 498 async _hasCookieBannerRuleForBrowsingContextTree(aCallback) { 499 const { browsingContext } = this.actor; 500 aCallback.onSuccess( 501 Services.cookieBanners.hasRuleForBrowsingContextTree(browsingContext) 502 ); 503 } 504 505 _findInPage(aData, aCallback) { 506 debug`findInPage: data=${aData} callback=${aCallback && "non-null"}`; 507 508 let finder; 509 try { 510 finder = this.browser.finder; 511 } catch (e) { 512 if (aCallback) { 513 aCallback.onError(`No finder: ${e}`); 514 } 515 return; 516 } 517 518 if (this._finderListener) { 519 finder.removeResultListener(this._finderListener); 520 } 521 522 this._finderListener = { 523 response: { 524 found: false, 525 wrapped: false, 526 current: 0, 527 total: -1, 528 searchString: aData.searchString || finder.searchString, 529 linkURL: null, 530 clientRect: null, 531 flags: { 532 backwards: !!aData.backwards, 533 linksOnly: !!aData.linksOnly, 534 matchCase: !!aData.matchCase, 535 wholeWord: !!aData.wholeWord, 536 }, 537 }, 538 539 onFindResult(aOptions) { 540 if (!aCallback || aOptions.searchString !== aData.searchString) { 541 // Result from a previous search. 542 return; 543 } 544 545 Object.assign(this.response, { 546 found: aOptions.result !== Ci.nsITypeAheadFind.FIND_NOTFOUND, 547 wrapped: aOptions.result !== Ci.nsITypeAheadFind.FIND_FOUND, 548 linkURL: aOptions.linkURL, 549 clientRect: aOptions.rect && { 550 left: aOptions.rect.left, 551 top: aOptions.rect.top, 552 right: aOptions.rect.right, 553 bottom: aOptions.rect.bottom, 554 }, 555 flags: { 556 backwards: aOptions.findBackwards, 557 linksOnly: aOptions.linksOnly, 558 matchCase: this.response.flags.matchCase, 559 wholeWord: this.response.flags.wholeWord, 560 }, 561 }); 562 563 if (!this.response.found) { 564 this.response.current = 0; 565 this.response.total = 0; 566 } 567 568 // Only send response if we have a count. 569 if (!this.response.found || this.response.current !== 0) { 570 debug`onFindResult: ${this.response}`; 571 aCallback.onSuccess(this.response); 572 aCallback = undefined; 573 } 574 }, 575 576 onMatchesCountResult(aResult) { 577 if (!aCallback || finder.searchString !== aData.searchString) { 578 // Result from a previous search. 579 return; 580 } 581 582 Object.assign(this.response, { 583 current: aResult.current, 584 total: aResult.total, 585 }); 586 587 // Only send response if we have a result. `found` and `wrapped` are 588 // both false only when we haven't received a result yet. 589 if (this.response.found || this.response.wrapped) { 590 debug`onMatchesCountResult: ${this.response}`; 591 aCallback.onSuccess(this.response); 592 aCallback = undefined; 593 } 594 }, 595 596 onCurrentSelection() {}, 597 598 onHighlightFinished() {}, 599 }; 600 601 finder.caseSensitive = !!aData.matchCase; 602 finder.entireWord = !!aData.wholeWord; 603 finder.matchDiacritics = !!aData.matchDiacritics; 604 finder.addResultListener(this._finderListener); 605 606 const drawOutline = 607 this._matchDisplayOptions && !!this._matchDisplayOptions.drawOutline; 608 609 if (!aData.searchString || aData.searchString === finder.searchString) { 610 // Search again. 611 aData.searchString = finder.searchString; 612 finder.findAgain( 613 aData.searchString, 614 !!aData.backwards, 615 !!aData.linksOnly, 616 drawOutline 617 ); 618 } else { 619 finder.fastFind(aData.searchString, !!aData.linksOnly, drawOutline); 620 } 621 } 622 623 _clearMatches() { 624 debug`clearMatches`; 625 626 let finder; 627 try { 628 finder = this.browser.finder; 629 } catch (e) { 630 return; 631 } 632 633 finder.removeSelection(); 634 finder.highlight(false); 635 636 if (this._finderListener) { 637 finder.removeResultListener(this._finderListener); 638 this._finderListener = null; 639 } 640 } 641 642 _displayMatches(aData) { 643 debug`displayMatches: data=${aData}`; 644 645 let finder; 646 try { 647 finder = this.browser.finder; 648 } catch (e) { 649 return; 650 } 651 652 this._matchDisplayOptions = aData; 653 finder.onModalHighlightChange(!!aData.dimPage); 654 finder.onHighlightAllChange(!!aData.highlightAll); 655 656 if (!aData.highlightAll && !aData.dimPage) { 657 finder.highlight(false); 658 return; 659 } 660 661 if (!this._finderListener || !finder.searchString) { 662 return; 663 } 664 const linksOnly = this._finderListener.response.linksOnly; 665 finder.highlight(true, finder.searchString, linksOnly, !!aData.drawOutline); 666 } 667 } 668 669 const { debug, warn } = GeckoViewContent.initLogging("GeckoViewContent");