tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

PointerLockManager.cpp (17305B)


      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/. */
      6 
      7 #include "PointerLockManager.h"
      8 
      9 #include "mozilla/AsyncEventDispatcher.h"
     10 #include "mozilla/EventStateManager.h"
     11 #include "mozilla/Logging.h"
     12 #include "mozilla/PresShell.h"
     13 #include "mozilla/ScopeExit.h"
     14 #include "mozilla/StaticPrefs_full_screen_api.h"
     15 #include "mozilla/dom/BindingDeclarations.h"
     16 #include "mozilla/dom/BrowserChild.h"
     17 #include "mozilla/dom/BrowserParent.h"
     18 #include "mozilla/dom/BrowsingContext.h"
     19 #include "mozilla/dom/CanonicalBrowsingContext.h"
     20 #include "mozilla/dom/Document.h"
     21 #include "mozilla/dom/Element.h"
     22 #include "mozilla/dom/PointerEventHandler.h"
     23 #include "mozilla/dom/WindowContext.h"
     24 #include "nsCOMPtr.h"
     25 #include "nsMenuPopupFrame.h"
     26 #include "nsSandboxFlags.h"
     27 
     28 mozilla::LazyLogModule gPointerLockLog("PointerLock");
     29 
     30 #define MOZ_POINTERLOCK_LOG(...) \
     31  MOZ_LOG(gPointerLockLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
     32 
     33 namespace mozilla {
     34 
     35 using mozilla::dom::BrowserChild;
     36 using mozilla::dom::BrowserParent;
     37 using mozilla::dom::BrowsingContext;
     38 using mozilla::dom::CallerType;
     39 using mozilla::dom::CanonicalBrowsingContext;
     40 using mozilla::dom::Document;
     41 using mozilla::dom::Element;
     42 using mozilla::dom::WindowContext;
     43 
     44 // Reference to the pointer locked element.
     45 constinit static nsWeakPtr sLockedElement;
     46 
     47 // Reference to the document which requested pointer lock.
     48 constinit static nsWeakPtr sLockedDoc;
     49 
     50 // Reference to the BrowserParent requested pointer lock.
     51 static BrowserParent* sLockedRemoteTarget = nullptr;
     52 
     53 /* static */
     54 bool PointerLockManager::sIsLocked = false;
     55 
     56 /* static */
     57 already_AddRefed<dom::Element> PointerLockManager::GetLockedElement() {
     58  nsCOMPtr<Element> element = do_QueryReferent(sLockedElement);
     59  return element.forget();
     60 }
     61 
     62 /* static */
     63 already_AddRefed<dom::Document> PointerLockManager::GetLockedDocument() {
     64  nsCOMPtr<Document> document = do_QueryReferent(sLockedDoc);
     65  return document.forget();
     66 }
     67 
     68 /* static */
     69 BrowserParent* PointerLockManager::GetLockedRemoteTarget() {
     70  MOZ_ASSERT(XRE_IsParentProcess());
     71  return sLockedRemoteTarget;
     72 }
     73 
     74 static void DispatchPointerLockChange(Document* aTarget) {
     75  if (!aTarget) {
     76    return;
     77  }
     78 
     79  MOZ_POINTERLOCK_LOG("Dispatch pointerlockchange event [document=0x%p]",
     80                      aTarget);
     81  RefPtr<AsyncEventDispatcher> asyncDispatcher =
     82      new AsyncEventDispatcher(aTarget, u"pointerlockchange"_ns,
     83                               CanBubble::eYes, ChromeOnlyDispatch::eNo);
     84  asyncDispatcher->PostDOMEvent();
     85 }
     86 
     87 static void DispatchPointerLockError(Document* aTarget, const char* aMessage) {
     88  if (!aTarget) {
     89    return;
     90  }
     91 
     92  MOZ_POINTERLOCK_LOG(
     93      "Dispatch pointerlockerror event [document=0x%p, message=%s]", aTarget,
     94      aMessage);
     95  RefPtr<AsyncEventDispatcher> asyncDispatcher =
     96      new AsyncEventDispatcher(aTarget, u"pointerlockerror"_ns, CanBubble::eYes,
     97                               ChromeOnlyDispatch::eNo);
     98  asyncDispatcher->PostDOMEvent();
     99  nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns,
    100                                  aTarget, nsContentUtils::eDOM_PROPERTIES,
    101                                  aMessage);
    102 }
    103 
    104 static bool IsPopupOpened() {
    105  // Check if any popup is open.
    106  nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
    107  if (!pm) {
    108    return false;
    109  }
    110 
    111  nsTArray<nsMenuPopupFrame*> popups;
    112  pm->GetVisiblePopups(popups, true);
    113 
    114  for (nsMenuPopupFrame* popup : popups) {
    115    if (popup->GetPopupType() != widget::PopupType::Tooltip) {
    116      return true;
    117    }
    118  }
    119 
    120  return false;
    121 }
    122 
    123 static const char* GetPointerLockError(Element* aElement, Element* aCurrentLock,
    124                                       bool aNoFocusCheck = false) {
    125  // Check if pointer lock pref is enabled
    126  if (!StaticPrefs::full_screen_api_pointer_lock_enabled()) {
    127    return "PointerLockDeniedDisabled";
    128  }
    129 
    130  nsCOMPtr<Document> ownerDoc = aElement->OwnerDoc();
    131  if (aCurrentLock && aCurrentLock->OwnerDoc() != ownerDoc) {
    132    return "PointerLockDeniedInUse";
    133  }
    134 
    135  if (!aElement->IsInComposedDoc()) {
    136    return "PointerLockDeniedNotInDocument";
    137  }
    138 
    139  if (ownerDoc->GetSandboxFlags() & SANDBOXED_POINTER_LOCK) {
    140    return "PointerLockDeniedSandboxed";
    141  }
    142 
    143  // Check if the element is in a document with a docshell.
    144  if (!ownerDoc->GetContainer()) {
    145    return "PointerLockDeniedHidden";
    146  }
    147  nsCOMPtr<nsPIDOMWindowOuter> ownerWindow = ownerDoc->GetWindow();
    148  if (!ownerWindow) {
    149    return "PointerLockDeniedHidden";
    150  }
    151  nsCOMPtr<nsPIDOMWindowInner> ownerInnerWindow = ownerDoc->GetInnerWindow();
    152  if (!ownerInnerWindow) {
    153    return "PointerLockDeniedHidden";
    154  }
    155  if (ownerWindow->GetCurrentInnerWindow() != ownerInnerWindow) {
    156    return "PointerLockDeniedHidden";
    157  }
    158 
    159  BrowsingContext* bc = ownerDoc->GetBrowsingContext();
    160  BrowsingContext* topBC = bc ? bc->Top() : nullptr;
    161  WindowContext* topWC = ownerDoc->GetTopLevelWindowContext();
    162  if (!topBC || !topBC->IsActive() || !topWC ||
    163      topWC != topBC->GetCurrentWindowContext()) {
    164    return "PointerLockDeniedHidden";
    165  }
    166 
    167  if (!aNoFocusCheck) {
    168    if (!IsInActiveTab(ownerDoc)) {
    169      return "PointerLockDeniedNotFocused";
    170    }
    171  }
    172 
    173  if (IsPopupOpened()) {
    174    return "PointerLockDeniedFailedToLock";
    175  }
    176 
    177  return nullptr;
    178 }
    179 
    180 /* static */
    181 void PointerLockManager::RequestLock(Element* aElement,
    182                                     CallerType aCallerType) {
    183  NS_ASSERTION(aElement,
    184               "Must pass non-null element to PointerLockManager::RequestLock");
    185 
    186  RefPtr<Document> doc = aElement->OwnerDoc();
    187  nsCOMPtr<Element> pointerLockedElement = GetLockedElement();
    188  MOZ_POINTERLOCK_LOG("Request lock on element 0x%p [document=0x%p]", aElement,
    189                      doc.get());
    190 
    191  if (aElement == pointerLockedElement) {
    192    DispatchPointerLockChange(doc);
    193    return;
    194  }
    195 
    196  if (const char* msg = GetPointerLockError(aElement, pointerLockedElement)) {
    197    DispatchPointerLockError(doc, msg);
    198    return;
    199  }
    200 
    201  bool userInputOrSystemCaller =
    202      doc->HasValidTransientUserGestureActivation() ||
    203      aCallerType == CallerType::System;
    204  nsCOMPtr<nsIRunnable> request =
    205      new PointerLockRequest(aElement, userInputOrSystemCaller);
    206  doc->Dispatch(request.forget());
    207 }
    208 
    209 /* static */
    210 void PointerLockManager::Unlock(const char* aReason, Document* aDoc) {
    211  if (sLockedRemoteTarget) {
    212    MOZ_ASSERT(XRE_IsParentProcess());
    213    MOZ_ASSERT(!sIsLocked);
    214    MOZ_POINTERLOCK_LOG(
    215        "Unlock document 0x%p [sLockedRemoteTarget=0x%p, reason=%s]", aDoc,
    216        sLockedRemoteTarget, aReason);
    217 
    218    if (aDoc) {
    219      CanonicalBrowsingContext* lockedBc =
    220          sLockedRemoteTarget->GetBrowsingContext();
    221      if (lockedBc &&
    222          lockedBc->TopCrossChromeBoundary()->GetExtantDocument() != aDoc) {
    223        return;
    224      }
    225    }
    226 
    227    (void)sLockedRemoteTarget->SendReleasePointerLock();
    228    sLockedRemoteTarget = nullptr;
    229    return;
    230  }
    231 
    232  if (!sIsLocked) {
    233    return;
    234  }
    235 
    236  nsCOMPtr<Document> pointerLockedDoc = GetLockedDocument();
    237  MOZ_POINTERLOCK_LOG("Unlock document 0x%p [LockedDocument=0x%p, reason=%s]",
    238                      aDoc, pointerLockedDoc.get(), aReason);
    239 
    240  if (!pointerLockedDoc || (aDoc && aDoc != pointerLockedDoc)) {
    241    return;
    242  }
    243  if (!SetPointerLock(nullptr, pointerLockedDoc, StyleCursorKind::Auto)) {
    244    return;
    245  }
    246 
    247  nsCOMPtr<Element> pointerLockedElement = GetLockedElement();
    248  ChangePointerLockedElement(nullptr, pointerLockedDoc, pointerLockedElement);
    249 
    250  if (BrowserChild* browserChild =
    251          BrowserChild::GetFrom(pointerLockedDoc->GetDocShell())) {
    252    browserChild->SendReleasePointerLock();
    253  }
    254 
    255  AsyncEventDispatcher::RunDOMEventWhenSafe(
    256      *pointerLockedElement, u"MozDOMPointerLock:Exited"_ns, CanBubble::eYes,
    257      ChromeOnlyDispatch::eYes);
    258 }
    259 
    260 /* static */
    261 void PointerLockManager::ChangePointerLockedElement(
    262    Element* aElement, Document* aDocument, Element* aPointerLockedElement) {
    263  // aDocument here is not really necessary, as it is the uncomposed
    264  // document of both aElement and aPointerLockedElement as far as one
    265  // is not nullptr, and they wouldn't both be nullptr in any case.
    266  // But since the caller of this function should have known what the
    267  // document is, we just don't try to figure out what it should be.
    268  MOZ_ASSERT(aDocument);
    269  MOZ_ASSERT(aElement != aPointerLockedElement);
    270  MOZ_POINTERLOCK_LOG("Change locked element from 0x%p to 0x%p [document=0x%p]",
    271                      aPointerLockedElement, aElement, aDocument);
    272  if (aPointerLockedElement) {
    273    MOZ_ASSERT(aPointerLockedElement->GetComposedDoc() == aDocument);
    274    aPointerLockedElement->ClearPointerLock();
    275  }
    276  if (aElement) {
    277    MOZ_ASSERT(aElement->GetComposedDoc() == aDocument);
    278    aElement->SetPointerLock();
    279    sLockedElement = do_GetWeakReference(aElement);
    280    sLockedDoc = do_GetWeakReference(aDocument);
    281    NS_ASSERTION(sLockedElement && sLockedDoc,
    282                 "aElement and this should support weak references!");
    283  } else {
    284    sLockedElement = nullptr;
    285    sLockedDoc = nullptr;
    286  }
    287  // Retarget all events to aElement via capture or
    288  // stop retargeting if aElement is nullptr.
    289  PresShell::SetCapturingContent(aElement, CaptureFlags::PointerLock);
    290  DispatchPointerLockChange(aDocument);
    291 }
    292 
    293 /* static */
    294 bool PointerLockManager::StartSetPointerLock(Element* aElement,
    295                                             Document* aDocument) {
    296  if (!SetPointerLock(aElement, aDocument, StyleCursorKind::None)) {
    297    DispatchPointerLockError(aDocument, "PointerLockDeniedFailedToLock");
    298    return false;
    299  }
    300 
    301  ChangePointerLockedElement(aElement, aDocument, nullptr);
    302  nsContentUtils::DispatchEventOnlyToChrome(
    303      aDocument, aElement, u"MozDOMPointerLock:Entered"_ns, CanBubble::eYes,
    304      Cancelable::eNo, /* DefaultAction */ nullptr);
    305 
    306  return true;
    307 }
    308 
    309 /* static */
    310 bool PointerLockManager::SetPointerLock(Element* aElement, Document* aDocument,
    311                                        StyleCursorKind aCursorStyle) {
    312  MOZ_ASSERT(!aElement || aElement->OwnerDoc() == aDocument,
    313             "We should be either unlocking pointer (aElement is nullptr), "
    314             "or locking pointer to an element in this document");
    315 #ifdef DEBUG
    316  if (!aElement) {
    317    nsCOMPtr<Document> pointerLockedDoc = GetLockedDocument();
    318    MOZ_ASSERT(pointerLockedDoc == aDocument);
    319  }
    320 #endif
    321 
    322  PresShell* presShell = aDocument->GetPresShell();
    323  if (!presShell) {
    324    NS_WARNING("SetPointerLock(): No PresShell");
    325    if (!aElement) {
    326      sIsLocked = false;
    327      // If we are unlocking pointer lock, but for some reason the doc
    328      // has already detached from the presshell, just ask the event
    329      // state manager to release the pointer.
    330      EventStateManager::SetPointerLock(nullptr, nullptr);
    331      return true;
    332    }
    333    return false;
    334  }
    335  RefPtr<nsPresContext> presContext = presShell->GetPresContext();
    336  if (!presContext) {
    337    NS_WARNING("SetPointerLock(): Unable to get PresContext");
    338    return false;
    339  }
    340 
    341  nsCOMPtr<nsIWidget> widget;
    342  nsIFrame* rootFrame = presShell->GetRootFrame();
    343  if (!NS_WARN_IF(!rootFrame)) {
    344    widget = rootFrame->GetNearestWidget();
    345    NS_WARNING_ASSERTION(widget,
    346                         "SetPointerLock(): Unable to find widget in "
    347                         "presShell->GetRootFrame()->GetNearestWidget();");
    348  }
    349 
    350  if (aElement && !widget) {
    351    NS_WARNING("SetPointerLock(): No Widget while requesting pointer lock");
    352    return false;
    353  }
    354 
    355  sIsLocked = !!aElement;
    356 
    357  // Hide the cursor and set pointer lock for future mouse events
    358  RefPtr<EventStateManager> esm = presContext->EventStateManager();
    359  esm->SetCursor(aCursorStyle, nullptr, {}, Nothing(), widget, true);
    360  EventStateManager::SetPointerLock(widget, presContext);
    361 
    362  return true;
    363 }
    364 
    365 /* static */
    366 bool PointerLockManager::IsInLockContext(BrowsingContext* aContext) {
    367  if (!aContext) {
    368    return false;
    369  }
    370 
    371  nsCOMPtr<Document> pointerLockedDoc = GetLockedDocument();
    372  if (!pointerLockedDoc || !pointerLockedDoc->GetBrowsingContext()) {
    373    return false;
    374  }
    375 
    376  BrowsingContext* lockTop = pointerLockedDoc->GetBrowsingContext()->Top();
    377  BrowsingContext* top = aContext->Top();
    378 
    379  return top == lockTop;
    380 }
    381 
    382 /* static */
    383 void PointerLockManager::SetLockedRemoteTarget(BrowserParent* aBrowserParent,
    384                                               nsACString& aError) {
    385  MOZ_ASSERT(XRE_IsParentProcess());
    386  if (sLockedRemoteTarget) {
    387    if (sLockedRemoteTarget != aBrowserParent) {
    388      aError = "PointerLockDeniedInUse"_ns;
    389    }
    390    return;
    391  }
    392 
    393  // Check if any popup is open.
    394  if (IsPopupOpened()) {
    395    aError = "PointerLockDeniedFailedToLock"_ns;
    396    return;
    397  }
    398 
    399  MOZ_POINTERLOCK_LOG("Set locked remote target to 0x%p", aBrowserParent);
    400  sLockedRemoteTarget = aBrowserParent;
    401  PointerEventHandler::ReleaseAllPointerCaptureRemoteTarget();
    402 }
    403 
    404 /* static */
    405 void PointerLockManager::ReleaseLockedRemoteTarget(
    406    BrowserParent* aBrowserParent) {
    407  MOZ_ASSERT(XRE_IsParentProcess());
    408  if (sLockedRemoteTarget == aBrowserParent) {
    409    MOZ_POINTERLOCK_LOG("Release locked remote target 0x%p",
    410                        sLockedRemoteTarget);
    411    sLockedRemoteTarget = nullptr;
    412  }
    413 }
    414 
    415 PointerLockManager::PointerLockRequest::PointerLockRequest(
    416    Element* aElement, bool aUserInputOrChromeCaller)
    417    : mozilla::Runnable("PointerLockRequest"),
    418      mElement(do_GetWeakReference(aElement)),
    419      mDocument(do_GetWeakReference(aElement->OwnerDoc())),
    420      mUserInputOrChromeCaller(aUserInputOrChromeCaller) {}
    421 
    422 NS_IMETHODIMP
    423 PointerLockManager::PointerLockRequest::Run() {
    424  nsCOMPtr<Element> element = do_QueryReferent(mElement);
    425  nsCOMPtr<Document> document = do_QueryReferent(mDocument);
    426 
    427  const char* error = nullptr;
    428  if (!element || !document || !element->GetComposedDoc()) {
    429    error = "PointerLockDeniedNotInDocument";
    430  } else if (element->GetComposedDoc() != document) {
    431    error = "PointerLockDeniedMovedDocument";
    432  }
    433  if (!error) {
    434    nsCOMPtr<Element> pointerLockedElement = do_QueryReferent(sLockedElement);
    435    if (element == pointerLockedElement) {
    436      DispatchPointerLockChange(document);
    437      return NS_OK;
    438    }
    439    // Note, we must bypass focus change, so pass true as the last parameter!
    440    error = GetPointerLockError(element, pointerLockedElement, true);
    441    // Another element in the same document is requesting pointer lock,
    442    // just grant it without user input check.
    443    if (!error && pointerLockedElement) {
    444      ChangePointerLockedElement(element, document, pointerLockedElement);
    445      return NS_OK;
    446    }
    447  }
    448  // If it is neither user input initiated, nor requested in fullscreen,
    449  // it should be rejected.
    450  if (!error && !mUserInputOrChromeCaller && !document->Fullscreen()) {
    451    error = "PointerLockDeniedNotInputDriven";
    452  }
    453 
    454  if (error) {
    455    DispatchPointerLockError(document, error);
    456    return NS_OK;
    457  }
    458 
    459  if (BrowserChild* browserChild =
    460          BrowserChild::GetFrom(document->GetDocShell())) {
    461    nsWeakPtr e = do_GetWeakReference(element);
    462    nsWeakPtr doc = do_GetWeakReference(element->OwnerDoc());
    463    nsWeakPtr bc = do_GetWeakReference(browserChild);
    464    browserChild->SendRequestPointerLock(
    465        [e, doc, bc](const nsCString& aError) {
    466          nsCOMPtr<Document> document = do_QueryReferent(doc);
    467          if (!aError.IsEmpty()) {
    468            DispatchPointerLockError(document, aError.get());
    469            return;
    470          }
    471 
    472          const char* error = nullptr;
    473          auto autoCleanup = MakeScopeExit([&] {
    474            if (error) {
    475              DispatchPointerLockError(document, error);
    476              // If we are failed to set pointer lock, notify parent to stop
    477              // redirect mouse event to this process.
    478              if (nsCOMPtr<nsIBrowserChild> browserChild =
    479                      do_QueryReferent(bc)) {
    480                static_cast<BrowserChild*>(browserChild.get())
    481                    ->SendReleasePointerLock();
    482              }
    483            }
    484          });
    485 
    486          nsCOMPtr<Element> element = do_QueryReferent(e);
    487          if (!element || !document || !element->GetComposedDoc()) {
    488            error = "PointerLockDeniedNotInDocument";
    489            return;
    490          }
    491 
    492          if (element->GetComposedDoc() != document) {
    493            error = "PointerLockDeniedMovedDocument";
    494            return;
    495          }
    496 
    497          nsCOMPtr<Element> pointerLockedElement = GetLockedElement();
    498          error = GetPointerLockError(element, pointerLockedElement, true);
    499          if (error) {
    500            return;
    501          }
    502 
    503          if (!StartSetPointerLock(element, document)) {
    504            error = "PointerLockDeniedFailedToLock";
    505            return;
    506          }
    507        },
    508        [doc](mozilla::ipc::ResponseRejectReason) {
    509          // IPC layer error
    510          nsCOMPtr<Document> document = do_QueryReferent(doc);
    511          if (!document) {
    512            return;
    513          }
    514 
    515          DispatchPointerLockError(document, "PointerLockDeniedFailedToLock");
    516        });
    517  } else {
    518    StartSetPointerLock(element, document);
    519  }
    520 
    521  return NS_OK;
    522 }
    523 
    524 }  // namespace mozilla