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/. */
5 import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs";
8 ChromeUtils.defineESModuleGetters(lazy, {
9 isProductURL: "chrome://global/content/shopping/ShoppingProduct.mjs",
10 ShoppingProduct: "chrome://global/content/shopping/ShoppingProduct.mjs",
13 export class GeckoViewContent extends GeckoViewModule {
15 this.registerListener([
16 "GeckoViewContent:ExitFullScreen",
17 "GeckoView:ClearMatches",
18 "GeckoView:DisplayMatches",
19 "GeckoView:FindInPage",
20 "GeckoView:HasCookieBannerRuleForBrowsingContextTree",
21 "GeckoView:RestoreState",
22 "GeckoView:ContainsFormData",
23 "GeckoView:RequestCreateAnalysis",
24 "GeckoView:RequestAnalysisStatus",
25 "GeckoView:RequestAnalysisCreationStatus",
26 "GeckoView:PollForAnalysisCompleted",
27 "GeckoView:SendClickAttributionEvent",
28 "GeckoView:SendImpressionAttributionEvent",
29 "GeckoView:SendPlacementAttributionEvent",
30 "GeckoView:RequestAnalysis",
31 "GeckoView:RequestRecommendations",
32 "GeckoView:ReportBackInStock",
35 "GeckoView:SetActive",
36 "GeckoView:SetFocused",
37 "GeckoView:SetPriorityHint",
38 "GeckoView:UpdateInitData",
39 "GeckoView:ZoomToInput",
45 this.window.addEventListener(
46 "MozDOMFullscreen:Entered",
51 this.window.addEventListener(
52 "MozDOMFullscreen:Exited",
57 this.window.addEventListener(
58 "framefocusrequested",
64 this.window.addEventListener("DOMWindowClose", this);
65 this.window.addEventListener("pagetitlechanged", this);
66 this.window.addEventListener("pageinfo", this);
68 this.window.addEventListener("cookiebannerdetected", this);
69 this.window.addEventListener("cookiebannerhandled", this);
71 Services.obs.addObserver(this, "oop-frameloader-crashed");
72 Services.obs.addObserver(this, "ipc:content-shutdown");
76 this.window.removeEventListener(
77 "MozDOMFullscreen:Entered",
81 this.window.removeEventListener(
82 "MozDOMFullscreen:Exited",
86 this.window.removeEventListener(
87 "framefocusrequested",
92 this.window.removeEventListener("DOMWindowClose", this);
93 this.window.removeEventListener("pagetitlechanged", this);
94 this.window.removeEventListener("pageinfo", this);
96 this.window.removeEventListener("cookiebannerdetected", this);
97 this.window.removeEventListener("cookiebannerhandled", this);
99 Services.obs.removeObserver(this, "oop-frameloader-crashed");
100 Services.obs.removeObserver(this, "ipc:content-shutdown");
104 return this.getActor("GeckoViewContent");
109 this.browser.contentPrincipal.spec === "resource://pdf.js/web/viewer.html"
113 // Goes up the browsingContext chain and sends the message every time
114 // we cross the process boundary so that every process in the chain is
116 sendToAllChildren(aEvent, aData) {
117 let { browsingContext } = this.actor;
119 while (browsingContext) {
120 if (!browsingContext.currentWindowGlobal) {
124 const currentPid = browsingContext.currentWindowGlobal.osPid;
125 const parentPid = browsingContext.parent?.currentWindowGlobal.osPid;
127 if (currentPid != parentPid) {
129 browsingContext.currentWindowGlobal.getActor("GeckoViewContent");
130 actor.sendAsyncMessage(aEvent, aData);
133 browsingContext = browsingContext.parent;
137 #sendDOMFullScreenEventToAllChildren(aEvent) {
138 let { browsingContext } = this.actor;
140 while (browsingContext) {
141 if (!browsingContext.currentWindowGlobal) {
145 const currentPid = browsingContext.currentWindowGlobal.osPid;
146 const parentPid = browsingContext.parent?.currentWindowGlobal.osPid;
148 if (currentPid != parentPid) {
149 if (!browsingContext.parent) {
150 // Top level browsing context. Use origin actor (Bug 1505916).
151 const chromeBC = browsingContext.topChromeWindow?.browsingContext;
152 const requestOrigin = chromeBC?.fullscreenRequestOrigin?.get();
154 requestOrigin.browsingContext.currentWindowGlobal
155 .getActor("GeckoViewContent")
156 .sendAsyncMessage(aEvent, {});
157 delete chromeBC.fullscreenRequestOrigin;
162 browsingContext.currentWindowGlobal.getActor("GeckoViewContent");
163 actor.sendAsyncMessage(aEvent, {});
166 browsingContext = browsingContext.parent;
170 // Bundle event handler.
171 onEvent(aEvent, aData, aCallback) {
172 debug`onEvent: event=${aEvent}, data=${aData}`;
175 case "GeckoViewContent:ExitFullScreen":
176 this.browser.ownerDocument.exitFullscreen();
178 case "GeckoView:ClearMatches": {
180 this._clearMatches();
184 case "GeckoView:DisplayMatches": {
186 this._displayMatches(aData);
190 case "GeckoView:FindInPage": {
192 this._findInPage(aData, aCallback);
196 case "GeckoView:ZoomToInput": {
197 const sendZoomToFocusedInputMessage = function () {
198 // For ZoomToInput we just need to send the message to the current focused one.
200 Services.focus.focusedContentBrowsingContext.currentWindowGlobal.getActor(
204 actor.sendAsyncMessage(aEvent, aData);
207 const { force } = aData;
208 let gotResize = false;
209 const onResize = function () {
211 if (this.window.windowUtils.isMozAfterPaintPending) {
212 this.window.addEventListener(
214 () => sendZoomToFocusedInputMessage(),
215 { capture: true, once: true }
218 sendZoomToFocusedInputMessage();
222 this.window.addEventListener("resize", onResize, { capture: true });
224 // When the keyboard is displayed, we can get one resize event,
225 // multiple resize events, or none at all. Try to handle all these
226 // cases by allowing resizing within a set interval, and still zoom to
227 // input if there is no resize event at the end of the interval.
228 this.window.setTimeout(() => {
229 this.window.removeEventListener("resize", onResize, {
232 if (!gotResize && force) {
238 case "GeckoView:ScrollBy":
239 // Unclear if that actually works with oop iframes?
240 this.sendToAllChildren(aEvent, aData);
242 case "GeckoView:ScrollTo":
243 // Unclear if that actually works with oop iframes?
244 this.sendToAllChildren(aEvent, aData);
246 case "GeckoView:UpdateInitData":
247 this.sendToAllChildren(aEvent, aData);
249 case "GeckoView:SetActive":
250 this.browser.docShellIsActive = !!aData.active;
252 case "GeckoView:SetFocused":
254 this.browser.focus();
255 this.browser.setAttribute("primary", "true");
257 this.browser.removeAttribute("primary");
261 case "GeckoView:SetPriorityHint":
262 if (this.browser.isRemoteBrowser) {
263 const remoteTab = this.browser.frameLoader?.remoteTab;
265 remoteTab.priorityHint = aData.priorityHint;
269 case "GeckoView:RestoreState":
270 this.actor.restoreState(aData);
272 case "GeckoView:ContainsFormData":
273 this._containsFormData(aCallback);
275 case "GeckoView:RequestAnalysis":
276 this._requestAnalysis(aData, aCallback);
278 case "GeckoView:RequestCreateAnalysis":
279 this._requestCreateAnalysis(aData, aCallback);
281 case "GeckoView:RequestAnalysisStatus":
282 this._requestAnalysisStatus(aData, aCallback);
284 case "GeckoView:RequestAnalysisCreationStatus":
285 this._requestAnalysisCreationStatus(aData, aCallback);
287 case "GeckoView:PollForAnalysisCompleted":
288 this._pollForAnalysisCompleted(aData, aCallback);
290 case "GeckoView:SendClickAttributionEvent":
291 this._sendAttributionEvent("click", aData, aCallback);
293 case "GeckoView:SendImpressionAttributionEvent":
294 this._sendAttributionEvent("impression", aData, aCallback);
296 case "GeckoView:SendPlacementAttributionEvent":
297 this._sendAttributionEvent("placement", aData, aCallback);
299 case "GeckoView:RequestRecommendations":
300 this._requestRecommendations(aData, aCallback);
302 case "GeckoView:ReportBackInStock":
303 this._reportBackInStock(aData, aCallback);
305 case "GeckoView:IsPdfJs":
306 aCallback.onSuccess(this.isPdfJs);
308 case "GeckoView:HasCookieBannerRuleForBrowsingContextTree":
309 this._hasCookieBannerRuleForBrowsingContextTree(aCallback);
315 handleEvent(aEvent) {
316 debug`handleEvent: ${aEvent.type}`;
318 switch (aEvent.type) {
319 case "framefocusrequested":
320 if (this.browser != aEvent.target) {
323 if (this.browser.hasAttribute("primary")) {
326 this.eventDispatcher.sendRequest({
327 type: "GeckoView:FocusRequest",
329 aEvent.preventDefault();
331 case "MozDOMFullscreen:Entered":
332 if (this.browser == aEvent.target) {
333 // Remote browser; dispatch to content process.
334 this.#sendDOMFullScreenEventToAllChildren(
335 "GeckoView:DOMFullscreenEntered"
339 case "MozDOMFullscreen:Exited":
340 this.#sendDOMFullScreenEventToAllChildren(
341 "GeckoView:DOMFullscreenExited"
344 case "pagetitlechanged":
345 this.eventDispatcher.sendRequest({
346 type: "GeckoView:PageTitleChanged",
347 title: this.browser.contentTitle,
350 case "DOMWindowClose":
351 // We need this because we want to allow the app
352 // to close the window itself. If we don't preventDefault()
353 // here Gecko will close it immediately.
354 aEvent.preventDefault();
356 this.eventDispatcher.sendRequest({
357 type: "GeckoView:DOMWindowClose",
361 if (aEvent.detail.previewImageURL) {
362 this.eventDispatcher.sendRequest({
363 type: "GeckoView:PreviewImage",
364 previewImageUrl: aEvent.detail.previewImageURL,
368 case "cookiebannerdetected":
369 this.eventDispatcher.sendRequest({
370 type: "GeckoView:CookieBannerEvent:Detected",
373 case "cookiebannerhandled":
374 this.eventDispatcher.sendRequest({
375 type: "GeckoView:CookieBannerEvent:Handled",
381 // nsIObserver event handler
382 observe(aSubject, aTopic) {
383 debug`observe: ${aTopic}`;
384 this._contentCrashed = false;
385 const browser = aSubject.ownerElement;
388 case "oop-frameloader-crashed": {
389 if (!browser || browser != this.browser) {
392 this.window.setTimeout(() => {
393 if (this._contentCrashed) {
394 this.eventDispatcher.sendRequest({
395 type: "GeckoView:ContentCrash",
398 this.eventDispatcher.sendRequest({
399 type: "GeckoView:ContentKill",
405 case "ipc:content-shutdown": {
406 aSubject.QueryInterface(Ci.nsIPropertyBag2);
407 if (aSubject.get("dumpID")) {
410 aSubject.get("childID") != browser.frameLoader.childID
414 this._contentCrashed = true;
421 async _containsFormData(aCallback) {
422 aCallback.onSuccess(await this.actor.containsFormData());
425 async _requestAnalysis(aData, aCallback) {
427 Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
430 analysis_url: "https://www.example.com/mock_analysis_url",
431 product_id: "ABCDEFG123",
433 adjusted_rating: 4.5,
434 needs_analysis: true,
435 page_not_supported: true,
436 not_enough_reviews: true,
438 last_analysis_time: 12345,
439 deleted_product_reported: true,
440 deleted_product: true,
442 aCallback.onSuccess({ analysis });
445 const url = Services.io.newURI(aData.url);
446 if (!lazy.isProductURL(url)) {
447 aCallback.onError(`Cannot requestAnalysis on a non-product url.`);
449 const product = new lazy.ShoppingProduct(url);
450 const analysis = await product.requestAnalysis();
452 aCallback.onError(`Product analysis returned null.`);
455 aCallback.onSuccess({ analysis });
459 async _requestCreateAnalysis(aData, aCallback) {
461 Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
463 const status = "pending";
464 aCallback.onSuccess(status);
467 const url = Services.io.newURI(aData.url);
468 if (!lazy.isProductURL(url)) {
469 aCallback.onError(`Cannot requestCreateAnalysis on a non-product url.`);
471 const product = new lazy.ShoppingProduct(url);
472 const status = await product.requestCreateAnalysis();
474 aCallback.onError(`Creation of product analysis returned null.`);
477 aCallback.onSuccess(status.status);
481 async _requestAnalysisCreationStatus(aData, aCallback) {
483 Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
485 const status = "in_progress";
486 aCallback.onSuccess(status);
489 const url = Services.io.newURI(aData.url);
490 if (!lazy.isProductURL(url)) {
492 `Cannot requestAnalysisCreationStatus on a non-product url.`
495 const product = new lazy.ShoppingProduct(url);
496 const status = await product.requestAnalysisCreationStatus();
499 `Status of creation of product analysis returned null.`
503 aCallback.onSuccess(status.status);
507 async _requestAnalysisStatus(aData, aCallback) {
509 Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
511 const status = { status: "in_progress", progress: 90.9 };
512 aCallback.onSuccess({ status });
515 const url = Services.io.newURI(aData.url);
516 if (!lazy.isProductURL(url)) {
517 aCallback.onError(`Cannot requestAnalysisStatus on a non-product url.`);
519 const product = new lazy.ShoppingProduct(url);
520 const status = await product.requestAnalysisCreationStatus();
522 aCallback.onError(`Status of product analysis returned null.`);
525 aCallback.onSuccess({ status });
529 async _pollForAnalysisCompleted(aData, aCallback) {
530 const url = Services.io.newURI(aData.url);
531 if (!lazy.isProductURL(url)) {
533 `Cannot pollForAnalysisCompleted on a non-product url.`
536 const product = new lazy.ShoppingProduct(url);
537 const status = await product.pollForAnalysisCompleted();
540 `Polling the status of creation of product analysis returned null.`
544 aCallback.onSuccess(status.status);
548 async _sendAttributionEvent(aEvent, aData, aCallback) {
551 Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
553 result = { TEST_AID: "TEST_AID_RESPONSE" };
555 result = await lazy.ShoppingProduct.sendAttributionEvent(
561 if (!result || !(aData.aid in result) || !result[aData.aid]) {
562 aCallback.onSuccess(false);
565 aCallback.onSuccess(true);
568 async _requestRecommendations(aData, aCallback) {
570 Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
572 const recommendations = [
574 name: "Mock Product",
575 url: "https://example.com/mock_url",
576 image_url: "https://example.com/mock_image_url",
580 adjusted_rating: 3.5,
581 analysis_url: "https://example.com/mock_analysis_url",
586 aCallback.onSuccess({ recommendations });
589 const url = Services.io.newURI(aData.url);
590 if (!lazy.isProductURL(url)) {
591 aCallback.onError(`Cannot requestRecommendations on a non-product url.`);
593 const product = new lazy.ShoppingProduct(url);
594 const recommendations = await product.requestRecommendations();
595 if (!recommendations) {
596 aCallback.onError(`Product recommendations returned null.`);
599 aCallback.onSuccess({ recommendations });
603 async _reportBackInStock(aData, aCallback) {
605 Services.prefs.getBoolPref("geckoview.shopping.mock_test_response", false)
607 const message = "report created";
608 aCallback.onSuccess(message);
611 const url = Services.io.newURI(aData.url);
612 if (!lazy.isProductURL(url)) {
613 aCallback.onError(`Cannot reportBackInStock on a non-product url.`);
615 const product = new lazy.ShoppingProduct(url);
616 const message = await product.sendReport();
618 aCallback.onError(`Reporting back in stock returned null.`);
621 aCallback.onSuccess(message.message);
625 async _hasCookieBannerRuleForBrowsingContextTree(aCallback) {
626 const { browsingContext } = this.actor;
628 Services.cookieBanners.hasRuleForBrowsingContextTree(browsingContext)
632 _findInPage(aData, aCallback) {
633 debug`findInPage: data=${aData} callback=${aCallback && "non-null"}`;
637 finder = this.browser.finder;
640 aCallback.onError(`No finder: ${e}`);
645 if (this._finderListener) {
646 finder.removeResultListener(this._finderListener);
649 this._finderListener = {
655 searchString: aData.searchString || finder.searchString,
659 backwards: !!aData.backwards,
660 linksOnly: !!aData.linksOnly,
661 matchCase: !!aData.matchCase,
662 wholeWord: !!aData.wholeWord,
666 onFindResult(aOptions) {
667 if (!aCallback || aOptions.searchString !== aData.searchString) {
668 // Result from a previous search.
672 Object.assign(this.response, {
673 found: aOptions.result !== Ci.nsITypeAheadFind.FIND_NOTFOUND,
674 wrapped: aOptions.result !== Ci.nsITypeAheadFind.FIND_FOUND,
675 linkURL: aOptions.linkURL,
676 clientRect: aOptions.rect && {
677 left: aOptions.rect.left,
678 top: aOptions.rect.top,
679 right: aOptions.rect.right,
680 bottom: aOptions.rect.bottom,
683 backwards: aOptions.findBackwards,
684 linksOnly: aOptions.linksOnly,
685 matchCase: this.response.flags.matchCase,
686 wholeWord: this.response.flags.wholeWord,
690 if (!this.response.found) {
691 this.response.current = 0;
692 this.response.total = 0;
695 // Only send response if we have a count.
696 if (!this.response.found || this.response.current !== 0) {
697 debug`onFindResult: ${this.response}`;
698 aCallback.onSuccess(this.response);
699 aCallback = undefined;
703 onMatchesCountResult(aResult) {
704 if (!aCallback || finder.searchString !== aData.searchString) {
705 // Result from a previous search.
709 Object.assign(this.response, {
710 current: aResult.current,
711 total: aResult.total,
714 // Only send response if we have a result. `found` and `wrapped` are
715 // both false only when we haven't received a result yet.
716 if (this.response.found || this.response.wrapped) {
717 debug`onMatchesCountResult: ${this.response}`;
718 aCallback.onSuccess(this.response);
719 aCallback = undefined;
723 onCurrentSelection() {},
725 onHighlightFinished() {},
728 finder.caseSensitive = !!aData.matchCase;
729 finder.entireWord = !!aData.wholeWord;
730 finder.matchDiacritics = !!aData.matchDiacritics;
731 finder.addResultListener(this._finderListener);
734 this._matchDisplayOptions && !!this._matchDisplayOptions.drawOutline;
736 if (!aData.searchString || aData.searchString === finder.searchString) {
738 aData.searchString = finder.searchString;
746 finder.fastFind(aData.searchString, !!aData.linksOnly, drawOutline);
755 finder = this.browser.finder;
760 finder.removeSelection();
761 finder.highlight(false);
763 if (this._finderListener) {
764 finder.removeResultListener(this._finderListener);
765 this._finderListener = null;
769 _displayMatches(aData) {
770 debug`displayMatches: data=${aData}`;
774 finder = this.browser.finder;
779 this._matchDisplayOptions = aData;
780 finder.onModalHighlightChange(!!aData.dimPage);
781 finder.onHighlightAllChange(!!aData.highlightAll);
783 if (!aData.highlightAll && !aData.dimPage) {
784 finder.highlight(false);
788 if (!this._finderListener || !finder.searchString) {
791 const linksOnly = this._finderListener.response.linksOnly;
792 finder.highlight(true, finder.searchString, linksOnly, !!aData.drawOutline);
796 const { debug, warn } = GeckoViewContent.initLogging("GeckoViewContent");