1 | /* |
2 | * Copyright (C) 2008-2017 Apple Inc. All rights reserved. |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Library General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2 of the License, or (at your option) any later version. |
8 | * |
9 | * This library is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 | * Library General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Library General Public License |
15 | * along with this library; see the file COPYING.LIB. If not, write to |
16 | * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
17 | * Boston, MA 02110-1301, USA. |
18 | * |
19 | */ |
20 | |
21 | #include "config.h" |
22 | #include "HTMLPlugInImageElement.h" |
23 | |
24 | #include "Chrome.h" |
25 | #include "ChromeClient.h" |
26 | #include "CommonVM.h" |
27 | #include "ContentSecurityPolicy.h" |
28 | #include "EventNames.h" |
29 | #include "Frame.h" |
30 | #include "FrameLoaderClient.h" |
31 | #include "HTMLImageLoader.h" |
32 | #include "JSDOMConvertBoolean.h" |
33 | #include "JSDOMConvertInterface.h" |
34 | #include "JSDOMConvertStrings.h" |
35 | #include "JSShadowRoot.h" |
36 | #include "LocalizedStrings.h" |
37 | #include "Logging.h" |
38 | #include "MouseEvent.h" |
39 | #include "Page.h" |
40 | #include "PlatformMouseEvent.h" |
41 | #include "PlugInClient.h" |
42 | #include "PluginViewBase.h" |
43 | #include "RenderImage.h" |
44 | #include "RenderSnapshottedPlugIn.h" |
45 | #include "RenderTreeUpdater.h" |
46 | #include "SchemeRegistry.h" |
47 | #include "ScriptController.h" |
48 | #include "SecurityOrigin.h" |
49 | #include "Settings.h" |
50 | #include "ShadowRoot.h" |
51 | #include "StyleTreeResolver.h" |
52 | #include "SubframeLoader.h" |
53 | #include "TypedElementDescendantIterator.h" |
54 | #include "UserGestureIndicator.h" |
55 | #include <JavaScriptCore/CatchScope.h> |
56 | #include <wtf/IsoMallocInlines.h> |
57 | |
58 | namespace WebCore { |
59 | |
60 | WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLPlugInImageElement); |
61 | |
62 | static const int sizingTinyDimensionThreshold = 40; |
63 | static const float sizingFullPageAreaRatioThreshold = 0.96; |
64 | static const Seconds autostartSoonAfterUserGestureThreshold = 5_s; |
65 | |
66 | // This delay should not exceed the snapshot delay in PluginView.cpp |
67 | static const Seconds simulatedMouseClickTimerDelay { 750_ms }; |
68 | |
69 | #if PLATFORM(COCOA) |
70 | static const Seconds removeSnapshotTimerDelay { 1500_ms }; |
71 | #endif |
72 | |
73 | static const String titleText(Page& page, const String& mimeType) |
74 | { |
75 | if (mimeType.isEmpty()) |
76 | return snapshottedPlugInLabelTitle(); |
77 | |
78 | // FIXME: It's not consistent to get a string from the page's chrome client, but then cache it globally. |
79 | // If it's global, it should come from elsewhere. If it's per-page then it should be cached per page. |
80 | static NeverDestroyed<HashMap<String, String>> mimeTypeToLabelTitleMap; |
81 | return mimeTypeToLabelTitleMap.get().ensure(mimeType, [&] { |
82 | auto title = page.chrome().client().plugInStartLabelTitle(mimeType); |
83 | if (!title.isEmpty()) |
84 | return title; |
85 | return snapshottedPlugInLabelTitle(); |
86 | }).iterator->value; |
87 | }; |
88 | |
89 | static const String subtitleText(Page& page, const String& mimeType) |
90 | { |
91 | if (mimeType.isEmpty()) |
92 | return snapshottedPlugInLabelSubtitle(); |
93 | |
94 | // FIXME: It's not consistent to get a string from the page's chrome client, but then cache it globally. |
95 | // If it's global, it should come from elsewhere. If it's per-page then it should be cached per page. |
96 | static NeverDestroyed<HashMap<String, String>> mimeTypeToLabelSubtitleMap; |
97 | return mimeTypeToLabelSubtitleMap.get().ensure(mimeType, [&] { |
98 | auto subtitle = page.chrome().client().plugInStartLabelSubtitle(mimeType); |
99 | if (!subtitle.isEmpty()) |
100 | return subtitle; |
101 | return snapshottedPlugInLabelSubtitle(); |
102 | }).iterator->value; |
103 | }; |
104 | |
105 | HTMLPlugInImageElement::HTMLPlugInImageElement(const QualifiedName& tagName, Document& document) |
106 | : HTMLPlugInElement(tagName, document) |
107 | , m_simulatedMouseClickTimer(*this, &HTMLPlugInImageElement::simulatedMouseClickTimerFired, simulatedMouseClickTimerDelay) |
108 | , m_removeSnapshotTimer(*this, &HTMLPlugInImageElement::removeSnapshotTimerFired) |
109 | , m_createdDuringUserGesture(UserGestureIndicator::processingUserGesture()) |
110 | { |
111 | setHasCustomStyleResolveCallbacks(); |
112 | } |
113 | |
114 | void HTMLPlugInImageElement::finishCreating() |
115 | { |
116 | scheduleUpdateForAfterStyleResolution(); |
117 | } |
118 | |
119 | HTMLPlugInImageElement::~HTMLPlugInImageElement() |
120 | { |
121 | if (m_needsDocumentActivationCallbacks) |
122 | document().unregisterForDocumentSuspensionCallbacks(*this); |
123 | } |
124 | |
125 | void HTMLPlugInImageElement::setDisplayState(DisplayState state) |
126 | { |
127 | #if PLATFORM(COCOA) |
128 | if (state == RestartingWithPendingMouseClick || state == Restarting) { |
129 | m_isRestartedPlugin = true; |
130 | m_snapshotDecision = NeverSnapshot; |
131 | invalidateStyleAndLayerComposition(); |
132 | if (displayState() == DisplayingSnapshot) |
133 | m_removeSnapshotTimer.startOneShot(removeSnapshotTimerDelay); |
134 | } |
135 | #endif |
136 | |
137 | HTMLPlugInElement::setDisplayState(state); |
138 | } |
139 | |
140 | RenderEmbeddedObject* HTMLPlugInImageElement::renderEmbeddedObject() const |
141 | { |
142 | // HTMLObjectElement and HTMLEmbedElement may return arbitrary renderers when using fallback content. |
143 | return is<RenderEmbeddedObject>(renderer()) ? downcast<RenderEmbeddedObject>(renderer()) : nullptr; |
144 | } |
145 | |
146 | bool HTMLPlugInImageElement::isImageType() |
147 | { |
148 | if (m_serviceType.isEmpty() && protocolIs(m_url, "data" )) |
149 | m_serviceType = mimeTypeFromDataURL(m_url); |
150 | |
151 | if (auto frame = makeRefPtr(document().frame())) |
152 | return frame->loader().client().objectContentType(document().completeURL(m_url), m_serviceType) == ObjectContentType::Image; |
153 | |
154 | return Image::supportsType(m_serviceType); |
155 | } |
156 | |
157 | // We don't use m_url, as it may not be the final URL that the object loads, depending on <param> values. |
158 | bool HTMLPlugInImageElement::allowedToLoadFrameURL(const String& url) |
159 | { |
160 | URL completeURL = document().completeURL(url); |
161 | if (contentFrame() && WTF::protocolIsJavaScript(completeURL) && !document().securityOrigin().canAccess(contentDocument()->securityOrigin())) |
162 | return false; |
163 | return document().frame()->isURLAllowed(completeURL); |
164 | } |
165 | |
166 | // We don't use m_url, or m_serviceType as they may not be the final values |
167 | // that <object> uses depending on <param> values. |
168 | bool HTMLPlugInImageElement::wouldLoadAsPlugIn(const String& url, const String& serviceType) |
169 | { |
170 | ASSERT(document().frame()); |
171 | URL completedURL; |
172 | if (!url.isEmpty()) |
173 | completedURL = document().completeURL(url); |
174 | return document().frame()->loader().client().objectContentType(completedURL, serviceType) == ObjectContentType::PlugIn; |
175 | } |
176 | |
177 | RenderPtr<RenderElement> HTMLPlugInImageElement::createElementRenderer(RenderStyle&& style, const RenderTreePosition& insertionPosition) |
178 | { |
179 | ASSERT(document().pageCacheState() == Document::NotInPageCache); |
180 | |
181 | if (displayState() >= PreparingPluginReplacement) |
182 | return HTMLPlugInElement::createElementRenderer(WTFMove(style), insertionPosition); |
183 | |
184 | // Once a plug-in element creates its renderer, it needs to be told when the document goes |
185 | // inactive or reactivates so it can clear the renderer before going into the page cache. |
186 | if (!m_needsDocumentActivationCallbacks) { |
187 | m_needsDocumentActivationCallbacks = true; |
188 | document().registerForDocumentSuspensionCallbacks(*this); |
189 | } |
190 | |
191 | if (displayState() == DisplayingSnapshot) { |
192 | auto renderSnapshottedPlugIn = createRenderer<RenderSnapshottedPlugIn>(*this, WTFMove(style)); |
193 | renderSnapshottedPlugIn->updateSnapshot(m_snapshotImage.get()); |
194 | return renderSnapshottedPlugIn; |
195 | } |
196 | |
197 | if (useFallbackContent()) |
198 | return RenderElement::createFor(*this, WTFMove(style)); |
199 | |
200 | if (isImageType()) |
201 | return createRenderer<RenderImage>(*this, WTFMove(style)); |
202 | |
203 | return HTMLPlugInElement::createElementRenderer(WTFMove(style), insertionPosition); |
204 | } |
205 | |
206 | bool HTMLPlugInImageElement::childShouldCreateRenderer(const Node& child) const |
207 | { |
208 | if (is<RenderSnapshottedPlugIn>(renderer()) && !hasShadowRootParent(child)) |
209 | return false; |
210 | |
211 | return HTMLPlugInElement::childShouldCreateRenderer(child); |
212 | } |
213 | |
214 | void HTMLPlugInImageElement::willRecalcStyle(Style::Change change) |
215 | { |
216 | // Make sure style recalcs scheduled by a child shadow tree don't trigger reconstruction and cause flicker. |
217 | if (change == Style::NoChange && styleValidity() == Style::Validity::Valid) |
218 | return; |
219 | |
220 | // FIXME: There shoudn't be need to force render tree reconstruction here. |
221 | // It is only done because loading and load event dispatching is tied to render tree construction. |
222 | if (!useFallbackContent() && needsWidgetUpdate() && renderer() && !isImageType() && displayState() != DisplayingSnapshot) |
223 | invalidateStyleAndRenderersForSubtree(); |
224 | } |
225 | |
226 | void HTMLPlugInImageElement::didRecalcStyle(Style::Change styleChange) |
227 | { |
228 | scheduleUpdateForAfterStyleResolution(); |
229 | |
230 | HTMLPlugInElement::didRecalcStyle(styleChange); |
231 | } |
232 | |
233 | void HTMLPlugInImageElement::didAttachRenderers() |
234 | { |
235 | m_needsWidgetUpdate = true; |
236 | scheduleUpdateForAfterStyleResolution(); |
237 | |
238 | // Update the RenderImageResource of the associated RenderImage. |
239 | if (m_imageLoader && is<RenderImage>(renderer())) { |
240 | auto& renderImageResource = downcast<RenderImage>(*renderer()).imageResource(); |
241 | if (!renderImageResource.cachedImage()) |
242 | renderImageResource.setCachedImage(m_imageLoader->image()); |
243 | } |
244 | |
245 | HTMLPlugInElement::didAttachRenderers(); |
246 | } |
247 | |
248 | void HTMLPlugInImageElement::willDetachRenderers() |
249 | { |
250 | auto widget = makeRefPtr(pluginWidget(PluginLoadingPolicy::DoNotLoad)); |
251 | if (is<PluginViewBase>(widget)) |
252 | downcast<PluginViewBase>(*widget).willDetachRenderer(); |
253 | |
254 | HTMLPlugInElement::willDetachRenderers(); |
255 | } |
256 | |
257 | void HTMLPlugInImageElement::scheduleUpdateForAfterStyleResolution() |
258 | { |
259 | if (m_hasUpdateScheduledForAfterStyleResolution) |
260 | return; |
261 | |
262 | document().incrementLoadEventDelayCount(); |
263 | |
264 | m_hasUpdateScheduledForAfterStyleResolution = true; |
265 | |
266 | Style::queuePostResolutionCallback([protectedThis = makeRef(*this)] { |
267 | protectedThis->updateAfterStyleResolution(); |
268 | }); |
269 | } |
270 | |
271 | void HTMLPlugInImageElement::updateAfterStyleResolution() |
272 | { |
273 | m_hasUpdateScheduledForAfterStyleResolution = false; |
274 | |
275 | // Do this after style resolution, since the image or widget load might complete synchronously |
276 | // and cause us to re-enter otherwise. Also, we can't really answer the question "do I have a renderer" |
277 | // accurately until after style resolution. |
278 | |
279 | if (renderer() && !useFallbackContent()) { |
280 | if (isImageType()) { |
281 | if (!m_imageLoader) |
282 | m_imageLoader = std::make_unique<HTMLImageLoader>(*this); |
283 | if (m_needsImageReload) |
284 | m_imageLoader->updateFromElementIgnoringPreviousError(); |
285 | else |
286 | m_imageLoader->updateFromElement(); |
287 | } else { |
288 | if (needsWidgetUpdate() && renderEmbeddedObject() && !renderEmbeddedObject()->isPluginUnavailable()) |
289 | updateWidget(CreatePlugins::No); |
290 | } |
291 | } |
292 | |
293 | // Either we reloaded the image just now, or we had some reason not to. |
294 | // Either way, clear the flag now, since we don't need to remember to try again. |
295 | m_needsImageReload = false; |
296 | |
297 | document().decrementLoadEventDelayCount(); |
298 | } |
299 | |
300 | void HTMLPlugInImageElement::didMoveToNewDocument(Document& oldDocument, Document& newDocument) |
301 | { |
302 | ASSERT_WITH_SECURITY_IMPLICATION(&document() == &newDocument); |
303 | if (m_needsDocumentActivationCallbacks) { |
304 | oldDocument.unregisterForDocumentSuspensionCallbacks(*this); |
305 | newDocument.registerForDocumentSuspensionCallbacks(*this); |
306 | } |
307 | |
308 | if (m_imageLoader) |
309 | m_imageLoader->elementDidMoveToNewDocument(); |
310 | |
311 | if (m_hasUpdateScheduledForAfterStyleResolution) { |
312 | oldDocument.decrementLoadEventDelayCount(); |
313 | newDocument.incrementLoadEventDelayCount(); |
314 | } |
315 | |
316 | HTMLPlugInElement::didMoveToNewDocument(oldDocument, newDocument); |
317 | } |
318 | |
319 | void HTMLPlugInImageElement::prepareForDocumentSuspension() |
320 | { |
321 | if (renderer()) |
322 | RenderTreeUpdater::tearDownRenderers(*this); |
323 | |
324 | HTMLPlugInElement::prepareForDocumentSuspension(); |
325 | } |
326 | |
327 | void HTMLPlugInImageElement::resumeFromDocumentSuspension() |
328 | { |
329 | scheduleUpdateForAfterStyleResolution(); |
330 | invalidateStyleAndRenderersForSubtree(); |
331 | |
332 | HTMLPlugInElement::resumeFromDocumentSuspension(); |
333 | } |
334 | |
335 | void HTMLPlugInImageElement::updateSnapshot(Image* image) |
336 | { |
337 | if (displayState() > DisplayingSnapshot) |
338 | return; |
339 | |
340 | m_snapshotImage = image; |
341 | |
342 | auto* renderer = this->renderer(); |
343 | if (!renderer) |
344 | return; |
345 | |
346 | if (is<RenderSnapshottedPlugIn>(*renderer)) { |
347 | downcast<RenderSnapshottedPlugIn>(*renderer).updateSnapshot(image); |
348 | return; |
349 | } |
350 | |
351 | if (is<RenderEmbeddedObject>(*renderer)) |
352 | renderer->repaint(); |
353 | } |
354 | |
355 | static DOMWrapperWorld& plugInImageElementIsolatedWorld() |
356 | { |
357 | static auto& isolatedWorld = DOMWrapperWorld::create(commonVM()).leakRef(); |
358 | return isolatedWorld; |
359 | } |
360 | |
361 | void HTMLPlugInImageElement::didAddUserAgentShadowRoot(ShadowRoot& root) |
362 | { |
363 | HTMLPlugInElement::didAddUserAgentShadowRoot(root); |
364 | if (displayState() >= PreparingPluginReplacement) |
365 | return; |
366 | |
367 | auto* page = document().page(); |
368 | if (!page) |
369 | return; |
370 | |
371 | // Reset any author styles that may apply as we only want explicit |
372 | // styles defined in the injected user agents stylesheets to specify |
373 | // the look-and-feel of the snapshotted plug-in overlay. |
374 | root.setResetStyleInheritance(true); |
375 | |
376 | String mimeType = serviceType(); |
377 | |
378 | auto& isolatedWorld = plugInImageElementIsolatedWorld(); |
379 | document().ensurePlugInsInjectedScript(isolatedWorld); |
380 | |
381 | auto& scriptController = document().frame()->script(); |
382 | auto& globalObject = *JSC::jsCast<JSDOMGlobalObject*>(scriptController.globalObject(isolatedWorld)); |
383 | |
384 | auto& vm = globalObject.vm(); |
385 | JSC::JSLockHolder lock(vm); |
386 | auto scope = DECLARE_CATCH_SCOPE(vm); |
387 | auto& state = *globalObject.globalExec(); |
388 | |
389 | JSC::MarkedArgumentBuffer argList; |
390 | argList.append(toJS<IDLInterface<ShadowRoot>>(state, globalObject, root)); |
391 | argList.append(toJS<IDLDOMString>(state, titleText(*page, mimeType))); |
392 | argList.append(toJS<IDLDOMString>(state, subtitleText(*page, mimeType))); |
393 | |
394 | // This parameter determines whether or not the snapshot overlay should always be visible over the plugin snapshot. |
395 | // If no snapshot was found then we want the overlay to be visible. |
396 | argList.append(toJS<IDLBoolean>(!m_snapshotImage)); |
397 | ASSERT(!argList.hasOverflowed()); |
398 | |
399 | // It is expected the JS file provides a createOverlay(shadowRoot, title, subtitle) function. |
400 | auto* overlay = globalObject.get(&state, JSC::Identifier::fromString(&state, "createOverlay" )).toObject(&state); |
401 | ASSERT(!overlay == !!scope.exception()); |
402 | if (!overlay) { |
403 | scope.clearException(); |
404 | return; |
405 | } |
406 | JSC::CallData callData; |
407 | auto callType = overlay->methodTable(vm)->getCallData(overlay, callData); |
408 | if (callType == JSC::CallType::None) |
409 | return; |
410 | |
411 | call(&state, overlay, callType, callData, &globalObject, argList); |
412 | scope.clearException(); |
413 | } |
414 | |
415 | bool HTMLPlugInImageElement::partOfSnapshotOverlay(const EventTarget* target) const |
416 | { |
417 | static NeverDestroyed<AtomicString> selector(".snapshot-overlay" , AtomicString::ConstructFromLiteral); |
418 | auto shadow = userAgentShadowRoot(); |
419 | if (!shadow) |
420 | return false; |
421 | if (!is<Node>(target)) |
422 | return false; |
423 | auto queryResult = shadow->querySelector(selector.get()); |
424 | if (queryResult.hasException()) |
425 | return false; |
426 | auto snapshotLabel = makeRefPtr(queryResult.releaseReturnValue()); |
427 | return snapshotLabel && snapshotLabel->contains(downcast<Node>(target)); |
428 | } |
429 | |
430 | void HTMLPlugInImageElement::removeSnapshotTimerFired() |
431 | { |
432 | m_snapshotImage = nullptr; |
433 | m_isRestartedPlugin = false; |
434 | invalidateStyleAndLayerComposition(); |
435 | if (renderer()) |
436 | renderer()->repaint(); |
437 | } |
438 | |
439 | void HTMLPlugInImageElement::restartSimilarPlugIns() |
440 | { |
441 | // Restart any other snapshotted plugins in the page with the same origin. Note that they |
442 | // may be in different frames, so traverse from the top of the document. |
443 | |
444 | auto plugInOrigin = m_loadedUrl.host(); |
445 | String mimeType = serviceType(); |
446 | Vector<Ref<HTMLPlugInImageElement>> similarPlugins; |
447 | |
448 | if (!document().page()) |
449 | return; |
450 | |
451 | for (RefPtr<Frame> frame = &document().page()->mainFrame(); frame; frame = frame->tree().traverseNext()) { |
452 | if (!frame->loader().subframeLoader().containsPlugins()) |
453 | continue; |
454 | |
455 | if (!frame->document()) |
456 | continue; |
457 | |
458 | for (auto& element : descendantsOfType<HTMLPlugInImageElement>(*frame->document())) { |
459 | if (plugInOrigin == element.loadedUrl().host() && mimeType == element.serviceType()) |
460 | similarPlugins.append(element); |
461 | } |
462 | } |
463 | |
464 | for (auto& plugInToRestart : similarPlugins) { |
465 | if (plugInToRestart->displayState() <= HTMLPlugInElement::DisplayingSnapshot) { |
466 | LOG(Plugins, "%p Plug-in looks similar to a restarted plug-in. Restart." , plugInToRestart.ptr()); |
467 | plugInToRestart->restartSnapshottedPlugIn(); |
468 | } |
469 | plugInToRestart->m_snapshotDecision = NeverSnapshot; |
470 | } |
471 | } |
472 | |
473 | void HTMLPlugInImageElement::userDidClickSnapshot(MouseEvent& event, bool forwardEvent) |
474 | { |
475 | if (forwardEvent) |
476 | m_pendingClickEventFromSnapshot = &event; |
477 | |
478 | auto plugInOrigin = m_loadedUrl.host(); |
479 | if (document().page() && !SchemeRegistry::shouldTreatURLSchemeAsLocal(document().page()->mainFrame().document()->baseURL().protocol().toStringWithoutCopying()) && document().page()->settings().autostartOriginPlugInSnapshottingEnabled()) |
480 | document().page()->plugInClient()->didStartFromOrigin(document().page()->mainFrame().document()->baseURL().host().toString(), plugInOrigin.toString(), serviceType(), document().page()->sessionID()); |
481 | |
482 | LOG(Plugins, "%p User clicked on snapshotted plug-in. Restart." , this); |
483 | restartSnapshottedPlugIn(); |
484 | if (forwardEvent) |
485 | setDisplayState(RestartingWithPendingMouseClick); |
486 | restartSimilarPlugIns(); |
487 | } |
488 | |
489 | void HTMLPlugInImageElement::setIsPrimarySnapshottedPlugIn(bool isPrimarySnapshottedPlugIn) |
490 | { |
491 | if (!document().page() || !document().page()->settings().primaryPlugInSnapshotDetectionEnabled() || document().page()->settings().snapshotAllPlugIns()) |
492 | return; |
493 | |
494 | if (isPrimarySnapshottedPlugIn) { |
495 | if (m_plugInWasCreated) { |
496 | LOG(Plugins, "%p Plug-in was detected as the primary element in the page. Restart." , this); |
497 | restartSnapshottedPlugIn(); |
498 | restartSimilarPlugIns(); |
499 | } else { |
500 | LOG(Plugins, "%p Plug-in was detected as the primary element in the page, but is not yet created. Will restart later." , this); |
501 | m_deferredPromotionToPrimaryPlugIn = true; |
502 | } |
503 | } |
504 | } |
505 | |
506 | void HTMLPlugInImageElement::restartSnapshottedPlugIn() |
507 | { |
508 | if (displayState() >= RestartingWithPendingMouseClick) |
509 | return; |
510 | |
511 | setDisplayState(Restarting); |
512 | invalidateStyleAndRenderersForSubtree(); |
513 | } |
514 | |
515 | void HTMLPlugInImageElement::dispatchPendingMouseClick() |
516 | { |
517 | ASSERT(!m_simulatedMouseClickTimer.isActive()); |
518 | m_simulatedMouseClickTimer.restart(); |
519 | } |
520 | |
521 | void HTMLPlugInImageElement::simulatedMouseClickTimerFired() |
522 | { |
523 | ASSERT(displayState() == RestartingWithPendingMouseClick); |
524 | ASSERT(m_pendingClickEventFromSnapshot); |
525 | |
526 | setDisplayState(Playing); |
527 | dispatchSimulatedClick(m_pendingClickEventFromSnapshot.get(), SendMouseOverUpDownEvents, DoNotShowPressedLook); |
528 | |
529 | m_pendingClickEventFromSnapshot = nullptr; |
530 | } |
531 | |
532 | static bool documentHadRecentUserGesture(Document& document) |
533 | { |
534 | MonotonicTime lastKnownUserGestureTimestamp = document.lastHandledUserGestureTimestamp(); |
535 | if (document.frame() != &document.page()->mainFrame() && document.page()->mainFrame().document()) |
536 | lastKnownUserGestureTimestamp = std::max(lastKnownUserGestureTimestamp, document.page()->mainFrame().document()->lastHandledUserGestureTimestamp()); |
537 | |
538 | return MonotonicTime::now() - lastKnownUserGestureTimestamp < autostartSoonAfterUserGestureThreshold; |
539 | } |
540 | |
541 | void HTMLPlugInImageElement::checkSizeChangeForSnapshotting() |
542 | { |
543 | if (!m_needsCheckForSizeChange || m_snapshotDecision != MaySnapshotWhenResized || documentHadRecentUserGesture(document())) |
544 | return; |
545 | |
546 | m_needsCheckForSizeChange = false; |
547 | |
548 | auto contentBoxRect = downcast<RenderBox>(*renderer()).contentBoxRect(); |
549 | int contentWidth = contentBoxRect.width(); |
550 | int contentHeight = contentBoxRect.height(); |
551 | |
552 | if (contentWidth <= sizingTinyDimensionThreshold || contentHeight <= sizingTinyDimensionThreshold) |
553 | return; |
554 | |
555 | LOG(Plugins, "%p Plug-in originally avoided snapshotting because it was sized %dx%d. Now it is %dx%d. Tell it to snapshot.\n" , this, m_sizeWhenSnapshotted.width(), m_sizeWhenSnapshotted.height(), contentWidth, contentHeight); |
556 | setDisplayState(WaitingForSnapshot); |
557 | m_snapshotDecision = Snapshotted; |
558 | auto widget = makeRefPtr(pluginWidget()); |
559 | if (is<PluginViewBase>(widget)) |
560 | downcast<PluginViewBase>(*widget).beginSnapshottingRunningPlugin(); |
561 | } |
562 | |
563 | static inline bool is100Percent(Length length) |
564 | { |
565 | return length.isPercent() && length.percent() == 100; |
566 | } |
567 | |
568 | static inline bool isSmallerThanTinySizingThreshold(const RenderEmbeddedObject& renderer) |
569 | { |
570 | auto contentRect = renderer.contentBoxRect(); |
571 | return contentRect.width() <= sizingTinyDimensionThreshold || contentRect.height() <= sizingTinyDimensionThreshold; |
572 | } |
573 | |
574 | bool HTMLPlugInImageElement::isTopLevelFullPagePlugin(const RenderEmbeddedObject& renderer) const |
575 | { |
576 | ASSERT(document().frame()); |
577 | auto& frame = *document().frame(); |
578 | if (!frame.isMainFrame()) |
579 | return false; |
580 | |
581 | auto& style = renderer.style(); |
582 | auto visibleSize = frame.view()->visibleSize(); |
583 | auto contentRect = renderer.contentBoxRect(); |
584 | float contentWidth = contentRect.width(); |
585 | float contentHeight = contentRect.height(); |
586 | return is100Percent(style.width()) && is100Percent(style.height()) && contentWidth * contentHeight > visibleSize.area().unsafeGet() * sizingFullPageAreaRatioThreshold; |
587 | } |
588 | |
589 | void HTMLPlugInImageElement::checkSnapshotStatus() |
590 | { |
591 | if (!is<RenderSnapshottedPlugIn>(*renderer())) { |
592 | if (displayState() == Playing) |
593 | checkSizeChangeForSnapshotting(); |
594 | return; |
595 | } |
596 | |
597 | // If width and height styles were previously not set and we've snapshotted the plugin we may need to restart the plugin so that its state can be updated appropriately. |
598 | if (!document().page()->settings().snapshotAllPlugIns() && displayState() <= DisplayingSnapshot && !m_plugInDimensionsSpecified) { |
599 | auto& renderer = downcast<RenderSnapshottedPlugIn>(*this->renderer()); |
600 | if (!renderer.style().logicalWidth().isSpecified() && !renderer.style().logicalHeight().isSpecified()) |
601 | return; |
602 | |
603 | m_plugInDimensionsSpecified = true; |
604 | if (isTopLevelFullPagePlugin(renderer)) { |
605 | m_snapshotDecision = NeverSnapshot; |
606 | restartSnapshottedPlugIn(); |
607 | } else if (isSmallerThanTinySizingThreshold(renderer)) { |
608 | m_snapshotDecision = MaySnapshotWhenResized; |
609 | restartSnapshottedPlugIn(); |
610 | } |
611 | return; |
612 | } |
613 | |
614 | // Notify the shadow root that the size changed so that we may update the overlay layout. |
615 | ensureUserAgentShadowRoot().dispatchEvent(Event::create(eventNames().resizeEvent, Event::CanBubble::Yes, Event::IsCancelable::No)); |
616 | } |
617 | |
618 | void HTMLPlugInImageElement::subframeLoaderWillCreatePlugIn(const URL& url) |
619 | { |
620 | LOG(Plugins, "%p Plug-in URL: %s" , this, m_url.utf8().data()); |
621 | LOG(Plugins, " Actual URL: %s" , url.string().utf8().data()); |
622 | LOG(Plugins, " MIME type: %s" , serviceType().utf8().data()); |
623 | |
624 | m_loadedUrl = url; |
625 | m_plugInWasCreated = false; |
626 | m_deferredPromotionToPrimaryPlugIn = false; |
627 | |
628 | if (!document().page() || !document().page()->settings().plugInSnapshottingEnabled()) { |
629 | m_snapshotDecision = NeverSnapshot; |
630 | return; |
631 | } |
632 | |
633 | if (displayState() == Restarting) { |
634 | LOG(Plugins, "%p Plug-in is explicitly restarting" , this); |
635 | m_snapshotDecision = NeverSnapshot; |
636 | setDisplayState(Playing); |
637 | return; |
638 | } |
639 | |
640 | if (displayState() == RestartingWithPendingMouseClick) { |
641 | LOG(Plugins, "%p Plug-in is explicitly restarting but also waiting for a click" , this); |
642 | m_snapshotDecision = NeverSnapshot; |
643 | return; |
644 | } |
645 | |
646 | if (m_snapshotDecision == NeverSnapshot) { |
647 | LOG(Plugins, "%p Plug-in is blessed, allow it to start" , this); |
648 | return; |
649 | } |
650 | |
651 | bool inMainFrame = document().frame()->isMainFrame(); |
652 | |
653 | if (document().isPluginDocument() && inMainFrame) { |
654 | LOG(Plugins, "%p Plug-in document in main frame" , this); |
655 | m_snapshotDecision = NeverSnapshot; |
656 | return; |
657 | } |
658 | |
659 | if (UserGestureIndicator::processingUserGesture()) { |
660 | LOG(Plugins, "%p Script is currently processing user gesture, set to play" , this); |
661 | m_snapshotDecision = NeverSnapshot; |
662 | return; |
663 | } |
664 | |
665 | if (m_createdDuringUserGesture) { |
666 | LOG(Plugins, "%p Plug-in was created when processing user gesture, set to play" , this); |
667 | m_snapshotDecision = NeverSnapshot; |
668 | return; |
669 | } |
670 | |
671 | if (documentHadRecentUserGesture(document())) { |
672 | LOG(Plugins, "%p Plug-in was created shortly after a user gesture, set to play" , this); |
673 | m_snapshotDecision = NeverSnapshot; |
674 | return; |
675 | } |
676 | |
677 | if (document().page()->settings().snapshotAllPlugIns()) { |
678 | LOG(Plugins, "%p Plug-in forced to snapshot by user preference" , this); |
679 | m_snapshotDecision = Snapshotted; |
680 | setDisplayState(WaitingForSnapshot); |
681 | return; |
682 | } |
683 | |
684 | if (document().page()->settings().autostartOriginPlugInSnapshottingEnabled() && document().page()->plugInClient() && document().page()->plugInClient()->shouldAutoStartFromOrigin(document().page()->mainFrame().document()->baseURL().host().toString(), url.host().toString(), serviceType())) { |
685 | LOG(Plugins, "%p Plug-in from (%s, %s) is marked to auto-start, set to play" , this, document().page()->mainFrame().document()->baseURL().host().utf8().data(), url.host().utf8().data()); |
686 | m_snapshotDecision = NeverSnapshot; |
687 | return; |
688 | } |
689 | |
690 | if (m_loadedUrl.isEmpty() && !serviceType().isEmpty()) { |
691 | LOG(Plugins, "%p Plug-in has no src URL but does have a valid mime type %s, set to play" , this, serviceType().utf8().data()); |
692 | m_snapshotDecision = MaySnapshotWhenContentIsSet; |
693 | return; |
694 | } |
695 | |
696 | if (!SchemeRegistry::shouldTreatURLSchemeAsLocal(m_loadedUrl.protocol().toStringWithoutCopying()) && !m_loadedUrl.host().isEmpty() && m_loadedUrl.host() == document().page()->mainFrame().document()->baseURL().host()) { |
697 | LOG(Plugins, "%p Plug-in is served from page's domain, set to play" , this); |
698 | m_snapshotDecision = NeverSnapshot; |
699 | return; |
700 | } |
701 | |
702 | auto& renderer = downcast<RenderEmbeddedObject>(*this->renderer()); |
703 | auto contentRect = renderer.contentBoxRect(); |
704 | int contentWidth = contentRect.width(); |
705 | int contentHeight = contentRect.height(); |
706 | |
707 | m_plugInDimensionsSpecified = renderer.style().logicalWidth().isSpecified() || renderer.style().logicalHeight().isSpecified(); |
708 | |
709 | if (isTopLevelFullPagePlugin(renderer)) { |
710 | LOG(Plugins, "%p Plug-in is top level full page, set to play" , this); |
711 | m_snapshotDecision = NeverSnapshot; |
712 | return; |
713 | } |
714 | |
715 | if (isSmallerThanTinySizingThreshold(renderer)) { |
716 | LOG(Plugins, "%p Plug-in is very small %dx%d, set to play" , this, contentWidth, contentHeight); |
717 | m_sizeWhenSnapshotted = IntSize(contentWidth, contentHeight); |
718 | m_snapshotDecision = MaySnapshotWhenResized; |
719 | return; |
720 | } |
721 | |
722 | if (!document().page()->plugInClient()) { |
723 | LOG(Plugins, "%p There is no plug-in client. Set to wait for snapshot" , this); |
724 | m_snapshotDecision = NeverSnapshot; |
725 | setDisplayState(WaitingForSnapshot); |
726 | return; |
727 | } |
728 | |
729 | LOG(Plugins, "%p Plug-in from (%s, %s) is not auto-start, sized at %dx%d, set to wait for snapshot" , this, document().topDocument().baseURL().host().utf8().data(), url.host().utf8().data(), contentWidth, contentHeight); |
730 | m_snapshotDecision = Snapshotted; |
731 | setDisplayState(WaitingForSnapshot); |
732 | } |
733 | |
734 | void HTMLPlugInImageElement::subframeLoaderDidCreatePlugIn(const Widget& widget) |
735 | { |
736 | m_plugInWasCreated = true; |
737 | |
738 | if (is<PluginViewBase>(widget) && downcast<PluginViewBase>(widget).shouldAlwaysAutoStart()) { |
739 | LOG(Plugins, "%p Plug-in should auto-start, set to play" , this); |
740 | m_snapshotDecision = NeverSnapshot; |
741 | setDisplayState(Playing); |
742 | return; |
743 | } |
744 | |
745 | if (m_deferredPromotionToPrimaryPlugIn) { |
746 | LOG(Plugins, "%p Plug-in was created, previously deferred promotion to primary. Will promote" , this); |
747 | setIsPrimarySnapshottedPlugIn(true); |
748 | m_deferredPromotionToPrimaryPlugIn = false; |
749 | } |
750 | } |
751 | |
752 | void HTMLPlugInImageElement::defaultEventHandler(Event& event) |
753 | { |
754 | if (is<RenderEmbeddedObject>(renderer()) && displayState() == WaitingForSnapshot && is<MouseEvent>(event) && event.type() == eventNames().clickEvent) { |
755 | auto& mouseEvent = downcast<MouseEvent>(event); |
756 | if (mouseEvent.button() == LeftButton) { |
757 | userDidClickSnapshot(mouseEvent, true); |
758 | mouseEvent.setDefaultHandled(); |
759 | return; |
760 | } |
761 | } |
762 | HTMLPlugInElement::defaultEventHandler(event); |
763 | } |
764 | |
765 | bool HTMLPlugInImageElement::allowedToLoadPluginContent(const String& url, const String& mimeType) const |
766 | { |
767 | // Elements in user agent show tree should load whatever the embedding document policy is. |
768 | if (isInUserAgentShadowTree()) |
769 | return true; |
770 | |
771 | URL completedURL; |
772 | if (!url.isEmpty()) |
773 | completedURL = document().completeURL(url); |
774 | |
775 | ASSERT(document().contentSecurityPolicy()); |
776 | const ContentSecurityPolicy& contentSecurityPolicy = *document().contentSecurityPolicy(); |
777 | |
778 | contentSecurityPolicy.upgradeInsecureRequestIfNeeded(completedURL, ContentSecurityPolicy::InsecureRequestType::Load); |
779 | |
780 | if (!contentSecurityPolicy.allowObjectFromSource(completedURL)) |
781 | return false; |
782 | |
783 | auto& declaredMimeType = document().isPluginDocument() && document().ownerElement() ? |
784 | document().ownerElement()->attributeWithoutSynchronization(HTMLNames::typeAttr) : attributeWithoutSynchronization(HTMLNames::typeAttr); |
785 | return contentSecurityPolicy.allowPluginType(mimeType, declaredMimeType, completedURL); |
786 | } |
787 | |
788 | bool HTMLPlugInImageElement::requestObject(const String& url, const String& mimeType, const Vector<String>& paramNames, const Vector<String>& paramValues) |
789 | { |
790 | ASSERT(document().frame()); |
791 | |
792 | if (url.isEmpty() && mimeType.isEmpty()) |
793 | return false; |
794 | |
795 | if (!allowedToLoadPluginContent(url, mimeType)) { |
796 | renderEmbeddedObject()->setPluginUnavailabilityReason(RenderEmbeddedObject::PluginBlockedByContentSecurityPolicy); |
797 | return false; |
798 | } |
799 | |
800 | if (HTMLPlugInElement::requestObject(url, mimeType, paramNames, paramValues)) |
801 | return true; |
802 | |
803 | return document().frame()->loader().subframeLoader().requestObject(*this, url, getNameAttribute(), mimeType, paramNames, paramValues); |
804 | } |
805 | |
806 | void HTMLPlugInImageElement::updateImageLoaderWithNewURLSoon() |
807 | { |
808 | if (m_needsImageReload) |
809 | return; |
810 | |
811 | m_needsImageReload = true; |
812 | scheduleUpdateForAfterStyleResolution(); |
813 | invalidateStyle(); |
814 | } |
815 | |
816 | } // namespace WebCore |
817 | |