blob: 1e42871bc2b23cf98ae0d227cf5a705e7466b87f [file] [log] [blame]
/*
* Copyright (C) 2019 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "FullscreenManager.h"
#if ENABLE(FULLSCREEN_API)
#include "Chrome.h"
#include "ChromeClient.h"
#include "Document.h"
#include "Element.h"
#include "EventNames.h"
#include "Frame.h"
#include "HTMLFrameOwnerElement.h"
#include "HTMLMediaElement.h"
#include "Page.h"
#include "QualifiedName.h"
#include "RenderFullScreen.h"
#include "RenderTreeBuilder.h"
#include "Settings.h"
namespace WebCore {
using namespace HTMLNames;
static bool isAttributeOnAllOwners(const QualifiedName& attribute, const QualifiedName& prefixedAttribute, const HTMLFrameOwnerElement* owner)
{
if (!owner)
return true;
do {
if (!(owner->hasAttribute(attribute) || owner->hasAttribute(prefixedAttribute)))
return false;
} while ((owner = owner->document().ownerElement()));
return true;
}
FullscreenManager::FullscreenManager(Document& document)
: m_document { document }
{
}
FullscreenManager::~FullscreenManager() = default;
bool FullscreenManager::fullscreenIsAllowedForElement(Element& element) const
{
return isAttributeOnAllOwners(allowfullscreenAttr, webkitallowfullscreenAttr, element.document().ownerElement());
}
void FullscreenManager::requestFullscreenForElement(Element* element, FullscreenCheckType checkType)
{
if (!element)
element = documentElement();
auto failedPreflights = [this](auto element) mutable {
m_fullscreenErrorEventTargetQueue.append(WTFMove(element));
m_fullscreenTaskQueue.enqueueTask([this] {
dispatchFullscreenChangeEvents();
});
};
// 1. If any of the following conditions are true, terminate these steps and queue a task to fire
// an event named fullscreenerror with its bubbles attribute set to true on the context object's
// node document:
// This algorithm is not allowed to show a pop-up:
// An algorithm is allowed to show a pop-up if, in the task in which the algorithm is running, either:
// - an activation behavior is currently being processed whose click event was trusted, or
// - the event listener for a trusted click event is being handled.
if (!UserGestureIndicator::processingUserGesture()) {
failedPreflights(WTFMove(element));
return;
}
// We do not allow pressing the Escape key as a user gesture to enter fullscreen since this is the key
// to exit fullscreen.
if (UserGestureIndicator::currentUserGesture()->gestureType() == UserGestureType::EscapeKey) {
document().addConsoleMessage(MessageSource::Security, MessageLevel::Error, "The Escape key may not be used as a user gesture to enter fullscreen"_s);
failedPreflights(WTFMove(element));
return;
}
// There is a previously-established user preference, security risk, or platform limitation.
if (!page() || !page()->settings().fullScreenEnabled()) {
failedPreflights(WTFMove(element));
return;
}
bool hasKeyboardAccess = true;
if (!page()->chrome().client().supportsFullScreenForElement(*element, hasKeyboardAccess)) {
// The new full screen API does not accept a "flags" parameter, so fall back to disallowing
// keyboard input if the chrome client refuses to allow keyboard input.
hasKeyboardAccess = false;
if (!page()->chrome().client().supportsFullScreenForElement(*element, hasKeyboardAccess)) {
failedPreflights(WTFMove(element));
return;
}
}
m_pendingFullscreenElement = element;
m_fullscreenTaskQueue.enqueueTask([this, element = makeRefPtr(element), checkType, hasKeyboardAccess, failedPreflights] () mutable {
// Don't allow fullscreen if it has been cancelled or a different fullscreen element
// has requested fullscreen.
if (m_pendingFullscreenElement != element) {
failedPreflights(WTFMove(element));
return;
}
// Don't allow fullscreen if document is hidden.
if (document().hidden()) {
failedPreflights(WTFMove(element));
return;
}
// The context object is not in a document.
if (!element->isConnected()) {
failedPreflights(WTFMove(element));
return;
}
// The context object's node document, or an ancestor browsing context's document does not have
// the fullscreen enabled flag set.
if (checkType == EnforceIFrameAllowFullscreenRequirement && !fullscreenIsAllowedForElement(*element)) {
failedPreflights(WTFMove(element));
return;
}
// The context object's node document fullscreen element stack is not empty and its top element
// is not an ancestor of the context object.
if (!m_fullscreenElementStack.isEmpty() && !m_fullscreenElementStack.last()->contains(element.get())) {
failedPreflights(WTFMove(element));
return;
}
// A descendant browsing context's document has a non-empty fullscreen element stack.
bool descendentHasNonEmptyStack = false;
for (Frame* descendant = frame() ? frame()->tree().traverseNext() : nullptr; descendant; descendant = descendant->tree().traverseNext()) {
if (descendant->document()->fullscreenManager().fullscreenElement()) {
descendentHasNonEmptyStack = true;
break;
}
}
if (descendentHasNonEmptyStack) {
failedPreflights(WTFMove(element));
return;
}
// 2. Let doc be element's node document. (i.e. "this")
Document* currentDoc = &document();
// 3. Let docs be all doc's ancestor browsing context's documents (if any) and doc.
Deque<Document*> docs;
do {
docs.prepend(currentDoc);
currentDoc = currentDoc->ownerElement() ? &currentDoc->ownerElement()->document() : nullptr;
} while (currentDoc);
// 4. For each document in docs, run these substeps:
Deque<Document*>::iterator current = docs.begin(), following = docs.begin();
do {
++following;
// 1. Let following document be the document after document in docs, or null if there is no
// such document.
Document* currentDoc = *current;
Document* followingDoc = following != docs.end() ? *following : nullptr;
// 2. If following document is null, push context object on document's fullscreen element
// stack, and queue a task to fire an event named fullscreenchange with its bubbles attribute
// set to true on the document.
if (!followingDoc) {
currentDoc->fullscreenManager().pushFullscreenElementStack(*element);
addDocumentToFullscreenChangeEventQueue(*currentDoc);
continue;
}
// 3. Otherwise, if document's fullscreen element stack is either empty or its top element
// is not following document's browsing context container,
Element* topElement = currentDoc->fullscreenManager().fullscreenElement();
if (!topElement || topElement != followingDoc->ownerElement()) {
// ...push following document's browsing context container on document's fullscreen element
// stack, and queue a task to fire an event named fullscreenchange with its bubbles attribute
// set to true on document.
currentDoc->fullscreenManager().pushFullscreenElementStack(*followingDoc->ownerElement());
addDocumentToFullscreenChangeEventQueue(*currentDoc);
continue;
}
// 4. Otherwise, do nothing for this document. It stays the same.
} while (++current != docs.end());
// 5. Return, and run the remaining steps asynchronously.
// 6. Optionally, perform some animation.
m_areKeysEnabledInFullscreen = hasKeyboardAccess;
m_fullscreenTaskQueue.enqueueTask([this, element = WTFMove(element), failedPreflights = WTFMove(failedPreflights)] () mutable {
auto page = this->page();
if (!page || document().hidden() || m_pendingFullscreenElement != element || !element->isConnected()) {
failedPreflights(element);
return;
}
page->chrome().client().enterFullScreenForElement(*element.get());
});
// 7. Optionally, display a message indicating how the user can exit displaying the context object fullscreen.
});
}
void FullscreenManager::cancelFullscreen()
{
// The Mozilla "cancelFullscreen()" API behaves like the W3C "fully exit fullscreen" behavior, which
// is defined as:
// "To fully exit fullscreen act as if the exitFullscreen() method was invoked on the top-level browsing
// context's document and subsequently empty that document's fullscreen element stack."
Document& topDocument = document().topDocument();
if (!topDocument.fullscreenManager().fullscreenElement()) {
// If there is a pending fullscreen element but no top document fullscreen element,
// there is a pending task in enterFullscreen(). Cause it to cancel and fire an error
// by clearing the pending fullscreen element.
m_pendingFullscreenElement = nullptr;
return;
}
// To achieve that aim, remove all the elements from the top document's stack except for the first before
// calling webkitExitFullscreen():
Vector<RefPtr<Element>> replacementFullscreenElementStack;
replacementFullscreenElementStack.append(topDocument.fullscreenManager().fullscreenElement());
topDocument.fullscreenManager().m_fullscreenElementStack.swap(replacementFullscreenElementStack);
topDocument.fullscreenManager().exitFullscreen();
}
void FullscreenManager::exitFullscreen()
{
// The exitFullscreen() method must run these steps:
// 1. Let doc be the context object. (i.e. "this")
Document* currentDoc = &document();
// 2. If doc's fullscreen element stack is empty, terminate these steps.
if (m_fullscreenElementStack.isEmpty()) {
// If there is a pending fullscreen element but an empty fullscreen element stack,
// there is a pending task in requestFullscreenForElement(). Cause it to cancel and fire an error
// by clearing the pending fullscreen element.
m_pendingFullscreenElement = nullptr;
return;
}
// 3. Let descendants be all the doc's descendant browsing context's documents with a non-empty fullscreen
// element stack (if any), ordered so that the child of the doc is last and the document furthest
// away from the doc is first.
Deque<RefPtr<Document>> descendants;
for (Frame* descendant = frame() ? frame()->tree().traverseNext() : nullptr; descendant; descendant = descendant->tree().traverseNext()) {
if (descendant->document()->fullscreenManager().fullscreenElement())
descendants.prepend(descendant->document());
}
// 4. For each descendant in descendants, empty descendant's fullscreen element stack, and queue a
// task to fire an event named fullscreenchange with its bubbles attribute set to true on descendant.
for (auto& document : descendants) {
document->fullscreenManager().clearFullscreenElementStack();
addDocumentToFullscreenChangeEventQueue(*document);
}
// 5. While doc is not null, run these substeps:
Element* newTop = nullptr;
while (currentDoc) {
// 1. Pop the top element of doc's fullscreen element stack.
currentDoc->fullscreenManager().popFullscreenElementStack();
// If doc's fullscreen element stack is non-empty and the element now at the top is either
// not in a document or its node document is not doc, repeat this substep.
newTop = currentDoc->fullscreenManager().fullscreenElement();
if (newTop && (!newTop->isConnected() || &newTop->document() != currentDoc))
continue;
// 2. Queue a task to fire an event named fullscreenchange with its bubbles attribute set to true
// on doc.
addDocumentToFullscreenChangeEventQueue(*currentDoc);
// 3. If doc's fullscreen element stack is empty and doc's browsing context has a browsing context
// container, set doc to that browsing context container's node document.
if (!newTop && currentDoc->ownerElement()) {
currentDoc = &currentDoc->ownerElement()->document();
continue;
}
// 4. Otherwise, set doc to null.
currentDoc = nullptr;
}
// 6. Return, and run the remaining steps asynchronously.
// 7. Optionally, perform some animation.
m_fullscreenTaskQueue.enqueueTask([this, newTop = makeRefPtr(newTop), fullscreenElement = m_fullscreenElement] {
auto* page = this->page();
if (!page)
return;
// If there is a pending fullscreen element but no fullscreen element
// there is a pending task in requestFullscreenForElement(). Cause it to cancel and fire an error
// by clearing the pending fullscreen element.
if (!fullscreenElement && m_pendingFullscreenElement) {
m_pendingFullscreenElement = nullptr;
return;
}
// Only exit out of full screen window mode if there are no remaining elements in the
// full screen stack.
if (!newTop) {
page->chrome().client().exitFullScreenForElement(fullscreenElement.get());
return;
}
// Otherwise, notify the chrome of the new full screen element.
page->chrome().client().enterFullScreenForElement(*newTop);
});
}
bool FullscreenManager::isFullscreenEnabled() const
{
// 4. The fullscreenEnabled attribute must return true if the context object and all ancestor
// browsing context's documents have their fullscreen enabled flag set, or false otherwise.
// Top-level browsing contexts are implied to have their allowFullscreen attribute set.
return isAttributeOnAllOwners(allowfullscreenAttr, webkitallowfullscreenAttr, document().ownerElement());
}
static void unwrapFullscreenRenderer(RenderFullScreen* fullscreenRenderer, Element* fullscreenElement)
{
if (!fullscreenRenderer)
return;
bool requiresRenderTreeRebuild;
fullscreenRenderer->unwrapRenderer(requiresRenderTreeRebuild);
if (requiresRenderTreeRebuild && fullscreenElement && fullscreenElement->parentElement())
fullscreenElement->parentElement()->invalidateStyleAndRenderersForSubtree();
}
void FullscreenManager::willEnterFullscreen(Element& element)
{
if (!document().hasLivingRenderTree() || document().backForwardCacheState() != Document::NotInBackForwardCache)
return;
// Protect against being called after the document has been removed from the page.
if (!page())
return;
// If pending fullscreen element is unset or another element's was requested,
// issue a cancel fullscreen request to the client
if (m_pendingFullscreenElement != &element) {
page()->chrome().client().exitFullScreenForElement(&element);
return;
}
ASSERT(page()->settings().fullScreenEnabled());
unwrapFullscreenRenderer(m_fullscreenRenderer.get(), m_fullscreenElement.get());
element.willBecomeFullscreenElement();
ASSERT(&element == m_pendingFullscreenElement);
m_pendingFullscreenElement = nullptr;
m_fullscreenElement = &element;
#if USE(NATIVE_FULLSCREEN_VIDEO)
if (element.isMediaElement())
return;
#endif
// Create a placeholder block for a the full-screen element, to keep the page from reflowing
// when the element is removed from the normal flow. Only do this for a RenderBox, as only
// a box will have a frameRect. The placeholder will be created in setFullscreenRenderer()
// during layout.
auto renderer = m_fullscreenElement->renderer();
bool shouldCreatePlaceholder = is<RenderBox>(renderer);
if (shouldCreatePlaceholder) {
m_savedPlaceholderFrameRect = downcast<RenderBox>(*renderer).frameRect();
m_savedPlaceholderRenderStyle = RenderStyle::clonePtr(renderer->style());
}
if (m_fullscreenElement != documentElement() && renderer)
RenderFullScreen::wrapExistingRenderer(*renderer, document());
m_fullscreenElement->setContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(true);
document().resolveStyle(Document::ResolveStyleType::Rebuild);
dispatchFullscreenChangeEvents();
}
void FullscreenManager::didEnterFullscreen()
{
if (!m_fullscreenElement)
return;
if (!hasLivingRenderTree() || backForwardCacheState() != Document::NotInBackForwardCache)
return;
m_fullscreenElement->didBecomeFullscreenElement();
}
void FullscreenManager::willExitFullscreen()
{
auto fullscreenElement = fullscreenOrPendingElement();
if (!fullscreenElement)
return;
if (!hasLivingRenderTree() || backForwardCacheState() != Document::NotInBackForwardCache)
return;
fullscreenElement->willStopBeingFullscreenElement();
}
void FullscreenManager::didExitFullscreen()
{
auto fullscreenElement = fullscreenOrPendingElement();
if (!fullscreenElement)
return;
if (!hasLivingRenderTree() || backForwardCacheState() != Document::NotInBackForwardCache)
return;
fullscreenElement->setContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(false);
m_areKeysEnabledInFullscreen = false;
unwrapFullscreenRenderer(m_fullscreenRenderer.get(), m_fullscreenElement.get());
m_fullscreenElement = nullptr;
m_pendingFullscreenElement = nullptr;
scheduleFullStyleRebuild();
// When webkitCancelFullscreen is called, we call webkitExitFullscreen on the topDocument(). That
// means that the events will be queued there. So if we have no events here, start the timer on
// the exiting document.
bool eventTargetQueuesEmpty = m_fullscreenChangeEventTargetQueue.isEmpty() && m_fullscreenErrorEventTargetQueue.isEmpty();
Document& exitingDocument = eventTargetQueuesEmpty ? topDocument() : document();
exitingDocument.fullscreenManager().dispatchFullscreenChangeEvents();
}
void FullscreenManager::setFullscreenRenderer(RenderTreeBuilder& builder, RenderFullScreen& renderer)
{
if (&renderer == m_fullscreenRenderer)
return;
if (m_savedPlaceholderRenderStyle)
builder.createPlaceholderForFullScreen(renderer, WTFMove(m_savedPlaceholderRenderStyle), m_savedPlaceholderFrameRect);
else if (m_fullscreenRenderer && m_fullscreenRenderer->placeholder()) {
auto* placeholder = m_fullscreenRenderer->placeholder();
builder.createPlaceholderForFullScreen(renderer, RenderStyle::clonePtr(placeholder->style()), placeholder->frameRect());
}
if (m_fullscreenRenderer)
builder.destroy(*m_fullscreenRenderer);
ASSERT(!m_fullscreenRenderer);
m_fullscreenRenderer = makeWeakPtr(renderer);
}
RenderFullScreen* FullscreenManager::fullscreenRenderer() const
{
return m_fullscreenRenderer.get();
}
void FullscreenManager::dispatchFullscreenChangeEvents()
{
// Since we dispatch events in this function, it's possible that the
// document will be detached and GC'd. We protect it here to make sure we
// can finish the function successfully.
Ref<Document> protectedDocument(document());
Deque<RefPtr<Node>> changeQueue;
m_fullscreenChangeEventTargetQueue.swap(changeQueue);
Deque<RefPtr<Node>> errorQueue;
m_fullscreenErrorEventTargetQueue.swap(errorQueue);
dispatchFullscreenChangeOrErrorEvent(changeQueue, eventNames().webkitfullscreenchangeEvent, /* shouldNotifyMediaElement */ true);
dispatchFullscreenChangeOrErrorEvent(errorQueue, eventNames().webkitfullscreenerrorEvent, /* shouldNotifyMediaElement */ false);
}
void FullscreenManager::dispatchFullscreenChangeOrErrorEvent(Deque<RefPtr<Node>>& queue, const AtomString& eventName, bool shouldNotifyMediaElement)
{
while (!queue.isEmpty()) {
RefPtr<Node> node = queue.takeFirst();
if (!node)
node = documentElement();
// The dispatchEvent below may have blown away our documentElement.
if (!node)
continue;
// If the element was removed from our tree, also message the documentElement. Since we may
// have a document hierarchy, check that node isn't in another document.
if (!node->isConnected())
queue.append(documentElement());
#if ENABLE(VIDEO)
if (shouldNotifyMediaElement && is<HTMLMediaElement>(*node))
downcast<HTMLMediaElement>(*node).enteredOrExitedFullscreen();
#else
UNUSED_PARAM(shouldNotifyMediaElement);
#endif
node->dispatchEvent(Event::create(eventName, Event::CanBubble::Yes, Event::IsCancelable::No));
}
}
void FullscreenManager::adjustFullscreenElementOnNodeRemoval(Node& node, Document::NodeRemoval nodeRemoval)
{
auto fullscreenElement = fullscreenOrPendingElement();
if (!fullscreenElement)
return;
bool elementInSubtree = false;
if (nodeRemoval == Document::NodeRemoval::ChildrenOfNode)
elementInSubtree = fullscreenElement->isDescendantOf(node);
else
elementInSubtree = (fullscreenElement == &node) || fullscreenElement->isDescendantOf(node);
if (elementInSubtree) {
fullscreenElement->setContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(false);
cancelFullscreen();
}
}
bool FullscreenManager::isAnimatingFullscreen() const
{
return m_isAnimatingFullscreen;
}
void FullscreenManager::setAnimatingFullscreen(bool flag)
{
if (m_isAnimatingFullscreen == flag)
return;
m_isAnimatingFullscreen = flag;
if (m_fullscreenElement && m_fullscreenElement->isDescendantOf(document())) {
m_fullscreenElement->invalidateStyleForSubtree();
scheduleFullStyleRebuild();
}
}
bool FullscreenManager::areFullscreenControlsHidden() const
{
return m_areFullscreenControlsHidden;
}
void FullscreenManager::setFullscreenControlsHidden(bool flag)
{
if (m_areFullscreenControlsHidden == flag)
return;
m_areFullscreenControlsHidden = flag;
if (m_fullscreenElement && m_fullscreenElement->isDescendantOf(document())) {
m_fullscreenElement->invalidateStyleForSubtree();
scheduleFullStyleRebuild();
}
}
void FullscreenManager::clear()
{
m_fullscreenElement = nullptr;
m_pendingFullscreenElement = nullptr;
m_fullscreenElementStack.clear();
}
void FullscreenManager::emptyEventQueue()
{
m_fullscreenChangeEventTargetQueue.clear();
m_fullscreenErrorEventTargetQueue.clear();
}
void FullscreenManager::clearFullscreenElementStack()
{
m_fullscreenElementStack.clear();
}
void FullscreenManager::popFullscreenElementStack()
{
if (m_fullscreenElementStack.isEmpty())
return;
m_fullscreenElementStack.removeLast();
}
void FullscreenManager::pushFullscreenElementStack(Element& element)
{
m_fullscreenElementStack.append(&element);
}
void FullscreenManager::addDocumentToFullscreenChangeEventQueue(Document& document)
{
Node* target = document.fullscreenManager().fullscreenElement();
if (!target)
target = document.fullscreenManager().currentFullscreenElement();
if (!target)
target = &document;
m_fullscreenChangeEventTargetQueue.append(target);
}
}
#endif