1/*
2 * Copyright (C) 2019 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "FullscreenManager.h"
28
29#if ENABLE(FULLSCREEN_API)
30
31#include "Chrome.h"
32#include "ChromeClient.h"
33#include "Document.h"
34#include "Element.h"
35#include "EventNames.h"
36#include "Frame.h"
37#include "HTMLFrameOwnerElement.h"
38#include "HTMLMediaElement.h"
39#include "Page.h"
40#include "QualifiedName.h"
41#include "RenderFullScreen.h"
42#include "RenderTreeBuilder.h"
43#include "Settings.h"
44
45namespace WebCore {
46
47using namespace HTMLNames;
48
49static bool isAttributeOnAllOwners(const QualifiedName& attribute, const QualifiedName& prefixedAttribute, const HTMLFrameOwnerElement* owner)
50{
51 if (!owner)
52 return true;
53 do {
54 if (!(owner->hasAttribute(attribute) || owner->hasAttribute(prefixedAttribute)))
55 return false;
56 } while ((owner = owner->document().ownerElement()));
57 return true;
58}
59
60FullscreenManager::FullscreenManager(Document& document)
61 : m_document { document }
62{
63}
64
65FullscreenManager::~FullscreenManager() = default;
66
67bool FullscreenManager::fullscreenIsAllowedForElement(Element& element) const
68{
69 return isAttributeOnAllOwners(allowfullscreenAttr, webkitallowfullscreenAttr, element.document().ownerElement());
70}
71
72void FullscreenManager::requestFullscreenForElement(Element* element, FullscreenCheckType checkType)
73{
74 if (!element)
75 element = documentElement();
76
77 auto failedPreflights = [this](auto element) mutable {
78 m_fullscreenErrorEventTargetQueue.append(WTFMove(element));
79 m_fullscreenTaskQueue.enqueueTask([this] {
80 dispatchFullscreenChangeEvents();
81 });
82 };
83
84 // 1. If any of the following conditions are true, terminate these steps and queue a task to fire
85 // an event named fullscreenerror with its bubbles attribute set to true on the context object's
86 // node document:
87
88 // This algorithm is not allowed to show a pop-up:
89 // An algorithm is allowed to show a pop-up if, in the task in which the algorithm is running, either:
90 // - an activation behavior is currently being processed whose click event was trusted, or
91 // - the event listener for a trusted click event is being handled.
92 if (!UserGestureIndicator::processingUserGesture()) {
93 failedPreflights(WTFMove(element));
94 return;
95 }
96
97 // We do not allow pressing the Escape key as a user gesture to enter fullscreen since this is the key
98 // to exit fullscreen.
99 if (UserGestureIndicator::currentUserGesture()->gestureType() == UserGestureType::EscapeKey) {
100 document().addConsoleMessage(MessageSource::Security, MessageLevel::Error, "The Escape key may not be used as a user gesture to enter fullscreen"_s);
101 failedPreflights(WTFMove(element));
102 return;
103 }
104
105 // There is a previously-established user preference, security risk, or platform limitation.
106 if (!page() || !page()->settings().fullScreenEnabled()) {
107 failedPreflights(WTFMove(element));
108 return;
109 }
110
111 bool hasKeyboardAccess = true;
112 if (!page()->chrome().client().supportsFullScreenForElement(*element, hasKeyboardAccess)) {
113 // The new full screen API does not accept a "flags" parameter, so fall back to disallowing
114 // keyboard input if the chrome client refuses to allow keyboard input.
115 hasKeyboardAccess = false;
116
117 if (!page()->chrome().client().supportsFullScreenForElement(*element, hasKeyboardAccess)) {
118 failedPreflights(WTFMove(element));
119 return;
120 }
121 }
122
123 m_fullscreenTaskQueue.enqueueTask([this, element = makeRefPtr(element), checkType, hasKeyboardAccess, failedPreflights] () mutable {
124 // Don't allow fullscreen if document is hidden.
125 if (document().hidden()) {
126 failedPreflights(WTFMove(element));
127 return;
128 }
129
130 // The context object is not in a document.
131 if (!element->isConnected()) {
132 failedPreflights(WTFMove(element));
133 return;
134 }
135
136 // The context object's node document, or an ancestor browsing context's document does not have
137 // the fullscreen enabled flag set.
138 if (checkType == EnforceIFrameAllowFullscreenRequirement && !fullscreenIsAllowedForElement(*element)) {
139 failedPreflights(WTFMove(element));
140 return;
141 }
142
143 // The context object's node document fullscreen element stack is not empty and its top element
144 // is not an ancestor of the context object.
145 if (!m_fullscreenElementStack.isEmpty() && !m_fullscreenElementStack.last()->contains(element.get())) {
146 failedPreflights(WTFMove(element));
147 return;
148 }
149
150 // A descendant browsing context's document has a non-empty fullscreen element stack.
151 bool descendentHasNonEmptyStack = false;
152 for (Frame* descendant = frame() ? frame()->tree().traverseNext() : nullptr; descendant; descendant = descendant->tree().traverseNext()) {
153 if (descendant->document()->fullscreenManager().fullscreenElement()) {
154 descendentHasNonEmptyStack = true;
155 break;
156 }
157 }
158 if (descendentHasNonEmptyStack) {
159 failedPreflights(WTFMove(element));
160 return;
161 }
162
163 // 2. Let doc be element's node document. (i.e. "this")
164 Document* currentDoc = &document();
165
166 // 3. Let docs be all doc's ancestor browsing context's documents (if any) and doc.
167 Deque<Document*> docs;
168
169 do {
170 docs.prepend(currentDoc);
171 currentDoc = currentDoc->ownerElement() ? &currentDoc->ownerElement()->document() : nullptr;
172 } while (currentDoc);
173
174 // 4. For each document in docs, run these substeps:
175 Deque<Document*>::iterator current = docs.begin(), following = docs.begin();
176
177 do {
178 ++following;
179
180 // 1. Let following document be the document after document in docs, or null if there is no
181 // such document.
182 Document* currentDoc = *current;
183 Document* followingDoc = following != docs.end() ? *following : nullptr;
184
185 // 2. If following document is null, push context object on document's fullscreen element
186 // stack, and queue a task to fire an event named fullscreenchange with its bubbles attribute
187 // set to true on the document.
188 if (!followingDoc) {
189 currentDoc->fullscreenManager().pushFullscreenElementStack(*element);
190 addDocumentToFullscreenChangeEventQueue(*currentDoc);
191 continue;
192 }
193
194 // 3. Otherwise, if document's fullscreen element stack is either empty or its top element
195 // is not following document's browsing context container,
196 Element* topElement = currentDoc->fullscreenManager().fullscreenElement();
197 if (!topElement || topElement != followingDoc->ownerElement()) {
198 // ...push following document's browsing context container on document's fullscreen element
199 // stack, and queue a task to fire an event named fullscreenchange with its bubbles attribute
200 // set to true on document.
201 currentDoc->fullscreenManager().pushFullscreenElementStack(*followingDoc->ownerElement());
202 addDocumentToFullscreenChangeEventQueue(*currentDoc);
203 continue;
204 }
205
206 // 4. Otherwise, do nothing for this document. It stays the same.
207 } while (++current != docs.end());
208
209 // 5. Return, and run the remaining steps asynchronously.
210 // 6. Optionally, perform some animation.
211 m_areKeysEnabledInFullscreen = hasKeyboardAccess;
212 m_fullscreenTaskQueue.enqueueTask([this, element = WTFMove(element)] {
213 if (auto page = this->page())
214 page->chrome().client().enterFullScreenForElement(*element.get());
215 });
216
217 // 7. Optionally, display a message indicating how the user can exit displaying the context object fullscreen.
218 });
219}
220
221void FullscreenManager::cancelFullscreen()
222{
223 // The Mozilla "cancelFullscreen()" API behaves like the W3C "fully exit fullscreen" behavior, which
224 // is defined as:
225 // "To fully exit fullscreen act as if the exitFullscreen() method was invoked on the top-level browsing
226 // context's document and subsequently empty that document's fullscreen element stack."
227 Document& topDocument = document().topDocument();
228 if (!topDocument.fullscreenManager().fullscreenElement())
229 return;
230
231 // To achieve that aim, remove all the elements from the top document's stack except for the first before
232 // calling webkitExitFullscreen():
233 Vector<RefPtr<Element>> replacementFullscreenElementStack;
234 replacementFullscreenElementStack.append(topDocument.fullscreenManager().fullscreenElement());
235 topDocument.fullscreenManager().m_fullscreenElementStack.swap(replacementFullscreenElementStack);
236
237 topDocument.fullscreenManager().exitFullscreen();
238}
239
240void FullscreenManager::exitFullscreen()
241{
242 // The exitFullscreen() method must run these steps:
243
244 // 1. Let doc be the context object. (i.e. "this")
245 Document* currentDoc = &document();
246
247 // 2. If doc's fullscreen element stack is empty, terminate these steps.
248 if (m_fullscreenElementStack.isEmpty())
249 return;
250
251 // 3. Let descendants be all the doc's descendant browsing context's documents with a non-empty fullscreen
252 // element stack (if any), ordered so that the child of the doc is last and the document furthest
253 // away from the doc is first.
254 Deque<RefPtr<Document>> descendants;
255 for (Frame* descendant = frame() ? frame()->tree().traverseNext() : nullptr; descendant; descendant = descendant->tree().traverseNext()) {
256 if (descendant->document()->fullscreenManager().fullscreenElement())
257 descendants.prepend(descendant->document());
258 }
259
260 // 4. For each descendant in descendants, empty descendant's fullscreen element stack, and queue a
261 // task to fire an event named fullscreenchange with its bubbles attribute set to true on descendant.
262 for (auto& document : descendants) {
263 document->fullscreenManager().clearFullscreenElementStack();
264 addDocumentToFullscreenChangeEventQueue(*document);
265 }
266
267 // 5. While doc is not null, run these substeps:
268 Element* newTop = nullptr;
269 while (currentDoc) {
270 // 1. Pop the top element of doc's fullscreen element stack.
271 currentDoc->fullscreenManager().popFullscreenElementStack();
272
273 // If doc's fullscreen element stack is non-empty and the element now at the top is either
274 // not in a document or its node document is not doc, repeat this substep.
275 newTop = currentDoc->fullscreenManager().fullscreenElement();
276 if (newTop && (!newTop->isConnected() || &newTop->document() != currentDoc))
277 continue;
278
279 // 2. Queue a task to fire an event named fullscreenchange with its bubbles attribute set to true
280 // on doc.
281 addDocumentToFullscreenChangeEventQueue(*currentDoc);
282
283 // 3. If doc's fullscreen element stack is empty and doc's browsing context has a browsing context
284 // container, set doc to that browsing context container's node document.
285 if (!newTop && currentDoc->ownerElement()) {
286 currentDoc = &currentDoc->ownerElement()->document();
287 continue;
288 }
289
290 // 4. Otherwise, set doc to null.
291 currentDoc = nullptr;
292 }
293
294 // 6. Return, and run the remaining steps asynchronously.
295 // 7. Optionally, perform some animation.
296 m_fullscreenTaskQueue.enqueueTask([this, newTop = makeRefPtr(newTop), fullscreenElement = m_fullscreenElement] {
297 auto* page = this->page();
298 if (!page)
299 return;
300
301 // Only exit out of full screen window mode if there are no remaining elements in the
302 // full screen stack.
303 if (!newTop) {
304 page->chrome().client().exitFullScreenForElement(fullscreenElement.get());
305 return;
306 }
307
308 // Otherwise, notify the chrome of the new full screen element.
309 page->chrome().client().enterFullScreenForElement(*newTop);
310 });
311}
312
313bool FullscreenManager::isFullscreenEnabled() const
314{
315 // 4. The fullscreenEnabled attribute must return true if the context object and all ancestor
316 // browsing context's documents have their fullscreen enabled flag set, or false otherwise.
317
318 // Top-level browsing contexts are implied to have their allowFullscreen attribute set.
319 return isAttributeOnAllOwners(allowfullscreenAttr, webkitallowfullscreenAttr, document().ownerElement());
320}
321
322static void unwrapFullscreenRenderer(RenderFullScreen* fullscreenRenderer, Element* fullscreenElement)
323{
324 if (!fullscreenRenderer)
325 return;
326 bool requiresRenderTreeRebuild;
327 fullscreenRenderer->unwrapRenderer(requiresRenderTreeRebuild);
328
329 if (requiresRenderTreeRebuild && fullscreenElement && fullscreenElement->parentElement())
330 fullscreenElement->parentElement()->invalidateStyleAndRenderersForSubtree();
331}
332
333void FullscreenManager::willEnterFullscreen(Element& element)
334{
335 if (!document().hasLivingRenderTree() || document().pageCacheState() != Document::NotInPageCache)
336 return;
337
338 // Protect against being called after the document has been removed from the page.
339 if (!page())
340 return;
341
342 ASSERT(page()->settings().fullScreenEnabled());
343
344 unwrapFullscreenRenderer(m_fullscreenRenderer.get(), m_fullscreenElement.get());
345
346 element.willBecomeFullscreenElement();
347
348 m_fullscreenElement = &element;
349
350#if USE(NATIVE_FULLSCREEN_VIDEO)
351 if (element.isMediaElement())
352 return;
353#endif
354
355 // Create a placeholder block for a the full-screen element, to keep the page from reflowing
356 // when the element is removed from the normal flow. Only do this for a RenderBox, as only
357 // a box will have a frameRect. The placeholder will be created in setFullscreenRenderer()
358 // during layout.
359 auto renderer = m_fullscreenElement->renderer();
360 bool shouldCreatePlaceholder = is<RenderBox>(renderer);
361 if (shouldCreatePlaceholder) {
362 m_savedPlaceholderFrameRect = downcast<RenderBox>(*renderer).frameRect();
363 m_savedPlaceholderRenderStyle = RenderStyle::clonePtr(renderer->style());
364 }
365
366 if (m_fullscreenElement != documentElement() && renderer)
367 RenderFullScreen::wrapExistingRenderer(*renderer, document());
368
369 m_fullscreenElement->setContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(true);
370
371 document().resolveStyle(Document::ResolveStyleType::Rebuild);
372 dispatchFullscreenChangeEvents();
373}
374
375void FullscreenManager::didEnterFullscreen()
376{
377 if (!m_fullscreenElement)
378 return;
379
380 if (!hasLivingRenderTree() || pageCacheState() != Document::NotInPageCache)
381 return;
382
383 m_fullscreenElement->didBecomeFullscreenElement();
384}
385
386void FullscreenManager::willExitFullscreen()
387{
388 if (!m_fullscreenElement)
389 return;
390
391 if (!hasLivingRenderTree() || pageCacheState() != Document::NotInPageCache)
392 return;
393
394 m_fullscreenElement->willStopBeingFullscreenElement();
395}
396
397void FullscreenManager::didExitFullscreen()
398{
399 if (!m_fullscreenElement)
400 return;
401
402 if (!hasLivingRenderTree() || pageCacheState() != Document::NotInPageCache)
403 return;
404
405 m_fullscreenElement->setContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(false);
406
407 m_areKeysEnabledInFullscreen = false;
408
409 unwrapFullscreenRenderer(m_fullscreenRenderer.get(), m_fullscreenElement.get());
410
411 m_fullscreenElement = nullptr;
412 scheduleFullStyleRebuild();
413
414 // When webkitCancelFullscreen is called, we call webkitExitFullscreen on the topDocument(). That
415 // means that the events will be queued there. So if we have no events here, start the timer on
416 // the exiting document.
417 bool eventTargetQueuesEmpty = m_fullscreenChangeEventTargetQueue.isEmpty() && m_fullscreenErrorEventTargetQueue.isEmpty();
418 Document& exitingDocument = eventTargetQueuesEmpty ? topDocument() : document();
419
420 exitingDocument.fullscreenManager().dispatchFullscreenChangeEvents();
421}
422
423void FullscreenManager::setFullscreenRenderer(RenderTreeBuilder& builder, RenderFullScreen& renderer)
424{
425 if (&renderer == m_fullscreenRenderer)
426 return;
427
428 if (m_savedPlaceholderRenderStyle)
429 builder.createPlaceholderForFullScreen(renderer, WTFMove(m_savedPlaceholderRenderStyle), m_savedPlaceholderFrameRect);
430 else if (m_fullscreenRenderer && m_fullscreenRenderer->placeholder()) {
431 auto* placeholder = m_fullscreenRenderer->placeholder();
432 builder.createPlaceholderForFullScreen(renderer, RenderStyle::clonePtr(placeholder->style()), placeholder->frameRect());
433 }
434
435 if (m_fullscreenRenderer)
436 builder.destroy(*m_fullscreenRenderer);
437 ASSERT(!m_fullscreenRenderer);
438
439 m_fullscreenRenderer = makeWeakPtr(renderer);
440}
441
442RenderFullScreen* FullscreenManager::fullscreenRenderer() const
443{
444 return m_fullscreenRenderer.get();
445}
446
447void FullscreenManager::dispatchFullscreenChangeEvents()
448{
449 // Since we dispatch events in this function, it's possible that the
450 // document will be detached and GC'd. We protect it here to make sure we
451 // can finish the function successfully.
452 Ref<Document> protectedDocument(document());
453 Deque<RefPtr<Node>> changeQueue;
454 m_fullscreenChangeEventTargetQueue.swap(changeQueue);
455 Deque<RefPtr<Node>> errorQueue;
456 m_fullscreenErrorEventTargetQueue.swap(errorQueue);
457 dispatchFullscreenChangeOrErrorEvent(changeQueue, eventNames().webkitfullscreenchangeEvent, /* shouldNotifyMediaElement */ true);
458 dispatchFullscreenChangeOrErrorEvent(errorQueue, eventNames().webkitfullscreenerrorEvent, /* shouldNotifyMediaElement */ false);
459}
460
461void FullscreenManager::dispatchFullscreenChangeOrErrorEvent(Deque<RefPtr<Node>>& queue, const AtomString& eventName, bool shouldNotifyMediaElement)
462{
463 while (!queue.isEmpty()) {
464 RefPtr<Node> node = queue.takeFirst();
465 if (!node)
466 node = documentElement();
467 // The dispatchEvent below may have blown away our documentElement.
468 if (!node)
469 continue;
470
471 // If the element was removed from our tree, also message the documentElement. Since we may
472 // have a document hierarchy, check that node isn't in another document.
473 if (!node->isConnected())
474 queue.append(documentElement());
475
476#if ENABLE(VIDEO)
477 if (shouldNotifyMediaElement && is<HTMLMediaElement>(*node))
478 downcast<HTMLMediaElement>(*node).enteredOrExitedFullscreen();
479#else
480 UNUSED_PARAM(shouldNotifyMediaElement);
481#endif
482 node->dispatchEvent(Event::create(eventName, Event::CanBubble::Yes, Event::IsCancelable::No));
483 }
484}
485
486void FullscreenManager::fullscreenElementRemoved()
487{
488 m_fullscreenElement->setContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(false);
489 cancelFullscreen();
490}
491
492void FullscreenManager::adjustFullscreenElementOnNodeRemoval(Node& node, Document::NodeRemoval nodeRemoval)
493{
494 if (!m_fullscreenElement)
495 return;
496
497 bool elementInSubtree = false;
498 if (nodeRemoval == Document::NodeRemoval::ChildrenOfNode)
499 elementInSubtree = m_fullscreenElement->isDescendantOf(node);
500 else
501 elementInSubtree = (m_fullscreenElement == &node) || m_fullscreenElement->isDescendantOf(node);
502
503 if (elementInSubtree)
504 fullscreenElementRemoved();
505}
506
507bool FullscreenManager::isAnimatingFullscreen() const
508{
509 return m_isAnimatingFullscreen;
510}
511
512void FullscreenManager::setAnimatingFullscreen(bool flag)
513{
514 if (m_isAnimatingFullscreen == flag)
515 return;
516 m_isAnimatingFullscreen = flag;
517
518 if (m_fullscreenElement && m_fullscreenElement->isDescendantOf(document())) {
519 m_fullscreenElement->invalidateStyleForSubtree();
520 scheduleFullStyleRebuild();
521 }
522}
523
524bool FullscreenManager::areFullscreenControlsHidden() const
525{
526 return m_areFullscreenControlsHidden;
527}
528
529void FullscreenManager::setFullscreenControlsHidden(bool flag)
530{
531 if (m_areFullscreenControlsHidden == flag)
532 return;
533 m_areFullscreenControlsHidden = flag;
534
535 if (m_fullscreenElement && m_fullscreenElement->isDescendantOf(document())) {
536 m_fullscreenElement->invalidateStyleForSubtree();
537 scheduleFullStyleRebuild();
538 }
539}
540
541void FullscreenManager::clear()
542{
543 m_fullscreenElement = nullptr;
544 m_fullscreenElementStack.clear();
545}
546
547void FullscreenManager::emptyEventQueue()
548{
549 m_fullscreenChangeEventTargetQueue.clear();
550 m_fullscreenErrorEventTargetQueue.clear();
551}
552
553void FullscreenManager::clearFullscreenElementStack()
554{
555 m_fullscreenElementStack.clear();
556}
557
558void FullscreenManager::popFullscreenElementStack()
559{
560 if (m_fullscreenElementStack.isEmpty())
561 return;
562
563 m_fullscreenElementStack.removeLast();
564}
565
566void FullscreenManager::pushFullscreenElementStack(Element& element)
567{
568 m_fullscreenElementStack.append(&element);
569}
570
571void FullscreenManager::addDocumentToFullscreenChangeEventQueue(Document& document)
572{
573 Node* target = document.fullscreenManager().fullscreenElement();
574 if (!target)
575 target = document.fullscreenManager().currentFullscreenElement();
576 if (!target)
577 target = &document;
578 m_fullscreenChangeEventTargetQueue.append(target);
579}
580
581}
582
583#endif
584