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::isBelowSizeThreshold() const |
410 | { |
411 | auto* renderObject = renderer(); |
412 | if (!is<RenderEmbeddedObject>(renderObject)) |
413 | return true; |
414 | auto& renderEmbeddedObject = downcast<RenderEmbeddedObject>(*renderObject); |
415 | return renderEmbeddedObject.isPluginUnavailable() && renderEmbeddedObject.pluginUnavailabilityReason() == RenderEmbeddedObject::PluginTooSmall; |
416 | } |
417 | |
418 | bool HTMLPlugInElement::setReplacement(RenderEmbeddedObject::PluginUnavailabilityReason reason, const String& unavailabilityDescription) |
419 | { |
420 | if (!is<RenderEmbeddedObject>(renderer())) |
421 | return false; |
422 | |
423 | if (reason == RenderEmbeddedObject::UnsupportedPlugin) |
424 | document().addConsoleMessage(MessageSource::JS, MessageLevel::Warning, "Tried to use an unsupported plug-in."_s ); |
425 | |
426 | Ref<HTMLPlugInElement> protectedThis(*this); |
427 | downcast<RenderEmbeddedObject>(*renderer()).setPluginUnavailabilityReasonWithDescription(reason, unavailabilityDescription); |
428 | bool replacementIsObscured = isReplacementObscured(); |
429 | // hittest in isReplacementObscured() method could destroy the renderer. Let's refetch it. |
430 | if (is<RenderEmbeddedObject>(renderer())) |
431 | downcast<RenderEmbeddedObject>(*renderer()).setUnavailablePluginIndicatorIsHidden(replacementIsObscured); |
432 | return replacementIsObscured; |
433 | } |
434 | |
435 | bool HTMLPlugInElement::isReplacementObscured() |
436 | { |
437 | auto topDocument = makeRef(document().topDocument()); |
438 | auto topFrameView = makeRefPtr(topDocument->view()); |
439 | if (!topFrameView) |
440 | return false; |
441 | |
442 | topFrameView->updateLayoutAndStyleIfNeededRecursive(); |
443 | |
444 | // Updating the layout may have detached this document from the top document. |
445 | auto* renderView = topDocument->renderView(); |
446 | if (!renderView || !document().view() || &document().topDocument() != topDocument.ptr()) |
447 | return false; |
448 | |
449 | if (!renderer() || !is<RenderEmbeddedObject>(*renderer())) |
450 | return false; |
451 | auto& pluginRenderer = downcast<RenderEmbeddedObject>(*renderer()); |
452 | // Check the opacity of each layer containing the element or its ancestors. |
453 | float opacity = 1.0; |
454 | for (auto* layer = pluginRenderer.enclosingLayer(); layer; layer = layer->parent()) { |
455 | opacity *= layer->renderer().style().opacity(); |
456 | if (opacity < 0.1) |
457 | return true; |
458 | } |
459 | // Calculate the absolute rect for the blocked plugin replacement text. |
460 | LayoutPoint absoluteLocation(pluginRenderer.absoluteBoundingBoxRect().location()); |
461 | LayoutRect rect = pluginRenderer.unavailablePluginIndicatorBounds(absoluteLocation); |
462 | if (rect.isEmpty()) |
463 | return true; |
464 | auto viewRect = document().view()->convertToRootView(snappedIntRect(rect)); |
465 | auto x = viewRect.x(); |
466 | auto y = viewRect.y(); |
467 | auto width = viewRect.width(); |
468 | auto height = viewRect.height(); |
469 | // Hit test the center and near the corners of the replacement text to ensure |
470 | // it is visible and is not masked by other elements. |
471 | HitTestRequest request(HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::IgnoreClipping | HitTestRequest::DisallowUserAgentShadowContent | HitTestRequest::AllowChildFrameContent); |
472 | HitTestResult result; |
473 | HitTestLocation location = LayoutPoint(x + width / 2, y + height / 2); |
474 | ASSERT(!renderView->needsLayout()); |
475 | ASSERT(!renderView->document().needsStyleRecalc()); |
476 | bool hit = renderView->hitTest(request, location, result); |
477 | if (!hit || result.innerNode() != &pluginRenderer.frameOwnerElement()) |
478 | return true; |
479 | |
480 | location = LayoutPoint(x, y); |
481 | hit = renderView->hitTest(request, location, result); |
482 | if (!hit || result.innerNode() != &pluginRenderer.frameOwnerElement()) |
483 | return true; |
484 | |
485 | location = LayoutPoint(x + width, y); |
486 | hit = renderView->hitTest(request, location, result); |
487 | if (!hit || result.innerNode() != &pluginRenderer.frameOwnerElement()) |
488 | return true; |
489 | |
490 | location = LayoutPoint(x + width, y + height); |
491 | hit = renderView->hitTest(request, location, result); |
492 | if (!hit || result.innerNode() != &pluginRenderer.frameOwnerElement()) |
493 | return true; |
494 | |
495 | location = LayoutPoint(x, y + height); |
496 | hit = renderView->hitTest(request, location, result); |
497 | if (!hit || result.innerNode() != &pluginRenderer.frameOwnerElement()) |
498 | return true; |
499 | return false; |
500 | } |
501 | |
502 | } |
503 | |