diff options
Diffstat (limited to 'dom/base/PointerLockManager.cpp')
-rw-r--r-- | dom/base/PointerLockManager.cpp | 442 |
1 files changed, 442 insertions, 0 deletions
diff --git a/dom/base/PointerLockManager.cpp b/dom/base/PointerLockManager.cpp new file mode 100644 index 0000000000..38c395969f --- /dev/null +++ b/dom/base/PointerLockManager.cpp @@ -0,0 +1,442 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 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/. */ + +#include "PointerLockManager.h" + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/PresShell.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_full_screen_api.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/WindowContext.h" +#include "nsCOMPtr.h" +#include "nsSandboxFlags.h" + +namespace mozilla { + +using mozilla::dom::BrowserChild; +using mozilla::dom::BrowserParent; +using mozilla::dom::BrowsingContext; +using mozilla::dom::CallerType; +using mozilla::dom::Document; +using mozilla::dom::Element; +using mozilla::dom::WindowContext; + +// Reference to the pointer locked element. +static nsWeakPtr sLockedElement; + +// Reference to the document which requested pointer lock. +static nsWeakPtr sLockedDoc; + +// Reference to the BrowserParent requested pointer lock. +static BrowserParent* sLockedRemoteTarget = nullptr; + +/* static */ +bool PointerLockManager::sIsLocked = false; + +/* static */ +already_AddRefed<dom::Element> PointerLockManager::GetLockedElement() { + nsCOMPtr<Element> element = do_QueryReferent(sLockedElement); + return element.forget(); +} + +/* static */ +already_AddRefed<dom::Document> PointerLockManager::GetLockedDocument() { + nsCOMPtr<Document> document = do_QueryReferent(sLockedDoc); + return document.forget(); +} + +/* static */ +BrowserParent* PointerLockManager::GetLockedRemoteTarget() { + MOZ_ASSERT(XRE_IsParentProcess()); + return sLockedRemoteTarget; +} + +static void DispatchPointerLockChange(Document* aTarget) { + if (!aTarget) { + return; + } + + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(aTarget, u"pointerlockchange"_ns, + CanBubble::eYes, ChromeOnlyDispatch::eNo); + asyncDispatcher->PostDOMEvent(); +} + +static void DispatchPointerLockError(Document* aTarget, const char* aMessage) { + if (!aTarget) { + return; + } + + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(aTarget, u"pointerlockerror"_ns, CanBubble::eYes, + ChromeOnlyDispatch::eNo); + asyncDispatcher->PostDOMEvent(); + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, + aTarget, nsContentUtils::eDOM_PROPERTIES, + aMessage); +} + +static const char* GetPointerLockError(Element* aElement, Element* aCurrentLock, + bool aNoFocusCheck = false) { + // Check if pointer lock pref is enabled + if (!StaticPrefs::full_screen_api_pointer_lock_enabled()) { + return "PointerLockDeniedDisabled"; + } + + nsCOMPtr<Document> ownerDoc = aElement->OwnerDoc(); + if (aCurrentLock && aCurrentLock->OwnerDoc() != ownerDoc) { + return "PointerLockDeniedInUse"; + } + + if (!aElement->IsInComposedDoc()) { + return "PointerLockDeniedNotInDocument"; + } + + if (ownerDoc->GetSandboxFlags() & SANDBOXED_POINTER_LOCK) { + return "PointerLockDeniedSandboxed"; + } + + // Check if the element is in a document with a docshell. + if (!ownerDoc->GetContainer()) { + return "PointerLockDeniedHidden"; + } + nsCOMPtr<nsPIDOMWindowOuter> ownerWindow = ownerDoc->GetWindow(); + if (!ownerWindow) { + return "PointerLockDeniedHidden"; + } + nsCOMPtr<nsPIDOMWindowInner> ownerInnerWindow = ownerDoc->GetInnerWindow(); + if (!ownerInnerWindow) { + return "PointerLockDeniedHidden"; + } + if (ownerWindow->GetCurrentInnerWindow() != ownerInnerWindow) { + return "PointerLockDeniedHidden"; + } + + BrowsingContext* bc = ownerDoc->GetBrowsingContext(); + BrowsingContext* topBC = bc ? bc->Top() : nullptr; + WindowContext* topWC = ownerDoc->GetTopLevelWindowContext(); + if (!topBC || !topBC->IsActive() || !topWC || + topWC != topBC->GetCurrentWindowContext()) { + return "PointerLockDeniedHidden"; + } + + if (!aNoFocusCheck) { + if (!IsInActiveTab(ownerDoc)) { + return "PointerLockDeniedNotFocused"; + } + } + + return nullptr; +} + +/* static */ +void PointerLockManager::RequestLock(Element* aElement, + CallerType aCallerType) { + NS_ASSERTION(aElement, + "Must pass non-null element to PointerLockManager::RequestLock"); + + RefPtr<Document> doc = aElement->OwnerDoc(); + nsCOMPtr<Element> pointerLockedElement = GetLockedElement(); + if (aElement == pointerLockedElement) { + DispatchPointerLockChange(doc); + return; + } + + if (const char* msg = GetPointerLockError(aElement, pointerLockedElement)) { + DispatchPointerLockError(doc, msg); + return; + } + + bool userInputOrSystemCaller = + doc->HasValidTransientUserGestureActivation() || + aCallerType == CallerType::System; + nsCOMPtr<nsIRunnable> request = + new PointerLockRequest(aElement, userInputOrSystemCaller); + doc->Dispatch(request.forget()); +} + +/* static */ +void PointerLockManager::Unlock(Document* aDoc) { + if (!sIsLocked) { + return; + } + + nsCOMPtr<Document> pointerLockedDoc = GetLockedDocument(); + if (!pointerLockedDoc || (aDoc && aDoc != pointerLockedDoc)) { + return; + } + if (!SetPointerLock(nullptr, pointerLockedDoc, StyleCursorKind::Auto)) { + return; + } + + nsCOMPtr<Element> pointerLockedElement = GetLockedElement(); + ChangePointerLockedElement(nullptr, pointerLockedDoc, pointerLockedElement); + + if (BrowserChild* browserChild = + BrowserChild::GetFrom(pointerLockedDoc->GetDocShell())) { + browserChild->SendReleasePointerLock(); + } + + AsyncEventDispatcher::RunDOMEventWhenSafe( + *pointerLockedElement, u"MozDOMPointerLock:Exited"_ns, CanBubble::eYes, + ChromeOnlyDispatch::eYes); +} + +/* static */ +void PointerLockManager::ChangePointerLockedElement( + Element* aElement, Document* aDocument, Element* aPointerLockedElement) { + // aDocument here is not really necessary, as it is the uncomposed + // document of both aElement and aPointerLockedElement as far as one + // is not nullptr, and they wouldn't both be nullptr in any case. + // But since the caller of this function should have known what the + // document is, we just don't try to figure out what it should be. + MOZ_ASSERT(aDocument); + MOZ_ASSERT(aElement != aPointerLockedElement); + if (aPointerLockedElement) { + MOZ_ASSERT(aPointerLockedElement->GetComposedDoc() == aDocument); + aPointerLockedElement->ClearPointerLock(); + } + if (aElement) { + MOZ_ASSERT(aElement->GetComposedDoc() == aDocument); + aElement->SetPointerLock(); + sLockedElement = do_GetWeakReference(aElement); + sLockedDoc = do_GetWeakReference(aDocument); + NS_ASSERTION(sLockedElement && sLockedDoc, + "aElement and this should support weak references!"); + } else { + sLockedElement = nullptr; + sLockedDoc = nullptr; + } + // Retarget all events to aElement via capture or + // stop retargeting if aElement is nullptr. + PresShell::SetCapturingContent(aElement, CaptureFlags::PointerLock); + DispatchPointerLockChange(aDocument); +} + +/* static */ +bool PointerLockManager::StartSetPointerLock(Element* aElement, + Document* aDocument) { + if (!SetPointerLock(aElement, aDocument, StyleCursorKind::None)) { + DispatchPointerLockError(aDocument, "PointerLockDeniedFailedToLock"); + return false; + } + + ChangePointerLockedElement(aElement, aDocument, nullptr); + nsContentUtils::DispatchEventOnlyToChrome( + aDocument, aElement, u"MozDOMPointerLock:Entered"_ns, CanBubble::eYes, + Cancelable::eNo, /* DefaultAction */ nullptr); + + return true; +} + +/* static */ +bool PointerLockManager::SetPointerLock(Element* aElement, Document* aDocument, + StyleCursorKind aCursorStyle) { + MOZ_ASSERT(!aElement || aElement->OwnerDoc() == aDocument, + "We should be either unlocking pointer (aElement is nullptr), " + "or locking pointer to an element in this document"); +#ifdef DEBUG + if (!aElement) { + nsCOMPtr<Document> pointerLockedDoc = GetLockedDocument(); + MOZ_ASSERT(pointerLockedDoc == aDocument); + } +#endif + + PresShell* presShell = aDocument->GetPresShell(); + if (!presShell) { + NS_WARNING("SetPointerLock(): No PresShell"); + if (!aElement) { + sIsLocked = false; + // If we are unlocking pointer lock, but for some reason the doc + // has already detached from the presshell, just ask the event + // state manager to release the pointer. + EventStateManager::SetPointerLock(nullptr, nullptr); + return true; + } + return false; + } + RefPtr<nsPresContext> presContext = presShell->GetPresContext(); + if (!presContext) { + NS_WARNING("SetPointerLock(): Unable to get PresContext"); + return false; + } + + nsCOMPtr<nsIWidget> widget; + nsIFrame* rootFrame = presShell->GetRootFrame(); + if (!NS_WARN_IF(!rootFrame)) { + widget = rootFrame->GetNearestWidget(); + NS_WARNING_ASSERTION(widget, + "SetPointerLock(): Unable to find widget in " + "presShell->GetRootFrame()->GetNearestWidget();"); + if (aElement && !widget) { + return false; + } + } + + sIsLocked = !!aElement; + + // Hide the cursor and set pointer lock for future mouse events + RefPtr<EventStateManager> esm = presContext->EventStateManager(); + esm->SetCursor(aCursorStyle, nullptr, {}, Nothing(), widget, true); + EventStateManager::SetPointerLock(widget, presContext); + + return true; +} + +/* static */ +bool PointerLockManager::IsInLockContext(BrowsingContext* aContext) { + if (!aContext) { + return false; + } + + nsCOMPtr<Document> pointerLockedDoc = GetLockedDocument(); + if (!pointerLockedDoc || !pointerLockedDoc->GetBrowsingContext()) { + return false; + } + + BrowsingContext* lockTop = pointerLockedDoc->GetBrowsingContext()->Top(); + BrowsingContext* top = aContext->Top(); + + return top == lockTop; +} + +/* static */ +bool PointerLockManager::SetLockedRemoteTarget(BrowserParent* aBrowserParent) { + MOZ_ASSERT(XRE_IsParentProcess()); + if (sLockedRemoteTarget) { + return sLockedRemoteTarget == aBrowserParent; + } + + sLockedRemoteTarget = aBrowserParent; + return true; +} + +/* static */ +void PointerLockManager::ReleaseLockedRemoteTarget( + BrowserParent* aBrowserParent) { + MOZ_ASSERT(XRE_IsParentProcess()); + if (sLockedRemoteTarget == aBrowserParent) { + sLockedRemoteTarget = nullptr; + } +} + +PointerLockManager::PointerLockRequest::PointerLockRequest( + Element* aElement, bool aUserInputOrChromeCaller) + : mozilla::Runnable("PointerLockRequest"), + mElement(do_GetWeakReference(aElement)), + mDocument(do_GetWeakReference(aElement->OwnerDoc())), + mUserInputOrChromeCaller(aUserInputOrChromeCaller) {} + +NS_IMETHODIMP +PointerLockManager::PointerLockRequest::Run() { + nsCOMPtr<Element> element = do_QueryReferent(mElement); + nsCOMPtr<Document> document = do_QueryReferent(mDocument); + + const char* error = nullptr; + if (!element || !document || !element->GetComposedDoc()) { + error = "PointerLockDeniedNotInDocument"; + } else if (element->GetComposedDoc() != document) { + error = "PointerLockDeniedMovedDocument"; + } + if (!error) { + nsCOMPtr<Element> pointerLockedElement = do_QueryReferent(sLockedElement); + if (element == pointerLockedElement) { + DispatchPointerLockChange(document); + return NS_OK; + } + // Note, we must bypass focus change, so pass true as the last parameter! + error = GetPointerLockError(element, pointerLockedElement, true); + // Another element in the same document is requesting pointer lock, + // just grant it without user input check. + if (!error && pointerLockedElement) { + ChangePointerLockedElement(element, document, pointerLockedElement); + return NS_OK; + } + } + // If it is neither user input initiated, nor requested in fullscreen, + // it should be rejected. + if (!error && !mUserInputOrChromeCaller && !document->Fullscreen()) { + error = "PointerLockDeniedNotInputDriven"; + } + + if (error) { + DispatchPointerLockError(document, error); + return NS_OK; + } + + if (BrowserChild* browserChild = + BrowserChild::GetFrom(document->GetDocShell())) { + nsWeakPtr e = do_GetWeakReference(element); + nsWeakPtr doc = do_GetWeakReference(element->OwnerDoc()); + nsWeakPtr bc = do_GetWeakReference(browserChild); + browserChild->SendRequestPointerLock( + [e, doc, bc](const nsCString& aError) { + nsCOMPtr<Document> document = do_QueryReferent(doc); + if (!aError.IsEmpty()) { + DispatchPointerLockError(document, aError.get()); + return; + } + + const char* error = nullptr; + auto autoCleanup = MakeScopeExit([&] { + if (error) { + DispatchPointerLockError(document, error); + // If we are failed to set pointer lock, notify parent to stop + // redirect mouse event to this process. + if (nsCOMPtr<nsIBrowserChild> browserChild = + do_QueryReferent(bc)) { + static_cast<BrowserChild*>(browserChild.get()) + ->SendReleasePointerLock(); + } + } + }); + + nsCOMPtr<Element> element = do_QueryReferent(e); + if (!element || !document || !element->GetComposedDoc()) { + error = "PointerLockDeniedNotInDocument"; + return; + } + + if (element->GetComposedDoc() != document) { + error = "PointerLockDeniedMovedDocument"; + return; + } + + nsCOMPtr<Element> pointerLockedElement = GetLockedElement(); + error = GetPointerLockError(element, pointerLockedElement, true); + if (error) { + return; + } + + if (!StartSetPointerLock(element, document)) { + error = "PointerLockDeniedFailedToLock"; + return; + } + }, + [doc](mozilla::ipc::ResponseRejectReason) { + // IPC layer error + nsCOMPtr<Document> document = do_QueryReferent(doc); + if (!document) { + return; + } + + DispatchPointerLockError(document, "PointerLockDeniedFailedToLock"); + }); + } else { + StartSetPointerLock(element, document); + } + + return NS_OK; +} + +} // namespace mozilla |