1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "PointerLockManager.h"
9 #include "mozilla/AsyncEventDispatcher.h"
10 #include "mozilla/EventStateManager.h"
11 #include "mozilla/PresShell.h"
12 #include "mozilla/ScopeExit.h"
13 #include "mozilla/StaticPrefs_full_screen_api.h"
14 #include "mozilla/dom/BindingDeclarations.h"
15 #include "mozilla/dom/BrowserChild.h"
16 #include "mozilla/dom/BrowserParent.h"
17 #include "mozilla/dom/BrowsingContext.h"
18 #include "mozilla/dom/Document.h"
19 #include "mozilla/dom/Element.h"
20 #include "mozilla/dom/WindowContext.h"
22 #include "nsSandboxFlags.h"
26 using mozilla::dom::BrowserChild
;
27 using mozilla::dom::BrowserParent
;
28 using mozilla::dom::BrowsingContext
;
29 using mozilla::dom::CallerType
;
30 using mozilla::dom::Document
;
31 using mozilla::dom::Element
;
32 using mozilla::dom::WindowContext
;
34 // Reference to the pointer locked element.
35 static nsWeakPtr sLockedElement
;
37 // Reference to the document which requested pointer lock.
38 static nsWeakPtr sLockedDoc
;
40 // Reference to the BrowserParent requested pointer lock.
41 static BrowserParent
* sLockedRemoteTarget
= nullptr;
44 bool PointerLockManager::sIsLocked
= false;
47 already_AddRefed
<dom::Element
> PointerLockManager::GetLockedElement() {
48 nsCOMPtr
<Element
> element
= do_QueryReferent(sLockedElement
);
49 return element
.forget();
53 already_AddRefed
<dom::Document
> PointerLockManager::GetLockedDocument() {
54 nsCOMPtr
<Document
> document
= do_QueryReferent(sLockedDoc
);
55 return document
.forget();
59 BrowserParent
* PointerLockManager::GetLockedRemoteTarget() {
60 MOZ_ASSERT(XRE_IsParentProcess());
61 return sLockedRemoteTarget
;
64 static void DispatchPointerLockChange(Document
* aTarget
) {
69 RefPtr
<AsyncEventDispatcher
> asyncDispatcher
=
70 new AsyncEventDispatcher(aTarget
, u
"pointerlockchange"_ns
,
71 CanBubble::eYes
, ChromeOnlyDispatch::eNo
);
72 asyncDispatcher
->PostDOMEvent();
75 static void DispatchPointerLockError(Document
* aTarget
, const char* aMessage
) {
80 RefPtr
<AsyncEventDispatcher
> asyncDispatcher
=
81 new AsyncEventDispatcher(aTarget
, u
"pointerlockerror"_ns
, CanBubble::eYes
,
82 ChromeOnlyDispatch::eNo
);
83 asyncDispatcher
->PostDOMEvent();
84 nsContentUtils::ReportToConsole(nsIScriptError::warningFlag
, "DOM"_ns
,
85 aTarget
, nsContentUtils::eDOM_PROPERTIES
,
89 static const char* GetPointerLockError(Element
* aElement
, Element
* aCurrentLock
,
90 bool aNoFocusCheck
= false) {
91 // Check if pointer lock pref is enabled
92 if (!StaticPrefs::full_screen_api_pointer_lock_enabled()) {
93 return "PointerLockDeniedDisabled";
96 nsCOMPtr
<Document
> ownerDoc
= aElement
->OwnerDoc();
97 if (aCurrentLock
&& aCurrentLock
->OwnerDoc() != ownerDoc
) {
98 return "PointerLockDeniedInUse";
101 if (!aElement
->IsInComposedDoc()) {
102 return "PointerLockDeniedNotInDocument";
105 if (ownerDoc
->GetSandboxFlags() & SANDBOXED_POINTER_LOCK
) {
106 return "PointerLockDeniedSandboxed";
109 // Check if the element is in a document with a docshell.
110 if (!ownerDoc
->GetContainer()) {
111 return "PointerLockDeniedHidden";
113 nsCOMPtr
<nsPIDOMWindowOuter
> ownerWindow
= ownerDoc
->GetWindow();
115 return "PointerLockDeniedHidden";
117 nsCOMPtr
<nsPIDOMWindowInner
> ownerInnerWindow
= ownerDoc
->GetInnerWindow();
118 if (!ownerInnerWindow
) {
119 return "PointerLockDeniedHidden";
121 if (ownerWindow
->GetCurrentInnerWindow() != ownerInnerWindow
) {
122 return "PointerLockDeniedHidden";
125 BrowsingContext
* bc
= ownerDoc
->GetBrowsingContext();
126 BrowsingContext
* topBC
= bc
? bc
->Top() : nullptr;
127 WindowContext
* topWC
= ownerDoc
->GetTopLevelWindowContext();
128 if (!topBC
|| !topBC
->IsActive() || !topWC
||
129 topWC
!= topBC
->GetCurrentWindowContext()) {
130 return "PointerLockDeniedHidden";
133 if (!aNoFocusCheck
) {
134 if (!IsInActiveTab(ownerDoc
)) {
135 return "PointerLockDeniedNotFocused";
143 void PointerLockManager::RequestLock(Element
* aElement
,
144 CallerType aCallerType
) {
145 NS_ASSERTION(aElement
,
146 "Must pass non-null element to PointerLockManager::RequestLock");
148 RefPtr
<Document
> doc
= aElement
->OwnerDoc();
149 nsCOMPtr
<Element
> pointerLockedElement
= GetLockedElement();
150 if (aElement
== pointerLockedElement
) {
151 DispatchPointerLockChange(doc
);
155 if (const char* msg
= GetPointerLockError(aElement
, pointerLockedElement
)) {
156 DispatchPointerLockError(doc
, msg
);
160 bool userInputOrSystemCaller
=
161 doc
->HasValidTransientUserGestureActivation() ||
162 aCallerType
== CallerType::System
;
163 nsCOMPtr
<nsIRunnable
> request
=
164 new PointerLockRequest(aElement
, userInputOrSystemCaller
);
165 doc
->Dispatch(TaskCategory::Other
, request
.forget());
169 void PointerLockManager::Unlock(Document
* aDoc
) {
174 nsCOMPtr
<Document
> pointerLockedDoc
= GetLockedDocument();
175 if (!pointerLockedDoc
|| (aDoc
&& aDoc
!= pointerLockedDoc
)) {
178 if (!SetPointerLock(nullptr, pointerLockedDoc
, StyleCursorKind::Auto
)) {
182 nsCOMPtr
<Element
> pointerLockedElement
= GetLockedElement();
183 ChangePointerLockedElement(nullptr, pointerLockedDoc
, pointerLockedElement
);
185 if (BrowserChild
* browserChild
=
186 BrowserChild::GetFrom(pointerLockedDoc
->GetDocShell())) {
187 browserChild
->SendReleasePointerLock();
190 AsyncEventDispatcher::RunDOMEventWhenSafe(
191 *pointerLockedElement
, u
"MozDOMPointerLock:Exited"_ns
, CanBubble::eYes
,
192 ChromeOnlyDispatch::eYes
);
196 void PointerLockManager::ChangePointerLockedElement(
197 Element
* aElement
, Document
* aDocument
, Element
* aPointerLockedElement
) {
198 // aDocument here is not really necessary, as it is the uncomposed
199 // document of both aElement and aPointerLockedElement as far as one
200 // is not nullptr, and they wouldn't both be nullptr in any case.
201 // But since the caller of this function should have known what the
202 // document is, we just don't try to figure out what it should be.
203 MOZ_ASSERT(aDocument
);
204 MOZ_ASSERT(aElement
!= aPointerLockedElement
);
205 if (aPointerLockedElement
) {
206 MOZ_ASSERT(aPointerLockedElement
->GetComposedDoc() == aDocument
);
207 aPointerLockedElement
->ClearPointerLock();
210 MOZ_ASSERT(aElement
->GetComposedDoc() == aDocument
);
211 aElement
->SetPointerLock();
212 sLockedElement
= do_GetWeakReference(aElement
);
213 sLockedDoc
= do_GetWeakReference(aDocument
);
214 NS_ASSERTION(sLockedElement
&& sLockedDoc
,
215 "aElement and this should support weak references!");
217 sLockedElement
= nullptr;
218 sLockedDoc
= nullptr;
220 // Retarget all events to aElement via capture or
221 // stop retargeting if aElement is nullptr.
222 PresShell::SetCapturingContent(aElement
, CaptureFlags::PointerLock
);
223 DispatchPointerLockChange(aDocument
);
227 bool PointerLockManager::StartSetPointerLock(Element
* aElement
,
228 Document
* aDocument
) {
229 if (!SetPointerLock(aElement
, aDocument
, StyleCursorKind::None
)) {
230 DispatchPointerLockError(aDocument
, "PointerLockDeniedFailedToLock");
234 ChangePointerLockedElement(aElement
, aDocument
, nullptr);
235 nsContentUtils::DispatchEventOnlyToChrome(
236 aDocument
, ToSupports(aElement
), u
"MozDOMPointerLock:Entered"_ns
,
237 CanBubble::eYes
, Cancelable::eNo
, /* DefaultAction */ nullptr);
243 bool PointerLockManager::SetPointerLock(Element
* aElement
, Document
* aDocument
,
244 StyleCursorKind aCursorStyle
) {
245 MOZ_ASSERT(!aElement
|| aElement
->OwnerDoc() == aDocument
,
246 "We should be either unlocking pointer (aElement is nullptr), "
247 "or locking pointer to an element in this document");
250 nsCOMPtr
<Document
> pointerLockedDoc
= GetLockedDocument();
251 MOZ_ASSERT(pointerLockedDoc
== aDocument
);
255 PresShell
* presShell
= aDocument
->GetPresShell();
257 NS_WARNING("SetPointerLock(): No PresShell");
260 // If we are unlocking pointer lock, but for some reason the doc
261 // has already detached from the presshell, just ask the event
262 // state manager to release the pointer.
263 EventStateManager::SetPointerLock(nullptr, nullptr);
268 nsPresContext
* presContext
= presShell
->GetPresContext();
270 NS_WARNING("SetPointerLock(): Unable to get PresContext");
274 nsCOMPtr
<nsIWidget
> widget
;
275 nsIFrame
* rootFrame
= presShell
->GetRootFrame();
276 if (!NS_WARN_IF(!rootFrame
)) {
277 widget
= rootFrame
->GetNearestWidget();
278 NS_WARNING_ASSERTION(widget
,
279 "SetPointerLock(): Unable to find widget in "
280 "presShell->GetRootFrame()->GetNearestWidget();");
281 if (aElement
&& !widget
) {
286 sIsLocked
= !!aElement
;
288 // Hide the cursor and set pointer lock for future mouse events
289 RefPtr
<EventStateManager
> esm
= presContext
->EventStateManager();
290 esm
->SetCursor(aCursorStyle
, nullptr, {}, Nothing(), widget
, true);
291 EventStateManager::SetPointerLock(widget
, aElement
);
297 bool PointerLockManager::IsInLockContext(BrowsingContext
* aContext
) {
302 nsCOMPtr
<Document
> pointerLockedDoc
= GetLockedDocument();
303 if (!pointerLockedDoc
|| !pointerLockedDoc
->GetBrowsingContext()) {
307 BrowsingContext
* lockTop
= pointerLockedDoc
->GetBrowsingContext()->Top();
308 BrowsingContext
* top
= aContext
->Top();
310 return top
== lockTop
;
314 bool PointerLockManager::SetLockedRemoteTarget(BrowserParent
* aBrowserParent
) {
315 MOZ_ASSERT(XRE_IsParentProcess());
316 if (sLockedRemoteTarget
) {
317 return sLockedRemoteTarget
== aBrowserParent
;
320 sLockedRemoteTarget
= aBrowserParent
;
325 void PointerLockManager::ReleaseLockedRemoteTarget(
326 BrowserParent
* aBrowserParent
) {
327 MOZ_ASSERT(XRE_IsParentProcess());
328 if (sLockedRemoteTarget
== aBrowserParent
) {
329 sLockedRemoteTarget
= nullptr;
333 PointerLockManager::PointerLockRequest::PointerLockRequest(
334 Element
* aElement
, bool aUserInputOrChromeCaller
)
335 : mozilla::Runnable("PointerLockRequest"),
336 mElement(do_GetWeakReference(aElement
)),
337 mDocument(do_GetWeakReference(aElement
->OwnerDoc())),
338 mUserInputOrChromeCaller(aUserInputOrChromeCaller
) {}
341 PointerLockManager::PointerLockRequest::Run() {
342 nsCOMPtr
<Element
> element
= do_QueryReferent(mElement
);
343 nsCOMPtr
<Document
> document
= do_QueryReferent(mDocument
);
345 const char* error
= nullptr;
346 if (!element
|| !document
|| !element
->GetComposedDoc()) {
347 error
= "PointerLockDeniedNotInDocument";
348 } else if (element
->GetComposedDoc() != document
) {
349 error
= "PointerLockDeniedMovedDocument";
352 nsCOMPtr
<Element
> pointerLockedElement
= do_QueryReferent(sLockedElement
);
353 if (element
== pointerLockedElement
) {
354 DispatchPointerLockChange(document
);
357 // Note, we must bypass focus change, so pass true as the last parameter!
358 error
= GetPointerLockError(element
, pointerLockedElement
, true);
359 // Another element in the same document is requesting pointer lock,
360 // just grant it without user input check.
361 if (!error
&& pointerLockedElement
) {
362 ChangePointerLockedElement(element
, document
, pointerLockedElement
);
366 // If it is neither user input initiated, nor requested in fullscreen,
367 // it should be rejected.
368 if (!error
&& !mUserInputOrChromeCaller
&& !document
->Fullscreen()) {
369 error
= "PointerLockDeniedNotInputDriven";
373 DispatchPointerLockError(document
, error
);
377 if (BrowserChild
* browserChild
=
378 BrowserChild::GetFrom(document
->GetDocShell())) {
379 nsWeakPtr e
= do_GetWeakReference(element
);
380 nsWeakPtr doc
= do_GetWeakReference(element
->OwnerDoc());
381 nsWeakPtr bc
= do_GetWeakReference(browserChild
);
382 browserChild
->SendRequestPointerLock(
383 [e
, doc
, bc
](const nsCString
& aError
) {
384 nsCOMPtr
<Document
> document
= do_QueryReferent(doc
);
385 if (!aError
.IsEmpty()) {
386 DispatchPointerLockError(document
, aError
.get());
390 const char* error
= nullptr;
391 auto autoCleanup
= MakeScopeExit([&] {
393 DispatchPointerLockError(document
, error
);
394 // If we are failed to set pointer lock, notify parent to stop
395 // redirect mouse event to this process.
396 if (nsCOMPtr
<nsIBrowserChild
> browserChild
=
397 do_QueryReferent(bc
)) {
398 static_cast<BrowserChild
*>(browserChild
.get())
399 ->SendReleasePointerLock();
404 nsCOMPtr
<Element
> element
= do_QueryReferent(e
);
405 if (!element
|| !document
|| !element
->GetComposedDoc()) {
406 error
= "PointerLockDeniedNotInDocument";
410 if (element
->GetComposedDoc() != document
) {
411 error
= "PointerLockDeniedMovedDocument";
415 nsCOMPtr
<Element
> pointerLockedElement
= GetLockedElement();
416 error
= GetPointerLockError(element
, pointerLockedElement
, true);
421 if (!StartSetPointerLock(element
, document
)) {
422 error
= "PointerLockDeniedFailedToLock";
426 [doc
](mozilla::ipc::ResponseRejectReason
) {
428 nsCOMPtr
<Document
> document
= do_QueryReferent(doc
);
433 DispatchPointerLockError(document
, "PointerLockDeniedFailedToLock");
436 StartSetPointerLock(element
, document
);
442 } // namespace mozilla