WeakPtr breaks vtables when upcasting to base classes
[WebKit-https.git] / Source / WebCore / dom / FullscreenManager.cpp
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
45 namespace WebCore {
46
47 using namespace HTMLNames;
48
49 static 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
60 FullscreenManager::FullscreenManager(Document& document)
61     : m_document { document }
62 {
63 }
64
65 FullscreenManager::~FullscreenManager() = default;
66
67 bool FullscreenManager::fullscreenIsAllowedForElement(Element& element) const
68 {
69     return isAttributeOnAllOwners(allowfullscreenAttr, webkitallowfullscreenAttr, element.document().ownerElement());
70 }
71
72 void 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
221 void 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
240 void 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
313 bool 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
322 static 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
333 void 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
375 void 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
386 void 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
397 void 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
423 void 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
442 RenderFullScreen* FullscreenManager::fullscreenRenderer() const
443 {
444     return m_fullscreenRenderer.get();
445 }
446
447 void 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
461 void FullscreenManager::dispatchFullscreenChangeOrErrorEvent(Deque<RefPtr<Node>>& queue, const AtomicString& 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
486 void FullscreenManager::fullscreenElementRemoved()
487 {
488     m_fullscreenElement->setContainsFullScreenElementOnAncestorsCrossingFrameBoundaries(false);
489     cancelFullscreen();
490 }
491
492 void 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
507 bool FullscreenManager::isAnimatingFullscreen() const
508 {
509     return m_isAnimatingFullscreen;
510 }
511
512 void 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
524 bool FullscreenManager::areFullscreenControlsHidden() const
525 {
526     return m_areFullscreenControlsHidden;
527 }
528
529 void 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
541 void FullscreenManager::clear()
542 {
543     m_fullscreenElement = nullptr;
544     m_fullscreenElementStack.clear();
545 }
546
547 void FullscreenManager::emptyEventQueue()
548 {
549     m_fullscreenChangeEventTargetQueue.clear();
550     m_fullscreenErrorEventTargetQueue.clear();
551 }
552
553 void FullscreenManager::clearFullscreenElementStack()
554 {
555     m_fullscreenElementStack.clear();
556 }
557
558 void FullscreenManager::popFullscreenElementStack()
559 {
560     if (m_fullscreenElementStack.isEmpty())
561         return;
562
563     m_fullscreenElementStack.removeLast();
564 }
565
566 void FullscreenManager::pushFullscreenElementStack(Element& element)
567 {
568     m_fullscreenElementStack.append(&element);
569 }
570
571 void 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