Crash in the hit testing code via HTMLPlugInElement::isReplacementObscured()
[WebKit-https.git] / Source / WebCore / html / HTMLPlugInElement.cpp
1 /*
2  * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
3  *           (C) 1999 Antti Koivisto (koivisto@kde.org)
4  *           (C) 2000 Stefan Schimanski (1Stein@gmx.de)
5  * Copyright (C) 2004-2017 Apple Inc. All rights reserved.
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Library General Public
9  * License as published by the Free Software Foundation; either
10  * version 2 of the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Library General Public License for more details.
16  *
17  * You should have received a copy of the GNU Library General Public License
18  * along with this library; see the file COPYING.LIB.  If not, write to
19  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20  * Boston, MA 02110-1301, USA.
21  */
22
23 #include "config.h"
24 #include "HTMLPlugInElement.h"
25
26 #include "BridgeJSC.h"
27 #include "CSSPropertyNames.h"
28 #include "Document.h"
29 #include "Event.h"
30 #include "EventHandler.h"
31 #include "Frame.h"
32 #include "FrameLoader.h"
33 #include "FrameTree.h"
34 #include "HTMLNames.h"
35 #include "HitTestResult.h"
36 #include "Logging.h"
37 #include "MIMETypeRegistry.h"
38 #include "Page.h"
39 #include "PluginData.h"
40 #include "PluginReplacement.h"
41 #include "PluginViewBase.h"
42 #include "RenderEmbeddedObject.h"
43 #include "RenderLayer.h"
44 #include "RenderSnapshottedPlugIn.h"
45 #include "RenderView.h"
46 #include "RenderWidget.h"
47 #include "RuntimeEnabledFeatures.h"
48 #include "ScriptController.h"
49 #include "Settings.h"
50 #include "ShadowRoot.h"
51 #include "SubframeLoader.h"
52 #include "Widget.h"
53 #include <wtf/IsoMallocInlines.h>
54
55 #if ENABLE(NETSCAPE_PLUGIN_API)
56 #include "npruntime_impl.h"
57 #endif
58
59 #if PLATFORM(COCOA)
60 #include "QuickTimePluginReplacement.h"
61 #include "YouTubePluginReplacement.h"
62 #endif
63
64 namespace WebCore {
65
66 WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLPlugInElement);
67
68 using namespace HTMLNames;
69
70 HTMLPlugInElement::HTMLPlugInElement(const QualifiedName& tagName, Document& document)
71     : HTMLFrameOwnerElement(tagName, document)
72     , m_inBeforeLoadEventHandler(false)
73     , m_swapRendererTimer(*this, &HTMLPlugInElement::swapRendererTimerFired)
74     , m_isCapturingMouseEvents(false)
75     , m_displayState(Playing)
76 {
77     setHasCustomStyleResolveCallbacks();
78 }
79
80 HTMLPlugInElement::~HTMLPlugInElement()
81 {
82     ASSERT(!m_instance); // cleared in detach()
83 }
84
85 bool HTMLPlugInElement::canProcessDrag() const
86 {
87     const PluginViewBase* plugin = is<PluginViewBase>(pluginWidget()) ? downcast<PluginViewBase>(pluginWidget()) : nullptr;
88     return plugin ? plugin->canProcessDrag() : false;
89 }
90
91 bool HTMLPlugInElement::willRespondToMouseClickEvents()
92 {
93     if (isDisabledFormControl())
94         return false;
95     auto renderer = this->renderer();
96     return renderer && renderer->isWidget();
97 }
98
99 void HTMLPlugInElement::willDetachRenderers()
100 {
101     m_instance = nullptr;
102
103     if (m_isCapturingMouseEvents) {
104         if (RefPtr<Frame> frame = document().frame())
105             frame->eventHandler().setCapturingMouseEventsElement(nullptr);
106         m_isCapturingMouseEvents = false;
107     }
108 }
109
110 void HTMLPlugInElement::resetInstance()
111 {
112     m_instance = nullptr;
113 }
114
115 JSC::Bindings::Instance* HTMLPlugInElement::bindingsInstance()
116 {
117     auto frame = makeRefPtr(document().frame());
118     if (!frame)
119         return nullptr;
120
121     // If the host dynamically turns off JavaScript (or Java) we will still return
122     // the cached allocated Bindings::Instance.  Not supporting this edge-case is OK.
123
124     if (!m_instance) {
125         if (auto widget = makeRefPtr(pluginWidget()))
126             m_instance = frame->script().createScriptInstanceForWidget(widget.get());
127     }
128     return m_instance.get();
129 }
130
131 bool HTMLPlugInElement::guardedDispatchBeforeLoadEvent(const String& sourceURL)
132 {
133     // FIXME: Our current plug-in loading design can't guarantee the following
134     // assertion is true, since plug-in loading can be initiated during layout,
135     // and synchronous layout can be initiated in a beforeload event handler!
136     // See <http://webkit.org/b/71264>.
137     // ASSERT(!m_inBeforeLoadEventHandler);
138     m_inBeforeLoadEventHandler = true;
139     // static_cast is used to avoid a compile error since dispatchBeforeLoadEvent
140     // is intentionally undefined on this class.
141     bool beforeLoadAllowedLoad = static_cast<HTMLFrameOwnerElement*>(this)->dispatchBeforeLoadEvent(sourceURL);
142     m_inBeforeLoadEventHandler = false;
143     return beforeLoadAllowedLoad;
144 }
145
146 Widget* HTMLPlugInElement::pluginWidget(PluginLoadingPolicy loadPolicy) const
147 {
148     if (m_inBeforeLoadEventHandler) {
149         // The plug-in hasn't loaded yet, and it makes no sense to try to load if beforeload handler happened to touch the plug-in element.
150         // That would recursively call beforeload for the same element.
151         return nullptr;
152     }
153
154     RenderWidget* renderWidget = loadPolicy == PluginLoadingPolicy::Load ? renderWidgetLoadingPlugin() : this->renderWidget();
155     if (!renderWidget)
156         return nullptr;
157
158     return renderWidget->widget();
159 }
160
161 bool HTMLPlugInElement::isPresentationAttribute(const QualifiedName& name) const
162 {
163     if (name == widthAttr || name == heightAttr || name == vspaceAttr || name == hspaceAttr || name == alignAttr)
164         return true;
165     return HTMLFrameOwnerElement::isPresentationAttribute(name);
166 }
167
168 void HTMLPlugInElement::collectStyleForPresentationAttribute(const QualifiedName& name, const AtomicString& value, MutableStyleProperties& style)
169 {
170     if (name == widthAttr)
171         addHTMLLengthToStyle(style, CSSPropertyWidth, value);
172     else if (name == heightAttr)
173         addHTMLLengthToStyle(style, CSSPropertyHeight, value);
174     else if (name == vspaceAttr) {
175         addHTMLLengthToStyle(style, CSSPropertyMarginTop, value);
176         addHTMLLengthToStyle(style, CSSPropertyMarginBottom, value);
177     } else if (name == hspaceAttr) {
178         addHTMLLengthToStyle(style, CSSPropertyMarginLeft, value);
179         addHTMLLengthToStyle(style, CSSPropertyMarginRight, value);
180     } else if (name == alignAttr)
181         applyAlignmentAttributeToStyle(value, style);
182     else
183         HTMLFrameOwnerElement::collectStyleForPresentationAttribute(name, value, style);
184 }
185
186 void HTMLPlugInElement::defaultEventHandler(Event& event)
187 {
188     // Firefox seems to use a fake event listener to dispatch events to plug-in (tested with mouse events only).
189     // This is observable via different order of events - in Firefox, event listeners specified in HTML attributes fires first, then an event
190     // gets dispatched to plug-in, and only then other event listeners fire. Hopefully, this difference does not matter in practice.
191
192     // FIXME: Mouse down and scroll events are passed down to plug-in via custom code in EventHandler; these code paths should be united.
193
194     auto renderer = this->renderer();
195     if (!is<RenderWidget>(renderer))
196         return;
197
198     if (is<RenderEmbeddedObject>(*renderer)) {
199         if (downcast<RenderEmbeddedObject>(*renderer).isPluginUnavailable()) {
200             downcast<RenderEmbeddedObject>(*renderer).handleUnavailablePluginIndicatorEvent(&event);
201             return;
202         }
203
204         if (is<RenderSnapshottedPlugIn>(*renderer) && displayState() < Restarting) {
205             downcast<RenderSnapshottedPlugIn>(*renderer).handleEvent(event);
206             HTMLFrameOwnerElement::defaultEventHandler(event);
207             return;
208         }
209
210         if (displayState() < Playing)
211             return;
212     }
213
214     // Don't keep the widget alive over the defaultEventHandler call, since that can do things like navigate.
215     {
216         RefPtr<Widget> widget = downcast<RenderWidget>(*renderer).widget();
217         if (!widget)
218             return;
219         widget->handleEvent(event);
220         if (event.defaultHandled())
221             return;
222     }
223     HTMLFrameOwnerElement::defaultEventHandler(event);
224 }
225
226 bool HTMLPlugInElement::isKeyboardFocusable(KeyboardEvent*) const
227 {
228     // FIXME: Why is this check needed?
229     if (!document().page())
230         return false;
231
232     RefPtr<Widget> widget = pluginWidget();
233     if (!is<PluginViewBase>(widget))
234         return false;
235
236     return downcast<PluginViewBase>(*widget).supportsKeyboardFocus();
237 }
238
239 bool HTMLPlugInElement::isPluginElement() const
240 {
241     return true;
242 }
243
244 bool HTMLPlugInElement::isUserObservable() const
245 {
246     // No widget - can't be anything to see or hear here.
247     RefPtr<Widget> widget = pluginWidget(PluginLoadingPolicy::DoNotLoad);
248     if (!is<PluginViewBase>(widget))
249         return false;
250
251     PluginViewBase& pluginView = downcast<PluginViewBase>(*widget);
252
253     // If audio is playing (or might be) then the plugin is detectable.
254     if (pluginView.audioHardwareActivity() != AudioHardwareActivityType::IsInactive)
255         return true;
256
257     // If the plugin is visible and not vanishingly small in either dimension it is detectable.
258     return pluginView.isVisible() && pluginView.width() > 2 && pluginView.height() > 2;
259 }
260
261 bool HTMLPlugInElement::supportsFocus() const
262 {
263     if (HTMLFrameOwnerElement::supportsFocus())
264         return true;
265
266     if (useFallbackContent() || !is<RenderEmbeddedObject>(renderer()))
267         return false;
268     return !downcast<RenderEmbeddedObject>(*renderer()).isPluginUnavailable();
269 }
270
271 RenderPtr<RenderElement> HTMLPlugInElement::createElementRenderer(RenderStyle&& style, const RenderTreePosition& insertionPosition)
272 {
273     if (m_pluginReplacement && m_pluginReplacement->willCreateRenderer())
274         return m_pluginReplacement->createElementRenderer(*this, WTFMove(style), insertionPosition);
275
276     return createRenderer<RenderEmbeddedObject>(*this, WTFMove(style));
277 }
278
279 void HTMLPlugInElement::swapRendererTimerFired()
280 {
281     ASSERT(displayState() == PreparingPluginReplacement || displayState() == DisplayingSnapshot);
282     if (userAgentShadowRoot())
283         return;
284     
285     // Create a shadow root, which will trigger the code to add a snapshot container
286     // and reattach, thus making a new Renderer.
287     ensureUserAgentShadowRoot();
288 }
289
290 void HTMLPlugInElement::setDisplayState(DisplayState state)
291 {
292     if (state == m_displayState)
293         return;
294
295     m_displayState = state;
296     
297     m_swapRendererTimer.stop();
298     if (state == DisplayingSnapshot || displayState() == PreparingPluginReplacement)
299         m_swapRendererTimer.startOneShot(0_s);
300 }
301
302 void HTMLPlugInElement::didAddUserAgentShadowRoot(ShadowRoot& root)
303 {
304     if (!m_pluginReplacement || !document().page() || displayState() != PreparingPluginReplacement)
305         return;
306     
307     root.setResetStyleInheritance(true);
308     if (m_pluginReplacement->installReplacement(root)) {
309         setDisplayState(DisplayingPluginReplacement);
310         invalidateStyleAndRenderersForSubtree();
311     }
312 }
313
314 #if PLATFORM(COCOA)
315 static void registrar(const ReplacementPlugin&);
316 #endif
317
318 static Vector<ReplacementPlugin*>& registeredPluginReplacements()
319 {
320     static NeverDestroyed<Vector<ReplacementPlugin*>> registeredReplacements;
321     static bool enginesQueried = false;
322     
323     if (enginesQueried)
324         return registeredReplacements;
325     enginesQueried = true;
326
327 #if PLATFORM(COCOA)
328     QuickTimePluginReplacement::registerPluginReplacement(registrar);
329     YouTubePluginReplacement::registerPluginReplacement(registrar);
330 #endif
331     
332     return registeredReplacements;
333 }
334
335 #if PLATFORM(COCOA)
336 static void registrar(const ReplacementPlugin& replacement)
337 {
338     registeredPluginReplacements().append(new ReplacementPlugin(replacement));
339 }
340 #endif
341
342 static ReplacementPlugin* pluginReplacementForType(const URL& url, const String& mimeType)
343 {
344     Vector<ReplacementPlugin*>& replacements = registeredPluginReplacements();
345     if (replacements.isEmpty())
346         return nullptr;
347
348     String extension;
349     String lastPathComponent = url.lastPathComponent();
350     size_t dotOffset = lastPathComponent.reverseFind('.');
351     if (dotOffset != notFound)
352         extension = lastPathComponent.substring(dotOffset + 1);
353
354     String type = mimeType;
355     if (type.isEmpty() && url.protocolIsData())
356         type = mimeTypeFromDataURL(url.string());
357     
358     if (type.isEmpty() && !extension.isEmpty()) {
359         for (auto* replacement : replacements) {
360             if (replacement->supportsFileExtension(extension) && replacement->supportsURL(url))
361                 return replacement;
362         }
363     }
364     
365     if (type.isEmpty()) {
366         if (extension.isEmpty())
367             return nullptr;
368         type = MIMETypeRegistry::getMediaMIMETypeForExtension(extension);
369     }
370
371     if (type.isEmpty())
372         return nullptr;
373
374     for (auto* replacement : replacements) {
375         if (replacement->supportsType(type) && replacement->supportsURL(url))
376             return replacement;
377     }
378
379     return nullptr;
380 }
381
382 bool HTMLPlugInElement::requestObject(const String& url, const String& mimeType, const Vector<String>& paramNames, const Vector<String>& paramValues)
383 {
384     if (m_pluginReplacement)
385         return true;
386
387     URL completedURL;
388     if (!url.isEmpty())
389         completedURL = document().completeURL(url);
390
391     ReplacementPlugin* replacement = pluginReplacementForType(completedURL, mimeType);
392     if (!replacement || !replacement->isEnabledBySettings(document().settings()))
393         return false;
394
395     LOG(Plugins, "%p - Found plug-in replacement for %s.", this, completedURL.string().utf8().data());
396
397     m_pluginReplacement = replacement->create(*this, paramNames, paramValues);
398     setDisplayState(PreparingPluginReplacement);
399     return true;
400 }
401
402 JSC::JSObject* HTMLPlugInElement::scriptObjectForPluginReplacement()
403 {
404     if (m_pluginReplacement)
405         return m_pluginReplacement->scriptObject();
406     return nullptr;
407 }
408
409 bool HTMLPlugInElement::setReplacement(RenderEmbeddedObject::PluginUnavailabilityReason reason, const String& unavailabilityDescription)
410 {
411     if (!is<RenderEmbeddedObject>(renderer()))
412         return false;
413
414     if (reason == RenderEmbeddedObject::UnsupportedPlugin)
415         document().addConsoleMessage(MessageSource::JS, MessageLevel::Warning, "Tried to use an unsupported plug-in."_s);
416
417     Ref<HTMLPlugInElement> protectedThis(*this);
418     downcast<RenderEmbeddedObject>(*renderer()).setPluginUnavailabilityReasonWithDescription(reason, unavailabilityDescription);
419     bool replacementIsObscured = isReplacementObscured();
420     // hittest in isReplacementObscured() method could destroy the renderer. Let's refetch it.
421     if (is<RenderEmbeddedObject>(renderer()))
422         downcast<RenderEmbeddedObject>(*renderer()).setUnavailablePluginIndicatorIsHidden(replacementIsObscured);
423     return replacementIsObscured;
424 }
425
426 bool HTMLPlugInElement::isReplacementObscured()
427 {
428     auto topDocument = makeRef(document().topDocument());
429     auto topFrameView = makeRefPtr(topDocument->view());
430     if (!topFrameView)
431         return false;
432
433     topFrameView->updateLayoutAndStyleIfNeededRecursive();
434
435     // Updating the layout may have detached this document from the top document.
436     auto* renderView = topDocument->renderView();
437     if (!renderView || !document().view() || &document().topDocument() != topDocument.ptr())
438         return false;
439
440     if (!renderer() || !is<RenderEmbeddedObject>(*renderer()))
441         return false;
442     auto& pluginRenderer = downcast<RenderEmbeddedObject>(*renderer());
443     // Check the opacity of each layer containing the element or its ancestors.
444     float opacity = 1.0;
445     for (auto* layer = pluginRenderer.enclosingLayer(); layer; layer = layer->parent()) {
446         opacity *= layer->renderer().style().opacity();
447         if (opacity < 0.1)
448             return true;
449     }
450     // Calculate the absolute rect for the blocked plugin replacement text.
451     LayoutPoint absoluteLocation(pluginRenderer.absoluteBoundingBoxRect().location());
452     LayoutRect rect = pluginRenderer.unavailablePluginIndicatorBounds(absoluteLocation);
453     if (rect.isEmpty())
454         return true;
455     auto viewRect = document().view()->convertToRootView(snappedIntRect(rect));
456     auto x = viewRect.x();
457     auto y = viewRect.y();
458     auto width = viewRect.width();
459     auto height = viewRect.height();
460     // Hit test the center and near the corners of the replacement text to ensure
461     // it is visible and is not masked by other elements.
462     HitTestRequest request(HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::IgnoreClipping | HitTestRequest::DisallowUserAgentShadowContent | HitTestRequest::AllowChildFrameContent);
463     HitTestResult result;
464     HitTestLocation location = LayoutPoint(x + width / 2, y + height / 2);
465     ASSERT(!renderView->needsLayout());
466     ASSERT(!renderView->document().needsStyleRecalc());
467     bool hit = renderView->hitTest(request, location, result);
468     if (!hit || result.innerNode() != &pluginRenderer.frameOwnerElement())
469         return true;
470
471     location = LayoutPoint(x, y);
472     hit = renderView->hitTest(request, location, result);
473     if (!hit || result.innerNode() != &pluginRenderer.frameOwnerElement())
474         return true;
475
476     location = LayoutPoint(x + width, y);
477     hit = renderView->hitTest(request, location, result);
478     if (!hit || result.innerNode() != &pluginRenderer.frameOwnerElement())
479         return true;
480
481     location = LayoutPoint(x + width, y + height);
482     hit = renderView->hitTest(request, location, result);
483     if (!hit || result.innerNode() != &pluginRenderer.frameOwnerElement())
484         return true;
485
486     location = LayoutPoint(x, y + height);
487     hit = renderView->hitTest(request, location, result);
488     if (!hit || result.innerNode() != &pluginRenderer.frameOwnerElement())
489         return true;
490     return false;
491 }
492
493 }