| /* |
| * Copyright (C) 1999 Lars Knoll (knoll@kde.org) |
| * (C) 1999 Antti Koivisto (koivisto@kde.org) |
| * (C) 2001 Dirk Mueller (mueller@kde.org) |
| * (C) 2006 Alexey Proskuryakov (ap@webkit.org) |
| * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012 Apple Inc. All |
| * rights reserved. |
| * Copyright (C) 2008, 2009 Torch Mobile Inc. All rights reserved. |
| * (http://www.torchmobile.com/) |
| * Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies) |
| * Copyright (C) 2013 Google Inc. All rights reserved. |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Library General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Library General Public License for more details. |
| * |
| * You should have received a copy of the GNU Library General Public License |
| * along with this library; see the file COPYING.LIB. If not, write to |
| * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| * |
| */ |
| |
| #include "third_party/blink/renderer/core/fullscreen/fullscreen.h" |
| |
| #include "base/macros.h" |
| #include "third_party/blink/public/mojom/feature_policy/feature_policy.mojom-blink.h" |
| #include "third_party/blink/public/platform/task_type.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_fullscreen_options.h" |
| #include "third_party/blink/renderer/core/css/style_change_reason.h" |
| #include "third_party/blink/renderer/core/css/style_engine.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/dom/element_traversal.h" |
| #include "third_party/blink/renderer/core/dom/events/event.h" |
| #include "third_party/blink/renderer/core/dom/shadow_root.h" |
| #include "third_party/blink/renderer/core/frame/local_dom_window.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/local_frame_view.h" |
| #include "third_party/blink/renderer/core/frame/settings.h" |
| #include "third_party/blink/renderer/core/frame/visual_viewport.h" |
| #include "third_party/blink/renderer/core/fullscreen/scoped_allow_fullscreen.h" |
| #include "third_party/blink/renderer/core/html/html_body_element.h" |
| #include "third_party/blink/renderer/core/html/html_iframe_element.h" |
| #include "third_party/blink/renderer/core/html_element_type_helpers.h" |
| #include "third_party/blink/renderer/core/input/event_handler.h" |
| #include "third_party/blink/renderer/core/inspector/console_message.h" |
| #include "third_party/blink/renderer/core/page/chrome_client.h" |
| #include "third_party/blink/renderer/core/page/page.h" |
| #include "third_party/blink/renderer/core/svg/svg_svg_element.h" |
| #include "third_party/blink/renderer/platform/bindings/exception_messages.h" |
| #include "third_party/blink/renderer/platform/bindings/microtask.h" |
| #include "third_party/blink/renderer/platform/heap/heap.h" |
| #include "third_party/blink/renderer/platform/instrumentation/use_counter.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| void FullscreenElementChanged(Document& document, |
| Element* old_element, |
| Element* new_element, |
| FullscreenRequestType new_request_type, |
| const FullscreenOptions* new_options) { |
| DCHECK_NE(old_element, new_element); |
| |
| document.GetStyleEngine().EnsureUAStyleForFullscreen(); |
| |
| if (old_element) { |
| DCHECK_NE(old_element, Fullscreen::FullscreenElementFrom(document)); |
| |
| old_element->PseudoStateChanged(CSSSelector::kPseudoFullScreen); |
| old_element->PseudoStateChanged(CSSSelector::kPseudoFullscreen); |
| |
| old_element->SetContainsFullScreenElement(false); |
| old_element->SetContainsFullScreenElementOnAncestorsCrossingFrameBoundaries( |
| false); |
| } |
| |
| if (new_element) { |
| DCHECK_EQ(new_element, Fullscreen::FullscreenElementFrom(document)); |
| // FullscreenOptions should be provided for incoming fullscreen element. |
| CHECK(new_options); |
| |
| new_element->PseudoStateChanged(CSSSelector::kPseudoFullScreen); |
| new_element->PseudoStateChanged(CSSSelector::kPseudoFullscreen); |
| |
| // OOPIF: For RequestType::kForCrossProcessDescendant, |new_element| |
| // is the iframe element for the out-of-process frame that contains the |
| // fullscreen element. Hence, it must match :-webkit-full-screen-ancestor. |
| if (new_request_type & FullscreenRequestType::kForCrossProcessDescendant) { |
| DCHECK(IsA<HTMLIFrameElement>(new_element)); |
| new_element->SetContainsFullScreenElement(true); |
| } |
| new_element->SetContainsFullScreenElementOnAncestorsCrossingFrameBoundaries( |
| true); |
| } |
| |
| if (document.GetFrame()) { |
| // SetIsInert recurses through subframes to propagate the inert bit as |
| // needed. |
| document.GetFrame()->SetIsInert(document.LocalOwner() && |
| document.LocalOwner()->IsInert()); |
| } |
| |
| // Any element not contained by the fullscreen element is inert (see |
| // |Node::IsInert()|), so changing the fullscreen element will typically |
| // change the inertness of most elements. Clear the entire cache. |
| document.ClearAXObjectCache(); |
| |
| if (LocalFrame* frame = document.GetFrame()) { |
| // TODO(foolip): Synchronize hover state changes with animation frames. |
| // https://crbug.com/668758 |
| frame->GetEventHandler().ScheduleHoverStateUpdate(); |
| frame->GetChromeClient().FullscreenElementChanged( |
| old_element, new_element, new_options, new_request_type); |
| |
| // Update paint properties on the visual viewport since |
| // user-input-scrollable bits will change based on fullscreen state. |
| if (Page* page = frame->GetPage()) |
| page->GetVisualViewport().SetNeedsPaintPropertyUpdate(); |
| } |
| } |
| |
| class MetaParams : public GarbageCollected<MetaParams> { |
| public: |
| MetaParams() = default; |
| MetaParams(FullscreenRequestType request_type, |
| const FullscreenOptions* options) |
| : request_type_(request_type), options_(options) {} |
| virtual ~MetaParams() = default; |
| |
| MetaParams(const MetaParams&) = delete; |
| MetaParams& operator=(const MetaParams&) = delete; |
| |
| virtual void Trace(Visitor* visitor) const { visitor->Trace(options_); } |
| |
| FullscreenRequestType request_type() const { return request_type_; } |
| const FullscreenOptions* options() const { return options_; } |
| |
| private: |
| FullscreenRequestType request_type_; |
| Member<const FullscreenOptions> options_; |
| }; |
| |
| using ElementMetaParamsMap = |
| HeapHashMap<WeakMember<Element>, Member<const MetaParams>>; |
| |
| ElementMetaParamsMap& FullscreenParamsMap() { |
| DEFINE_STATIC_LOCAL(Persistent<ElementMetaParamsMap>, map, |
| (MakeGarbageCollected<ElementMetaParamsMap>())); |
| return *map; |
| } |
| |
| bool HasFullscreenFlag(Element& element) { |
| return FullscreenParamsMap().Contains(&element); |
| } |
| |
| void SetFullscreenFlag(Element& element, |
| FullscreenRequestType request_type, |
| const FullscreenOptions* options) { |
| FullscreenParamsMap().insert( |
| &element, MakeGarbageCollected<MetaParams>(request_type, options)); |
| } |
| |
| void UnsetFullscreenFlag(Element& element) { |
| FullscreenParamsMap().erase(&element); |
| } |
| |
| FullscreenRequestType GetRequestType(Element& element) { |
| return FullscreenParamsMap().find(&element)->value->request_type(); |
| } |
| |
| const MetaParams* GetParams(Element& element) { |
| return FullscreenParamsMap().find(&element)->value; |
| } |
| |
| // https://fullscreen.spec.whatwg.org/#fullscreen-an-element |
| void GoFullscreen(Element& element, |
| FullscreenRequestType request_type, |
| const FullscreenOptions* options) { |
| Document& document = element.GetDocument(); |
| Element* old_element = Fullscreen::FullscreenElementFrom(document); |
| |
| // If |element| is already in top layer remove it so it will |
| // be appended to the end. |
| if (element.IsInTopLayer()) |
| document.RemoveFromTopLayer(&element); |
| else |
| DCHECK(!HasFullscreenFlag(element)); |
| |
| // To fullscreen an |element| within a |document|, set the |element|'s |
| // fullscreen flag and add it to |document|'s top layer. |
| SetFullscreenFlag(element, request_type, options); |
| document.AddToTopLayer(&element); |
| |
| DCHECK_EQ(&element, Fullscreen::FullscreenElementFrom(document)); |
| FullscreenElementChanged(document, old_element, &element, request_type, |
| options); |
| } |
| |
| // https://fullscreen.spec.whatwg.org/#unfullscreen-an-element |
| void Unfullscreen(Element& element) { |
| Document& document = element.GetDocument(); |
| Element* old_element = Fullscreen::FullscreenElementFrom(document); |
| |
| // To unfullscreen an |element| within a |document|, unset the element's |
| // fullscreen flag and iframe fullscreen flag (if any), and remove it from |
| // |document|'s top layer. |
| DCHECK(element.IsInTopLayer()); |
| DCHECK(HasFullscreenFlag(element)); |
| UnsetFullscreenFlag(element); |
| document.RemoveFromTopLayer(&element); |
| |
| // WebXR DOM Overlay mode doesn't allow changing the fullscreen element, this |
| // is enforced in AllowedToRequestFullscreen. In this mode, unfullscreening |
| // should only be happening via ExitFullscreen. This may involve previous |
| // nested fullscreen elements being unfullscreened first, ignore those. This |
| // matches kPseudoXrOverlay rules in SelectorChecker::CheckPseudoClass(). |
| if (document.IsXrOverlay() && element == old_element) { |
| // If this was the active fullscreen element, we're exiting fullscreen mode, |
| // and this also ends WebXR DOM Overlay mode. |
| document.SetIsXrOverlay(false, &element); |
| } |
| |
| Element* new_element = Fullscreen::FullscreenElementFrom(document); |
| if (old_element != new_element) { |
| FullscreenRequestType new_request_type; |
| const FullscreenOptions* new_options; |
| if (new_element) { |
| const MetaParams* params = GetParams(*new_element); |
| new_request_type = params->request_type(); |
| new_options = params->options(); |
| } else { |
| new_request_type = FullscreenRequestType::kUnprefixed; |
| new_options = FullscreenOptions::Create(); |
| } |
| FullscreenElementChanged(document, old_element, new_element, |
| new_request_type, new_options); |
| } |
| } |
| |
| // https://fullscreen.spec.whatwg.org/#unfullscreen-a-document |
| void Unfullscreen(Document& document) { |
| // To unfullscreen a |document|, unfullscreen all elements, within |
| // |document|'s top layer, whose fullscreen flag is set. |
| |
| HeapVector<Member<Element>> fullscreen_elements; |
| for (Element* element : document.TopLayerElements()) { |
| if (HasFullscreenFlag(*element)) |
| fullscreen_elements.push_back(element); |
| } |
| |
| for (Element* element : fullscreen_elements) |
| Unfullscreen(*element); |
| } |
| |
| // https://html.spec.whatwg.org/C/#allowed-to-use |
| bool AllowedToUseFullscreen(const Document& document, |
| ReportOptions report_on_failure) { |
| // To determine whether a Document object |document| is allowed to use the |
| // feature indicated by attribute name |allowattribute|, run these steps: |
| |
| // 1. If |document| has no browsing context, then return false. |
| if (!document.GetFrame()) |
| return false; |
| |
| // 2. If Feature Policy is enabled, return the policy for "fullscreen" |
| // feature. |
| return document.GetExecutionContext()->IsFeatureEnabled( |
| mojom::blink::FeaturePolicyFeature::kFullscreen, report_on_failure); |
| } |
| |
| bool AllowedToRequestFullscreen(Document& document) { |
| // WebXR DOM Overlay integration, cf. |
| // https://immersive-web.github.io/dom-overlays/ |
| // |
| // The current implementation of WebXR's "dom-overlay" mode internally uses |
| // the Fullscreen API to show a single DOM element based on configuration at |
| // XR session start. The WebXR API doesn't support changing elements during |
| // the session, so to avoid inconsistencies between implementations we need |
| // to block changes via Fullscreen API while the XR session is active, while |
| // still allowing the XR code to set up fullscreen mode on session start. |
| if (ScopedAllowFullscreen::FullscreenAllowedReason() == |
| ScopedAllowFullscreen::kXrOverlay) { |
| DVLOG(1) << __func__ |
| << ": allowing fullscreen element setup for XR DOM overlay"; |
| return true; |
| } |
| if (document.IsXrOverlay()) { |
| DVLOG(1) << __func__ |
| << ": rejecting change of fullscreen element for XR DOM overlay"; |
| return false; |
| } |
| |
| // An algorithm is allowed to request fullscreen if one of the following is |
| // true: |
| |
| // The algorithm is triggered by a user activation. |
| if (LocalFrame::HasTransientUserActivation(document.GetFrame())) |
| return true; |
| |
| // The algorithm is triggered by a user-generated orientation change. |
| if (ScopedAllowFullscreen::FullscreenAllowedReason() == |
| ScopedAllowFullscreen::kOrientationChange) { |
| UseCounter::Count(document, |
| WebFeature::kFullscreenAllowedByOrientationChange); |
| return true; |
| } |
| |
| // The algorithm is triggered by another event with transient affordances, |
| // e.g. permission-gated events for user-generated screens changes. |
| if (document.GetFrame()->IsTransientAllowFullscreenActive()) { |
| UseCounter::Count(document, WebFeature::kFullscreenAllowedByScreensChange); |
| return true; |
| } |
| |
| String message = ExceptionMessages::FailedToExecute( |
| "requestFullscreen", "Element", |
| "API can only be initiated by a user gesture."); |
| document.AddConsoleMessage(MakeGarbageCollected<ConsoleMessage>( |
| mojom::ConsoleMessageSource::kJavaScript, |
| mojom::ConsoleMessageLevel::kWarning, message)); |
| |
| return false; |
| } |
| |
| // https://fullscreen.spec.whatwg.org/#fullscreen-is-supported |
| bool FullscreenIsSupported(const Document& document) { |
| LocalFrame* frame = document.GetFrame(); |
| if (!frame) |
| return false; |
| |
| // Fullscreen is supported if there is no previously-established user |
| // preference, security risk, or platform limitation. |
| return !document.GetSettings() || |
| document.GetSettings()->GetFullscreenSupported(); |
| } |
| |
| // https://fullscreen.spec.whatwg.org/#fullscreen-element-ready-check |
| bool FullscreenElementReady(const Element& element, |
| ReportOptions report_on_failure) { |
| // A fullscreen element ready check for an element |element| returns true if |
| // all of the following are true, and false otherwise: |
| |
| // |element| is in a document. |
| if (!element.isConnected()) |
| return false; |
| |
| // |element|'s node document is allowed to use the feature indicated by |
| // attribute name allowfullscreen. |
| if (!AllowedToUseFullscreen(element.GetDocument(), report_on_failure)) |
| return false; |
| |
| return true; |
| } |
| |
| // https://fullscreen.spec.whatwg.org/#dom-element-requestfullscreen step 4: |
| bool RequestFullscreenConditionsMet(Element& pending, Document& document) { |
| // |pending|'s namespace is the HTML namespace or |pending| is an SVG svg or |
| // MathML math element. Note: MathML is not supported. |
| if (!pending.IsHTMLElement() && !IsA<SVGSVGElement>(pending)) |
| return false; |
| |
| // |pending| is not a dialog or popup element. |
| if (IsA<HTMLDialogElement>(pending) || IsA<HTMLPopupElement>(pending)) |
| return false; |
| |
| // The fullscreen element ready check for |pending| returns false. |
| if (!FullscreenElementReady(pending, ReportOptions::kReportOnFailure)) |
| return false; |
| |
| // Fullscreen is supported. |
| if (!FullscreenIsSupported(document)) |
| return false; |
| |
| // This algorithm is allowed to request fullscreen. |
| if (!AllowedToRequestFullscreen(document)) |
| return false; |
| |
| return true; |
| } |
| |
| // RequestFullscreenScope is allocated at the top of |RequestFullscreen()| and |
| // used to avoid synchronously changing any state within that method, by |
| // deferring changes in |DidEnterFullscreen()|. |
| class RequestFullscreenScope { |
| STACK_ALLOCATED(); |
| |
| public: |
| RequestFullscreenScope() { |
| DCHECK(!running_request_fullscreen_); |
| running_request_fullscreen_ = true; |
| } |
| |
| ~RequestFullscreenScope() { |
| DCHECK(running_request_fullscreen_); |
| running_request_fullscreen_ = false; |
| } |
| |
| static bool RunningRequestFullscreen() { return running_request_fullscreen_; } |
| |
| private: |
| static bool running_request_fullscreen_; |
| DISALLOW_COPY_AND_ASSIGN(RequestFullscreenScope); |
| }; |
| |
| bool RequestFullscreenScope::running_request_fullscreen_ = false; |
| |
| // Walks the frame tree and returns the first local ancestor frame, if any. |
| LocalFrame* NextLocalAncestor(Frame& frame) { |
| Frame* parent = frame.Tree().Parent(); |
| if (!parent) |
| return nullptr; |
| if (auto* parent_local_frame = DynamicTo<LocalFrame>(parent)) |
| return parent_local_frame; |
| return NextLocalAncestor(*parent); |
| } |
| |
| // Walks the document's frame tree and returns the document of the first local |
| // ancestor frame, if any. |
| Document* NextLocalAncestor(Document& document) { |
| LocalFrame* frame = document.GetFrame(); |
| if (!frame) |
| return nullptr; |
| LocalFrame* next = NextLocalAncestor(*frame); |
| if (!next) |
| return nullptr; |
| DCHECK(next->GetDocument()); |
| return next->GetDocument(); |
| } |
| |
| // Helper to walk the ancestor chain and return the Document of the topmost |
| // local ancestor frame. Note that this is not the same as the topmost frame's |
| // Document, which might be unavailable in OOPIF scenarios. For example, with |
| // OOPIFs, when called on the bottom frame's Document in a A-B-C-B hierarchy in |
| // process B, this will skip remote frame C and return this frame: A-[B]-C-B. |
| Document& TopmostLocalAncestor(Document& document) { |
| if (Document* next = NextLocalAncestor(document)) |
| return TopmostLocalAncestor(*next); |
| return document; |
| } |
| |
| size_t CountFullscreenInTopLayer(const Document& document) { |
| size_t count = 0; |
| for (Element* element : document.TopLayerElements()) { |
| if (HasFullscreenFlag(*element)) |
| ++count; |
| } |
| return count; |
| } |
| |
| // https://fullscreen.spec.whatwg.org/#simple-fullscreen-document |
| bool IsSimpleFullscreenDocument(const Document& document) { |
| return CountFullscreenInTopLayer(document) == 1; |
| } |
| |
| // https://fullscreen.spec.whatwg.org/#collect-documents-to-unfullscreen |
| HeapVector<Member<Document>> CollectDocumentsToUnfullscreen(Document& doc) { |
| // 1. Let |docs| be an ordered set consisting of |doc|. |
| HeapVector<Member<Document>> docs; |
| docs.push_back(&doc); |
| |
| // 2. While true: |
| for (Document* lastDoc = &doc;;) { |
| // 2.1. Let |lastDoc| be |docs|'s last document. |
| |
| // 2.2. Assert: |lastDoc|'s fullscreen element is not null. |
| DCHECK(Fullscreen::FullscreenElementFrom(*lastDoc)); |
| |
| // 2.3. If |lastDoc| is not a simple fullscreen document, break. |
| if (!IsSimpleFullscreenDocument(*lastDoc)) |
| break; |
| |
| // 2.4. Let |container| be |lastDoc|'s browsing context container, if any, |
| // and otherwise break. |
| // |
| // OOPIF: Skip over remote frames, assuming that they have exactly one |
| // element in their fullscreen element stacks, thereby erring on the side of |
| // exiting fullscreen. TODO(alexmos): Deal with nested fullscreen cases, see |
| // https://crbug.com/617369. |
| lastDoc = NextLocalAncestor(*lastDoc); |
| if (!lastDoc) |
| break; |
| |
| // 2.5. If |container|'s iframe fullscreen flag is set, break. |
| // TODO(foolip): Support the iframe fullscreen flag. |
| // https://crbug.com/644695 |
| |
| // 2.6. Append |container|'s node document to |docs|. |
| docs.push_back(lastDoc); |
| } |
| |
| // 3. Return |docs|. |
| return docs; |
| } |
| |
| // https://fullscreen.spec.whatwg.org/#run-the-fullscreen-rendering-steps |
| void FireEvent(const AtomicString& type, Element* element, Document* document) { |
| if (!document || !element) |
| return; |
| |
| // |Document::EnqueueAnimationFrameTask()| is used instead of a "list of |
| // pending fullscreen events", so only the body of the "run the fullscreen |
| // rendering steps" loop appears here: |
| |
| // 3.1. Let |target| be |element| if |element| is connected and its node |
| // document is |document|, and otherwise let |target| be |document|. |
| EventTarget* target = |
| element->isConnected() && &element->GetDocument() == document |
| ? static_cast<EventTarget*>(element) |
| : static_cast<EventTarget*>(document); |
| |
| // 3.2. Fire an event named |type|, with its bubbles and composed attributes |
| // set to true, at |target|. |
| Event* event = Event::CreateBubble(type); |
| event->SetComposed(true); |
| target->DispatchEvent(*event); |
| } |
| |
| const AtomicString& AdjustEventType(const AtomicString& type, |
| FullscreenRequestType request_type) { |
| DCHECK(type == event_type_names::kFullscreenchange || |
| type == event_type_names::kFullscreenerror); |
| |
| if (!(request_type & FullscreenRequestType::kPrefixed)) |
| return type; |
| return type == event_type_names::kFullscreenchange |
| ? event_type_names::kWebkitfullscreenchange |
| : event_type_names::kWebkitfullscreenerror; |
| } |
| |
| void EnqueueEvent(const AtomicString& type, |
| Element& element, |
| Document& document, |
| FullscreenRequestType request_type) { |
| const AtomicString& adjusted_type = AdjustEventType(type, request_type); |
| document.EnqueueAnimationFrameTask(WTF::Bind(FireEvent, adjusted_type, |
| WrapWeakPersistent(&element), |
| WrapWeakPersistent(&document))); |
| } |
| |
| } // anonymous namespace |
| |
| const char Fullscreen::kSupplementName[] = "Fullscreen"; |
| |
| Fullscreen& Fullscreen::From(LocalDOMWindow& window) { |
| Fullscreen* fullscreen = Supplement<LocalDOMWindow>::From<Fullscreen>(window); |
| if (!fullscreen) { |
| fullscreen = MakeGarbageCollected<Fullscreen>(window); |
| ProvideTo(window, fullscreen); |
| } |
| return *fullscreen; |
| } |
| |
| Element* Fullscreen::FullscreenElementFrom(Document& document) { |
| // The fullscreen element is the topmost element in the document's top layer |
| // whose fullscreen flag is set, if any, and null otherwise. |
| |
| const auto& elements = document.TopLayerElements(); |
| for (auto it = elements.rbegin(); it != elements.rend(); ++it) { |
| Element* element = (*it).Get(); |
| if (HasFullscreenFlag(*element)) |
| return element; |
| } |
| |
| return nullptr; |
| } |
| |
| // https://fullscreen.spec.whatwg.org/#fullscreen-element |
| Element* Fullscreen::FullscreenElementForBindingFrom(TreeScope& scope) { |
| Element* element = FullscreenElementFrom(scope.GetDocument()); |
| if (!element) |
| return nullptr; |
| return scope.AdjustedElement(*element); |
| } |
| |
| bool Fullscreen::IsInFullscreenElementStack(const Element& element) { |
| return HasFullscreenFlag(const_cast<Element&>(element)); |
| } |
| |
| Fullscreen::Fullscreen(LocalDOMWindow& window) |
| : Supplement<LocalDOMWindow>(window), |
| ExecutionContextLifecycleObserver(&window) {} |
| |
| Fullscreen::~Fullscreen() = default; |
| |
| void Fullscreen::ContextDestroyed() { |
| pending_requests_.clear(); |
| pending_exits_.clear(); |
| } |
| |
| // https://fullscreen.spec.whatwg.org/#dom-element-requestfullscreen |
| void Fullscreen::RequestFullscreen(Element& pending) { |
| // TODO(foolip): Make RequestType::Unprefixed the default when the unprefixed |
| // API is enabled. https://crbug.com/383813 |
| FullscreenOptions* options = FullscreenOptions::Create(); |
| options->setNavigationUI("hide"); |
| RequestFullscreen(pending, options, FullscreenRequestType::kPrefixed); |
| } |
| |
| ScriptPromise Fullscreen::RequestFullscreen(Element& pending, |
| const FullscreenOptions* options, |
| FullscreenRequestType request_type, |
| ScriptState* script_state, |
| ExceptionState* exception_state) { |
| RequestFullscreenScope scope; |
| |
| // 1. Let |pending| be the context object. |
| |
| // 2. Let |pendingDoc| be |pending|'s node document. |
| Document& document = pending.GetDocument(); |
| |
| // 3. Let |promise| be a new promise. |
| // For optimization allocate the ScriptPromiseResolver just after step 4. |
| ScriptPromiseResolver* resolver = nullptr; |
| |
| // 4. If |pendingDoc| is not fully active, then reject |promise| with a |
| // TypeError exception and return |promise|. |
| if (!document.IsActive() || !document.GetFrame()) { |
| if (!exception_state) |
| return ScriptPromise(); |
| exception_state->ThrowTypeError("Document not active"); |
| return ScriptPromise(); |
| } |
| |
| if (script_state) { |
| // We should only be creating promises for unprefixed variants. |
| DCHECK(!(request_type & FullscreenRequestType::kPrefixed)); |
| resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| } |
| |
| bool for_cross_process_descendant = |
| request_type & FullscreenRequestType::kForCrossProcessDescendant; |
| |
| // Use counters only need to be incremented in the process of the actual |
| // fullscreen element. |
| LocalDOMWindow& window = *document.domWindow(); |
| if (!for_cross_process_descendant) { |
| if (window.IsSecureContext()) |
| UseCounter::Count(window, WebFeature::kFullscreenSecureOrigin); |
| else |
| UseCounter::Count(window, WebFeature::kFullscreenInsecureOrigin); |
| // Coarsely measure whether this request may be specifying another screen. |
| if (options->hasScreen()) |
| UseCounter::Count(window, WebFeature::kFullscreenCrossScreen); |
| } |
| |
| // 5. Let |error| be false. |
| bool error = false; |
| |
| // 6. If any of the following conditions are false, then set |error| to true: |
| // |
| // OOPIF: If |RequestFullscreen()| was already called in a descendant frame |
| // and passed the checks, do not check again here. |
| if (!for_cross_process_descendant && |
| !RequestFullscreenConditionsMet(pending, document)) |
| error = true; |
| |
| // 7. Return |promise|, and run the remaining steps in parallel. |
| ScriptPromise promise = resolver ? resolver->Promise() : ScriptPromise(); |
| |
| // 8. If |error| is false: Resize |pendingDoc|'s top-level browsing context's |
| // document's viewport's dimensions to match the dimensions of the screen of |
| // the output device. Optionally display a message how the end user can |
| // revert this. |
| if (!error) { |
| if (From(window).pending_requests_.size()) { |
| UseCounter::Count(window, |
| WebFeature::kFullscreenRequestWithPendingElement); |
| } |
| |
| From(window).pending_requests_.push_back( |
| MakeGarbageCollected<PendingRequest>(&pending, request_type, options, |
| resolver)); |
| LocalFrame& frame = *window.GetFrame(); |
| frame.GetChromeClient().EnterFullscreen(frame, options, request_type); |
| |
| // After the first fullscreen request, the user activation should be |
| // consumed, and the following fullscreen requests should receive an error. |
| if (!for_cross_process_descendant) |
| LocalFrame::ConsumeTransientUserActivation(&frame); |
| } else { |
| // Note: Although we are past the "in parallel" point, it's OK to continue |
| // synchronously because when |error| is true, |ContinueRequestFullscreen()| |
| // will only queue a task and return. This is indistinguishable from, e.g., |
| // enqueueing a microtask to continue at step 9. |
| ContinueRequestFullscreen(document, pending, request_type, options, |
| resolver, true /* error */); |
| } |
| |
| return promise; |
| } |
| |
| void Fullscreen::DidResolveEnterFullscreenRequest(Document& document, |
| bool granted) { |
| if (!document.domWindow()) |
| return; |
| |
| // We may be called synchronously from within |
| // |FullscreenController::EnterFullscreen()| if we were already fullscreen, |
| // but must still not synchronously change the fullscreen element. Instead |
| // enqueue a microtask to continue. |
| if (RequestFullscreenScope::RunningRequestFullscreen()) { |
| Microtask::EnqueueMicrotask(WTF::Bind( |
| [](Document* document, bool granted) { |
| DCHECK(document); |
| DidResolveEnterFullscreenRequest(*document, granted); |
| }, |
| WrapPersistent(&document), granted)); |
| return; |
| } |
| |
| PendingRequests requests; |
| requests.swap(From(*document.domWindow()).pending_requests_); |
| for (const Member<PendingRequest>& request : requests) { |
| ContinueRequestFullscreen(document, *request->element(), request->type(), |
| request->options(), request->resolver(), |
| !granted); |
| } |
| } |
| |
| void Fullscreen::ContinueRequestFullscreen(Document& document, |
| Element& pending, |
| FullscreenRequestType request_type, |
| const FullscreenOptions* options, |
| ScriptPromiseResolver* resolver, |
| bool error) { |
| DCHECK(document.IsActive()); |
| DCHECK(document.GetFrame()); |
| |
| // 9. If any of the following conditions are false, then set |error| to true: |
| // * |pending|'s node document is |pendingDoc|. |
| // * The fullscreen element ready check for |pending| returns true. |
| if (pending.GetDocument() != document || |
| !FullscreenElementReady(pending, ReportOptions::kDoNotReport)) |
| error = true; |
| |
| // 10. If |error| is true: |
| if (error) { |
| // 10.1. Append (fullscreenerror, |pending|) to |pendingDoc|'s list of |
| // pending fullscreen events. |
| EnqueueEvent(event_type_names::kFullscreenerror, pending, document, |
| request_type); |
| |
| // 10.2. Reject |promise| with a TypeError exception and terminate these |
| // steps. |
| if (resolver && resolver->GetScriptState()->ContextIsValid()) { |
| ScriptState::Scope scope(resolver->GetScriptState()); |
| // TODO(dtapuska): Change error to be something useful instead of just a |
| // boolean and return this to the user. |
| resolver->Reject(V8ThrowException::CreateTypeError( |
| resolver->GetScriptState()->GetIsolate(), "fullscreen error")); |
| } |
| return; |
| } |
| |
| // 11. Let |fullscreenElements| be an ordered set initially consisting of |
| // |pending|. |
| HeapVector<Member<Element>> fullscreen_elements; |
| fullscreen_elements.push_back(pending); |
| |
| // 12. While the first element in |fullscreenElements| is in a nested browsing |
| // context: append its browsing context container to |fullscreenElements|. |
| // |
| // OOPIF: |fullscreenElements| will only contain elements for local ancestors, |
| // and remote ancestors will be processed in their respective processes. This |
| // preserves the spec's event firing order for local ancestors, but not for |
| // remote ancestors. However, that difference shouldn't be observable in |
| // practice: a fullscreenchange event handler would need to postMessage a |
| // frame in another renderer process, where the message should be queued up |
| // and processed after the IPC that dispatches fullscreenchange. |
| for (Frame* frame = pending.GetDocument().GetFrame(); frame; |
| frame = frame->Tree().Parent()) { |
| Element* element = DynamicTo<HTMLFrameOwnerElement>(frame->Owner()); |
| if (!element) |
| continue; |
| fullscreen_elements.push_back(element); |
| } |
| |
| // 13. For each |element| in |fullscreenElements|: |
| for (Element* element : fullscreen_elements) { |
| // 13.1. Let |doc| be |element|'s node document. |
| Document& doc = element->GetDocument(); |
| |
| // If this fullscreen request is for WebXR DOM Overlay mode, apply that |
| // property to the document. This updates styling (setting the background |
| // transparent) and adds the :xr-overlay pseudoclass. |
| if (request_type & FullscreenRequestType::kForXrOverlay) { |
| // There's never more than one overlay element per document. (It's either |
| // the actual overlay element, or a containing iframe element if the |
| // actual element is in a different document.) It can't be changed during |
| // the session, that's enforced by AllowedToRequestFullscreen(). |
| DCHECK(!doc.IsXrOverlay()); |
| doc.SetIsXrOverlay(true, element); |
| } |
| |
| // 13.2. If |element| is |doc|'s fullscreen element, continue. |
| if (element == FullscreenElementFrom(doc)) |
| continue; |
| |
| // 13.3. If |element| is |pending| and |pending| is an iframe element, set |
| // |element|'s iframe fullscreen flag. |
| // TODO(foolip): Support the iframe fullscreen flag. |
| // https://crbug.com/644695 |
| |
| // 13.4. Fullscreen |element| within |doc|. |
| GoFullscreen(*element, request_type, options); |
| |
| // 13.5. Append (fullscreenchange, |element|) to |doc|'s list of pending |
| // fullscreen events. |
| EnqueueEvent(event_type_names::kFullscreenchange, *element, doc, |
| request_type); |
| } |
| |
| // 14. Resolve |promise| with undefined. |
| if (resolver) { |
| ScriptState::Scope scope(resolver->GetScriptState()); |
| resolver->Resolve(); |
| } |
| } |
| |
| // https://fullscreen.spec.whatwg.org/#fully-exit-fullscreen |
| void Fullscreen::FullyExitFullscreen(Document& document, bool ua_originated) { |
| // TODO(foolip): The spec used to have a first step saying "Let |doc| be the |
| // top-level browsing context's document" which was removed in |
| // https://github.com/whatwg/fullscreen/commit/3243119d027a8ff5b80998eb1f17f8eba148a346. |
| // Remove it here as well. |
| Document& doc = TopmostLocalAncestor(document); |
| |
| // 1. If |document|'s fullscreen element is null, terminate these steps. |
| Element* fullscreen_element = FullscreenElementFrom(doc); |
| if (!fullscreen_element) |
| return; |
| |
| // 2. Unfullscreen elements whose fullscreen flag is set, within |
| // |document|'s top layer, except for |document|'s fullscreen element. |
| HeapVector<Member<Element>> unfullscreen_elements; |
| for (Element* element : doc.TopLayerElements()) { |
| if (HasFullscreenFlag(*element) && element != fullscreen_element) |
| unfullscreen_elements.push_back(element); |
| } |
| for (Element* element : unfullscreen_elements) |
| Unfullscreen(*element); |
| DCHECK(IsSimpleFullscreenDocument(doc)); |
| |
| // 3. Exit fullscreen |document|. |
| ExitFullscreen(doc, nullptr, nullptr, ua_originated); |
| } |
| |
| // https://fullscreen.spec.whatwg.org/#exit-fullscreen |
| ScriptPromise Fullscreen::ExitFullscreen(Document& doc, |
| ScriptState* script_state, |
| ExceptionState* exception_state, |
| bool ua_originated) { |
| // 1. Let |promise| be a new promise. |
| // ScriptPromiseResolver is allocated after step 2. |
| ScriptPromiseResolver* resolver = nullptr; |
| |
| // 2. If |doc| is not fully active or |doc|'s fullscreen element is null, then |
| // reject |promise| with a TypeError exception and return |promise|. |
| if (!doc.IsActive() || !doc.GetFrame() || !FullscreenElementFrom(doc)) { |
| if (!exception_state) |
| return ScriptPromise(); |
| exception_state->ThrowTypeError("Document not active"); |
| return ScriptPromise(); |
| } |
| |
| if (script_state) |
| resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| |
| // 3. Let |resize| be false. |
| bool resize = false; |
| |
| // 4. Let |docs| be the result of collecting documents to unfullscreen given |
| // |doc|. |
| HeapVector<Member<Document>> docs = CollectDocumentsToUnfullscreen(doc); |
| |
| // 5. Let |topLevelDoc| be |doc|'s top-level browsing context's active |
| // document. |
| // |
| // OOPIF: Let |topLevelDoc| be the topmost local ancestor instead. If the main |
| // frame is in another process, we will still fully exit fullscreen even |
| // though that's wrong if the main frame was in nested fullscreen. |
| // TODO(alexmos): Deal with nested fullscreen cases, see |
| // https://crbug.com/617369. |
| Document& top_level_doc = TopmostLocalAncestor(doc); |
| |
| // 6. If |topLevelDoc| is in |docs|, and it is a simple fullscreen document, |
| // then set |doc| to |topLevelDoc| and |resize| to true. |
| // |
| // Note: |doc| is not set here, but |doc| will be the topmost local ancestor |
| // in |Fullscreen::ContinueExitFullscreen| if |resize| is true. |
| if (!docs.IsEmpty() && docs.back() == &top_level_doc && |
| IsSimpleFullscreenDocument(top_level_doc)) { |
| resize = true; |
| } |
| |
| // 7. If |doc|'s fullscreen element is not connected. |
| Element* element = FullscreenElementFrom(doc); |
| if (!element->isConnected()) { |
| FullscreenRequestType request_type = GetRequestType(*element); |
| |
| // 7.1. Append (fullscreenchange, |doc|'s fullscreen element) to |
| // |doc|'s list of pending fullscreen events. |
| EnqueueEvent(event_type_names::kFullscreenchange, *element, doc, |
| request_type); |
| |
| // 7.2. Unfullscreen |element|. |
| Unfullscreen(*element); |
| } |
| |
| // 7. Return |promise|, and run the remaining steps in parallel. |
| ScriptPromise promise = resolver ? resolver->Promise() : ScriptPromise(); |
| |
| // 8. If |resize| is true, resize |doc|'s viewport to its "normal" dimensions. |
| if (resize) { |
| if (ua_originated) { |
| ContinueExitFullscreen(&doc, resolver, true /* resize */); |
| } else { |
| From(*top_level_doc.domWindow()).pending_exits_.push_back(resolver); |
| LocalFrame& frame = *doc.GetFrame(); |
| frame.GetChromeClient().ExitFullscreen(frame); |
| } |
| } else { |
| DCHECK(!ua_originated); |
| // Note: We are past the "in parallel" point, and |ContinueExitFullscreen()| |
| // will change script-observable state (document.fullscreenElement) |
| // synchronously, so we have to continue asynchronously. |
| Microtask::EnqueueMicrotask( |
| WTF::Bind(ContinueExitFullscreen, WrapPersistent(&doc), |
| WrapPersistent(resolver), false /* resize */)); |
| } |
| return promise; |
| } |
| |
| void Fullscreen::DidExitFullscreen(Document& document) { |
| // If this is a response to an ExitFullscreen call then |
| // continue exiting. Otherwise call FullyExitFullscreen. |
| Fullscreen& fullscreen = From(*document.domWindow()); |
| PendingExits exits; |
| exits.swap(fullscreen.pending_exits_); |
| if (exits.IsEmpty()) { |
| FullyExitFullscreen(document, true /* ua_originated */); |
| } else { |
| for (const Member<PendingExit>& exit : exits) |
| ContinueExitFullscreen(&document, exit, true /* resize */); |
| } |
| } |
| |
| void Fullscreen::ContinueExitFullscreen(Document* doc, |
| ScriptPromiseResolver* resolver, |
| bool resize) { |
| if (!doc || !doc->IsActive() || !doc->GetFrame()) { |
| if (resolver) { |
| ScriptState::Scope scope(resolver->GetScriptState()); |
| resolver->Reject(V8ThrowException::CreateTypeError( |
| resolver->GetScriptState()->GetIsolate(), "Document is not active")); |
| } |
| return; |
| } |
| |
| if (resize) { |
| // See comment for step 6. |
| DCHECK_EQ(nullptr, NextLocalAncestor(*doc)); |
| } |
| |
| // 9. If |doc|'s fullscreen element is null, then resolve |promise| with |
| // undefined and terminate these steps. |
| if (!FullscreenElementFrom(*doc)) { |
| if (resolver) { |
| ScriptState::Scope scope(resolver->GetScriptState()); |
| resolver->Resolve(); |
| } |
| return; |
| } |
| |
| // 10. Let |exitDocs| be the result of collecting documents to unfullscreen |
| // given |doc|. |
| HeapVector<Member<Document>> exit_docs = CollectDocumentsToUnfullscreen(*doc); |
| |
| // 11. Let |descendantDocs| be an ordered set consisting of |doc|'s |
| // descendant browsing contexts' documents whose fullscreen element is |
| // non-null, if any, in tree order. |
| HeapVector<Member<Document>> descendant_docs; |
| for (Frame* descendant = doc->GetFrame()->Tree().FirstChild(); descendant; |
| descendant = descendant->Tree().TraverseNext(doc->GetFrame())) { |
| auto* descendant_local_frame = DynamicTo<LocalFrame>(descendant); |
| if (!descendant_local_frame) |
| continue; |
| DCHECK(descendant_local_frame->GetDocument()); |
| if (FullscreenElementFrom(*descendant_local_frame->GetDocument())) |
| descendant_docs.push_back(descendant_local_frame->GetDocument()); |
| } |
| |
| // 12. For each |exitDoc| in |exitDocs|: |
| for (Document* exit_doc : exit_docs) { |
| Element* exit_element = FullscreenElementFrom(*exit_doc); |
| DCHECK(exit_element); |
| FullscreenRequestType request_type = GetRequestType(*exit_element); |
| |
| // 12.1. Append (fullscreenchange, |exitDoc|'s fullscreen element) to |
| // |exitDoc|'s list of pending fullscreen events. |
| EnqueueEvent(event_type_names::kFullscreenchange, *exit_element, *exit_doc, |
| request_type); |
| |
| // 12.2. If |resize| is true, unfullscreen |exitDoc|. |
| // 12.3. Otherwise, unfullscreen |exitDoc|'s fullscreen element. |
| if (resize) |
| Unfullscreen(*exit_doc); |
| else |
| Unfullscreen(*exit_element); |
| } |
| |
| // 13. For each |descendantDoc| in |descendantDocs|: |
| for (Document* descendant_doc : descendant_docs) { |
| Element* descendant_element = FullscreenElementFrom(*descendant_doc); |
| DCHECK(descendant_element); |
| FullscreenRequestType request_type = GetRequestType(*descendant_element); |
| |
| // 13.1. Append (fullscreenchange, |descendantDoc|'s fullscreen element) to |
| // |descendantDoc|'s list of pending fullscreen events. |
| EnqueueEvent(event_type_names::kFullscreenchange, *descendant_element, |
| *descendant_doc, request_type); |
| |
| // 13.2. Unfullscreen |descendantDoc|. |
| Unfullscreen(*descendant_doc); |
| } |
| |
| // 14. Resolve |promise| with undefined. |
| if (resolver) { |
| ScriptState::Scope scope(resolver->GetScriptState()); |
| resolver->Resolve(); |
| } |
| } |
| |
| // https://fullscreen.spec.whatwg.org/#dom-document-fullscreenenabled |
| bool Fullscreen::FullscreenEnabled(Document& document) { |
| // The fullscreenEnabled attribute's getter must return true if the context |
| // object is allowed to use the feature indicated by attribute name |
| // allowfullscreen and fullscreen is supported, and false otherwise. |
| return AllowedToUseFullscreen(document, ReportOptions::kDoNotReport) && |
| FullscreenIsSupported(document); |
| } |
| |
| void Fullscreen::DidUpdateSize(Element& element) { |
| // StyleAdjuster will set the size so we need to do a style recalc. |
| // Normally changing size means layout so just doing a style recalc is a |
| // bit surprising. |
| element.SetNeedsStyleRecalc( |
| kLocalStyleChange, |
| StyleChangeReasonForTracing::Create(style_change_reason::kFullscreen)); |
| } |
| |
| void Fullscreen::ElementRemoved(Element& node) { |
| DCHECK(node.IsInTopLayer()); |
| if (!HasFullscreenFlag(node)) |
| return; |
| |
| // 1. Let |document| be removedNode's node document. |
| Document& document = node.GetDocument(); |
| |
| // |Fullscreen::ElementRemoved()| is called for each removed element, so only |
| // the body of the spec "removing steps" loop appears here: |
| |
| // 3.1. If |node| is its node document's fullscreen element, exit fullscreen |
| // that document. |
| if (IsFullscreenElement(node)) { |
| ExitFullscreen(document); |
| } else { |
| // 3.2. Otherwise, unfullscreen |node| within its node document. |
| Unfullscreen(node); |
| } |
| |
| // 3.3 If document's top layer contains node, remove node from document's top |
| // layer. This is done in Element::RemovedFrom. |
| } |
| |
| void Fullscreen::Trace(Visitor* visitor) const { |
| visitor->Trace(pending_requests_); |
| visitor->Trace(pending_exits_); |
| Supplement<LocalDOMWindow>::Trace(visitor); |
| ExecutionContextLifecycleObserver::Trace(visitor); |
| } |
| |
| Fullscreen::PendingRequest::PendingRequest(Element* element, |
| FullscreenRequestType type, |
| const FullscreenOptions* options, |
| ScriptPromiseResolver* resolver) |
| : element_(element), type_(type), options_(options), resolver_(resolver) {} |
| |
| Fullscreen::PendingRequest::~PendingRequest() = default; |
| |
| void Fullscreen::PendingRequest::Trace(Visitor* visitor) const { |
| visitor->Trace(element_); |
| visitor->Trace(options_); |
| visitor->Trace(resolver_); |
| } |
| |
| } // namespace blink |