/* vim: set ts=2 sw=2 sts=2 et tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ export class DOMFullscreenParent extends JSWindowActorParent { // These properties get set by browser-fullScreenAndPointerLock.js. // TODO: Bug 1743703 - Consider moving the messaging component of // browser-fullScreenAndPointerLock.js into the actor waitingForChildEnterFullscreen = false; waitingForChildExitFullscreen = false; // Cache the next message recipient actor and in-process browsing context that // is computed by _getNextMsgRecipientActor() of // browser-fullScreenAndPointerLock.js, this is used to ensure the fullscreen // cleanup messages goes the same route as fullscreen request, especially for // the cleanup that happens after actor is destroyed. // TODO: Bug 1743703 - Consider moving the messaging component of // browser-fullScreenAndPointerLock.js into the actor nextMsgRecipient = null; updateFullscreenWindowReference(aWindow) { if (aWindow.document.documentElement.hasAttribute("inDOMFullscreen")) { this._fullscreenWindow = aWindow; } else { delete this._fullscreenWindow; } } cleanupDomFullscreen(aWindow) { if (!aWindow.FullScreen) { return; } // If we don't need to wait for child reply, i.e. cleanupDomFullscreen // doesn't message to child, and we've exit the fullscreen, there won't be // DOMFullscreen:Painted message from child and it is possible that no more // paint would be triggered, so just notify fullscreen-painted observer. if ( !aWindow.FullScreen.cleanupDomFullscreen(this) && !aWindow.document.fullscreen ) { Services.obs.notifyObservers(aWindow, "fullscreen-painted"); } } /** * Clean up fullscreen state and resume chrome UI if window is in fullscreen * and this actor is the one where the original fullscreen enter or * exit request comes. */ _cleanupFullscreenStateAndResumeChromeUI(aWindow) { this.cleanupDomFullscreen(aWindow); if (this.requestOrigin == this && aWindow.document.fullscreen) { aWindow.windowUtils.remoteFrameFullscreenReverted(); } } didDestroy() { this._didDestroy = true; let window = this._fullscreenWindow; if (!window) { let topBrowsingContext = this.browsingContext.top; let browser = topBrowsingContext.embedderElement; if (!browser) { return; } if ( this.waitingForChildExitFullscreen || this.waitingForChildEnterFullscreen ) { this.waitingForChildExitFullscreen = false; this.waitingForChildEnterFullscreen = false; // We were destroyed while waiting for our DOMFullscreenChild to exit // or enter fullscreen, run cleanup steps anyway. this._cleanupFullscreenStateAndResumeChromeUI(browser.ownerGlobal); } if (this != this.requestOrigin) { // The current fullscreen requester should handle the fullsceen event // if any. this.removeListeners(browser.ownerGlobal); } return; } if (this.waitingForChildEnterFullscreen) { this.waitingForChildEnterFullscreen = false; if (window.document.fullscreen) { // We were destroyed while waiting for our DOMFullscreenChild // to transition to fullscreen so we abort the entire // fullscreen transition to prevent getting stuck in a // partial fullscreen state. We need to go through the // document since window.Fullscreen could be undefined // at this point. // // This could reject if we're not currently in fullscreen // so just ignore rejection. window.document.exitFullscreen().catch(() => {}); return; } this.cleanupDomFullscreen(window); } // Need to resume Chrome UI if the window is still in fullscreen UI // to avoid the window stays in fullscreen problem. (See Bug 1620341) if (window.document.documentElement.hasAttribute("inDOMFullscreen")) { this.cleanupDomFullscreen(window); if (window.windowUtils) { window.windowUtils.remoteFrameFullscreenReverted(); } } else if (this.waitingForChildExitFullscreen) { this.waitingForChildExitFullscreen = false; // We were destroyed while waiting for our DOMFullscreenChild to exit // run cleanup steps anyway. this._cleanupFullscreenStateAndResumeChromeUI(window); } this.updateFullscreenWindowReference(window); } receiveMessage(aMessage) { let topBrowsingContext = this.browsingContext.top; let browser = topBrowsingContext.embedderElement; if (!browser) { // No need to go further when the browser is not accessible anymore // (which can happen when the tab is closed for instance), return; } let window = browser.ownerGlobal; switch (aMessage.name) { case "DOMFullscreen:Request": { this.manager.fullscreen = true; this.waitingForChildExitFullscreen = false; this.requestOrigin = this; this.addListeners(window); window.windowUtils.remoteFrameFullscreenChanged(browser); break; } case "DOMFullscreen:NewOrigin": { // Don't show the warning if we've already exited fullscreen. if (window.document.fullscreen) { window.PointerlockFsWarning.showFullScreen( aMessage.data.originNoSuffix ); } this.updateFullscreenWindowReference(window); break; } case "DOMFullscreen:Entered": { this.manager.fullscreen = true; this.nextMsgRecipient = null; this.waitingForChildEnterFullscreen = false; window.FullScreen.enterDomFullscreen(browser, this); this.updateFullscreenWindowReference(window); break; } case "DOMFullscreen:Exit": { this.manager.fullscreen = false; this.waitingForChildEnterFullscreen = false; window.windowUtils.remoteFrameFullscreenReverted(); break; } case "DOMFullscreen:Exited": { this.manager.fullscreen = false; this.waitingForChildExitFullscreen = false; this.cleanupDomFullscreen(window); this.updateFullscreenWindowReference(window); break; } case "DOMFullscreen:Painted": { this.waitingForChildExitFullscreen = false; Services.obs.notifyObservers(window, "fullscreen-painted"); this.sendAsyncMessage("DOMFullscreen:Painted", {}); TelemetryStopwatch.finish("FULLSCREEN_CHANGE_MS"); break; } } } handleEvent(aEvent) { let window = aEvent.currentTarget.ownerGlobal; // We can not get the corresponding browsing context from actor if the actor // has already destroyed, so use event target to get browsing context // instead. let requestOrigin = window.browsingContext.fullscreenRequestOrigin?.get(); if (this != requestOrigin) { // The current fullscreen requester should handle the fullsceen event, // ignore them if we are not the current requester. this.removeListeners(window); return; } switch (aEvent.type) { case "MozDOMFullscreen:Entered": { // The event target is the element which requested the DOM // fullscreen. If we were entering DOM fullscreen for a remote // browser, the target would be the browser which was the parameter of // `remoteFrameFullscreenChanged` call. If the fullscreen // request was initiated from an in-process browser, we need // to get its corresponding browser here. let browser; if (aEvent.target.ownerGlobal == window) { browser = aEvent.target; } else { browser = aEvent.target.ownerGlobal.docShell.chromeEventHandler; } // Addon installation should be cancelled when entering fullscreen for security and usability reasons. // Installation prompts in fullscreen can trick the user into installing unwanted addons. // In fullscreen the notification box does not have a clear visual association with its parent anymore. if (window.gXPInstallObserver) { window.gXPInstallObserver.removeAllNotifications(browser); } TelemetryStopwatch.start("FULLSCREEN_CHANGE_MS"); window.FullScreen.enterDomFullscreen(browser, this); this.updateFullscreenWindowReference(window); if (!this.hasBeenDestroyed() && this.requestOrigin) { window.PointerlockFsWarning.showFullScreen( this.requestOrigin.manager.documentPrincipal.originNoSuffix ); } break; } case "MozDOMFullscreen:Exited": { TelemetryStopwatch.start("FULLSCREEN_CHANGE_MS"); // Make sure that the actor has not been destroyed before // accessing its browsing context. Otherwise, a error may // occur and hence cleanupDomFullscreen not executed, resulting // in the browser window being in an unstable state. // (Bug 1590138). if (!this.hasBeenDestroyed() && !this.requestOrigin) { this.requestOrigin = this; } this.cleanupDomFullscreen(window); this.updateFullscreenWindowReference(window); // If the document is supposed to be in fullscreen, keep the listener to wait for // further events. if (!this.manager.fullscreen) { this.removeListeners(window); } break; } } } addListeners(aWindow) { aWindow.addEventListener( "MozDOMFullscreen:Entered", this, /* useCapture */ true, /* wantsUntrusted */ false ); aWindow.addEventListener( "MozDOMFullscreen:Exited", this, /* useCapture */ true, /* wantsUntrusted */ false ); } removeListeners(aWindow) { aWindow.removeEventListener("MozDOMFullscreen:Entered", this, true); aWindow.removeEventListener("MozDOMFullscreen:Exited", this, true); } /** * Get the actor where the original fullscreen * enter or exit request comes from. */ get requestOrigin() { let chromeBC = this.browsingContext.topChromeWindow?.browsingContext; let requestOrigin = chromeBC?.fullscreenRequestOrigin; return requestOrigin && requestOrigin.get(); } /** * Store the actor where the original fullscreen * enter or exit request comes from in the top level * browsing context. */ set requestOrigin(aActor) { let chromeBC = this.browsingContext.topChromeWindow?.browsingContext; if (!chromeBC) { console.error("not able to get browsingContext for chrome window."); return; } if (aActor) { chromeBC.fullscreenRequestOrigin = Cu.getWeakReference(aActor); } else { delete chromeBC.fullscreenRequestOrigin; } } hasBeenDestroyed() { if (this._didDestroy) { return true; } // The 'didDestroy' callback is not always getting called. // So we can't rely on it here. Instead, we will try to access // the browsing context to judge wether the actor has // been destroyed or not. try { return !this.browsingContext; } catch { return true; } } }