2 * Copyright (C) 2008, 2011, 2012 Apple Inc. All rights reserved.
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.
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.
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.
22 #include "HTMLPlugInImageElement.h"
25 #include "ChromeClient.h"
27 #include "FrameLoader.h"
28 #include "FrameLoaderClient.h"
29 #include "FrameView.h"
30 #include "HTMLDivElement.h"
31 #include "HTMLImageLoader.h"
33 #include "LocalizedStrings.h"
35 #include "MouseEvent.h"
37 #include "NodeRenderStyle.h"
38 #include "NodeRenderingContext.h"
40 #include "PlugInClient.h"
41 #include "PluginViewBase.h"
42 #include "RenderEmbeddedObject.h"
43 #include "RenderImage.h"
44 #include "RenderSnapshottedPlugIn.h"
45 #include "SchemeRegistry.h"
46 #include "ScriptController.h"
47 #include "SecurityOrigin.h"
49 #include "ShadowRoot.h"
50 #include "StyleResolver.h"
52 #include <wtf/CurrentTime.h>
56 using namespace HTMLNames;
58 typedef Vector<RefPtr<HTMLPlugInImageElement> > HTMLPlugInImageElementList;
60 static const int sizingTinyDimensionThreshold = 40;
61 static const int sizingSmallWidthThreshold = 250;
62 static const int sizingMediumWidthThreshold = 450;
63 static const int sizingMediumHeightThreshold = 300;
64 static const float sizingFullPageAreaRatioThreshold = 0.96;
65 static const float autostartSoonAfterUserGestureThreshold = 5.0;
67 // This delay should not exceed the snapshot delay in PluginView.cpp
68 static const double simulatedMouseClickTimerDelay = .75;
69 static const double removeSnapshotTimerDelay = 1.5;
71 HTMLPlugInImageElement::HTMLPlugInImageElement(const QualifiedName& tagName, Document* document, bool createdByParser, PreferPlugInsForImagesOption preferPlugInsForImagesOption)
72 : HTMLPlugInElement(tagName, document)
73 // m_needsWidgetUpdate(!createdByParser) allows HTMLObjectElement to delay
74 // widget updates until after all children are parsed. For HTMLEmbedElement
75 // this delay is unnecessary, but it is simpler to make both classes share
76 // the same codepath in this class.
77 , m_needsWidgetUpdate(!createdByParser)
78 , m_shouldPreferPlugInsForImages(preferPlugInsForImagesOption == ShouldPreferPlugInsForImages)
79 , m_needsDocumentActivationCallbacks(false)
80 , m_simulatedMouseClickTimer(this, &HTMLPlugInImageElement::simulatedMouseClickTimerFired, simulatedMouseClickTimerDelay)
81 , m_swapRendererTimer(this, &HTMLPlugInImageElement::swapRendererTimerFired)
82 , m_removeSnapshotTimer(this, &HTMLPlugInImageElement::removeSnapshotTimerFired)
83 , m_createdDuringUserGesture(ScriptController::processingUserGesture())
84 , m_restartedPlugin(false)
86 setHasCustomStyleCallbacks();
89 HTMLPlugInImageElement::~HTMLPlugInImageElement()
91 if (m_needsDocumentActivationCallbacks)
92 document()->unregisterForPageCacheSuspensionCallbacks(this);
95 void HTMLPlugInImageElement::setDisplayState(DisplayState state)
98 if (state == RestartingWithPendingMouseClick || state == Restarting) {
99 m_restartedPlugin = true;
100 if (displayState() == DisplayingSnapshot)
101 m_removeSnapshotTimer.startOneShot(removeSnapshotTimerDelay);
105 HTMLPlugInElement::setDisplayState(state);
107 if (state == DisplayingSnapshot)
108 m_swapRendererTimer.startOneShot(0);
111 RenderEmbeddedObject* HTMLPlugInImageElement::renderEmbeddedObject() const
113 // HTMLObjectElement and HTMLEmbedElement may return arbitrary renderers
114 // when using fallback content.
115 if (!renderer() || !renderer()->isEmbeddedObject())
117 return toRenderEmbeddedObject(renderer());
120 bool HTMLPlugInImageElement::isImageType()
122 if (m_serviceType.isEmpty() && protocolIs(m_url, "data"))
123 m_serviceType = mimeTypeFromDataURL(m_url);
125 if (Frame* frame = document()->frame()) {
126 KURL completedURL = document()->completeURL(m_url);
127 return frame->loader()->client()->objectContentType(completedURL, m_serviceType, shouldPreferPlugInsForImages()) == ObjectContentImage;
130 return Image::supportsType(m_serviceType);
133 // We don't use m_url, as it may not be the final URL that the object loads,
134 // depending on <param> values.
135 bool HTMLPlugInImageElement::allowedToLoadFrameURL(const String& url)
137 KURL completeURL = document()->completeURL(url);
139 if (contentFrame() && protocolIsJavaScript(completeURL)
140 && !document()->securityOrigin()->canAccess(contentDocument()->securityOrigin()))
143 return document()->frame()->isURLAllowed(completeURL);
146 // We don't use m_url, or m_serviceType as they may not be the final values
147 // that <object> uses depending on <param> values.
148 bool HTMLPlugInImageElement::wouldLoadAsNetscapePlugin(const String& url, const String& serviceType)
151 ASSERT(document()->frame());
154 completedURL = document()->completeURL(url);
156 FrameLoader* frameLoader = document()->frame()->loader();
158 if (frameLoader->client()->objectContentType(completedURL, serviceType, shouldPreferPlugInsForImages()) == ObjectContentNetscapePlugin)
163 RenderObject* HTMLPlugInImageElement::createRenderer(RenderArena* arena, RenderStyle* style)
165 // Once a PlugIn Element creates its renderer, it needs to be told when the Document goes
166 // inactive or reactivates so it can clear the renderer before going into the page cache.
167 if (!m_needsDocumentActivationCallbacks) {
168 m_needsDocumentActivationCallbacks = true;
169 document()->registerForPageCacheSuspensionCallbacks(this);
172 if (displayState() == DisplayingSnapshot) {
173 RenderSnapshottedPlugIn* renderSnapshottedPlugIn = new (arena) RenderSnapshottedPlugIn(this);
174 renderSnapshottedPlugIn->updateSnapshot(m_snapshotImage);
175 return renderSnapshottedPlugIn;
178 // Fallback content breaks the DOM->Renderer class relationship of this
179 // class and all superclasses because createObject won't necessarily
180 // return a RenderEmbeddedObject, RenderPart or even RenderWidget.
181 if (useFallbackContent())
182 return RenderObject::createObject(this, style);
185 RenderImage* image = new (arena) RenderImage(this);
186 image->setImageResource(RenderImageResource::create());
190 return new (arena) RenderEmbeddedObject(this);
193 bool HTMLPlugInImageElement::willRecalcStyle(StyleChange)
195 // FIXME: Why is this necessary? Manual re-attach is almost always wrong.
196 if (!useFallbackContent() && needsWidgetUpdate() && renderer() && !isImageType() && (displayState() != DisplayingSnapshot))
201 void HTMLPlugInImageElement::attach()
203 PostAttachCallbackDisabler disabler(this);
205 bool isImage = isImageType();
208 queuePostAttachCallback(&HTMLPlugInImageElement::updateWidgetCallback, this);
210 HTMLPlugInElement::attach();
212 if (isImage && renderer() && !useFallbackContent()) {
214 m_imageLoader = adoptPtr(new HTMLImageLoader(this));
215 m_imageLoader->updateFromElement();
219 void HTMLPlugInImageElement::detach()
221 // FIXME: Because of the insanity that is HTMLPlugInImageElement::recalcStyle,
222 // we can end up detaching during an attach() call, before we even have a
223 // renderer. In that case, don't mark the widget for update.
224 if (attached() && renderer() && !useFallbackContent())
225 // Update the widget the next time we attach (detaching destroys the plugin).
226 setNeedsWidgetUpdate(true);
227 HTMLPlugInElement::detach();
230 void HTMLPlugInImageElement::updateWidgetIfNecessary()
232 document()->updateStyleIfNeeded();
234 if (!needsWidgetUpdate() || useFallbackContent() || isImageType())
237 if (!renderEmbeddedObject() || renderEmbeddedObject()->showsUnavailablePluginIndicator())
240 updateWidget(CreateOnlyNonNetscapePlugins);
243 void HTMLPlugInImageElement::finishParsingChildren()
245 HTMLPlugInElement::finishParsingChildren();
246 if (useFallbackContent())
249 setNeedsWidgetUpdate(true);
251 setNeedsStyleRecalc();
254 void HTMLPlugInImageElement::didMoveToNewDocument(Document* oldDocument)
256 if (m_needsDocumentActivationCallbacks) {
258 oldDocument->unregisterForPageCacheSuspensionCallbacks(this);
259 document()->registerForPageCacheSuspensionCallbacks(this);
263 m_imageLoader->elementDidMoveToNewDocument();
264 HTMLPlugInElement::didMoveToNewDocument(oldDocument);
267 void HTMLPlugInImageElement::documentWillSuspendForPageCache()
269 if (RenderStyle* renderStyle = this->renderStyle()) {
270 m_customStyleForPageCache = RenderStyle::clone(renderStyle);
271 m_customStyleForPageCache->setDisplay(NONE);
275 HTMLPlugInElement::documentWillSuspendForPageCache();
278 void HTMLPlugInImageElement::documentDidResumeFromPageCache()
280 if (m_customStyleForPageCache) {
281 m_customStyleForPageCache = 0;
285 HTMLPlugInElement::documentDidResumeFromPageCache();
288 PassRefPtr<RenderStyle> HTMLPlugInImageElement::customStyleForRenderer()
290 if (!m_customStyleForPageCache)
291 return document()->styleResolver()->styleForElement(this);
292 return m_customStyleForPageCache;
295 void HTMLPlugInImageElement::updateWidgetCallback(Node* n, unsigned)
297 static_cast<HTMLPlugInImageElement*>(n)->updateWidgetIfNecessary();
300 void HTMLPlugInImageElement::updateSnapshot(PassRefPtr<Image> image)
302 if (displayState() > DisplayingSnapshot)
305 m_snapshotImage = image;
307 if (renderer()->isSnapshottedPlugIn()) {
308 toRenderSnapshottedPlugIn(renderer())->updateSnapshot(image);
312 if (renderer()->isEmbeddedObject())
313 renderer()->repaint();
316 static AtomicString classNameForShadowRoot(const Node* node)
318 DEFINE_STATIC_LOCAL(const AtomicString, plugInTinySizeClassName, ("tiny", AtomicString::ConstructFromLiteral));
319 DEFINE_STATIC_LOCAL(const AtomicString, plugInSmallSizeClassName, ("small", AtomicString::ConstructFromLiteral));
320 DEFINE_STATIC_LOCAL(const AtomicString, plugInMediumSizeClassName, ("medium", AtomicString::ConstructFromLiteral));
321 DEFINE_STATIC_LOCAL(const AtomicString, plugInLargeSizeClassName, ("large", AtomicString::ConstructFromLiteral));
323 RenderBox* renderBox = static_cast<RenderBox*>(node->renderer());
324 LayoutUnit width = renderBox->contentWidth();
325 LayoutUnit height = renderBox->contentHeight();
327 if (width < sizingTinyDimensionThreshold || height < sizingTinyDimensionThreshold)
328 return plugInTinySizeClassName;
330 if (width < sizingSmallWidthThreshold)
331 return plugInSmallSizeClassName;
333 if (width < sizingMediumWidthThreshold || height < sizingMediumHeightThreshold)
334 return plugInMediumSizeClassName;
336 return plugInLargeSizeClassName;
339 void HTMLPlugInImageElement::updateSnapshotInfo()
341 ShadowRoot* root = userAgentShadowRoot();
345 Element* shadowContainer = toElement(root->firstChild());
346 shadowContainer->setAttribute(classAttr, classNameForShadowRoot(this));
349 void HTMLPlugInImageElement::didAddUserAgentShadowRoot(ShadowRoot* root)
351 Document* doc = document();
353 m_shadowContainer = HTMLDivElement::create(doc);
354 m_shadowContainer->setPseudo(AtomicString("-webkit-snapshotted-plugin-content", AtomicString::ConstructFromLiteral));
356 RefPtr<Element> container = HTMLDivElement::create(doc);
357 container->setAttribute(classAttr, AtomicString("snapshot-container", AtomicString::ConstructFromLiteral));
359 RefPtr<Element> overlay = HTMLDivElement::create(doc);
360 overlay->setAttribute(classAttr, AtomicString("snapshot-overlay", AtomicString::ConstructFromLiteral));
361 container->appendChild(overlay, ASSERT_NO_EXCEPTION);
363 m_snapshotLabel = HTMLDivElement::create(doc);
364 m_snapshotLabel->setAttribute(classAttr, AtomicString("snapshot-label", AtomicString::ConstructFromLiteral));
366 String titleText = snapshottedPlugInLabelTitle();
367 String subtitleText = snapshottedPlugInLabelSubtitle();
368 if (document()->page()) {
369 String clientTitleText = document()->page()->chrome()->client()->plugInStartLabelTitle();
370 if (!clientTitleText.isEmpty())
371 titleText = clientTitleText;
372 String clientSubtitleText = document()->page()->chrome()->client()->plugInStartLabelSubtitle();
373 if (!clientSubtitleText.isEmpty())
374 subtitleText = clientSubtitleText;
377 RefPtr<Element> title = HTMLDivElement::create(doc);
378 title->setAttribute(classAttr, AtomicString("snapshot-title", AtomicString::ConstructFromLiteral));
379 title->appendChild(doc->createTextNode(titleText), ASSERT_NO_EXCEPTION);
380 m_snapshotLabel->appendChild(title, ASSERT_NO_EXCEPTION);
382 RefPtr<Element> subTitle = HTMLDivElement::create(doc);
383 subTitle->setAttribute(classAttr, AtomicString("snapshot-subtitle", AtomicString::ConstructFromLiteral));
384 subTitle->appendChild(doc->createTextNode(subtitleText), ASSERT_NO_EXCEPTION);
385 m_snapshotLabel->appendChild(subTitle, ASSERT_NO_EXCEPTION);
387 container->appendChild(m_snapshotLabel, ASSERT_NO_EXCEPTION);
389 // Make this into a button for accessibility clients.
390 String combinedText = titleText;
391 if (!combinedText.isEmpty() && !subtitleText.isEmpty())
392 combinedText.append(" ");
393 combinedText.append(subtitleText);
394 container->setAttribute(aria_labelAttr, combinedText);
395 container->setAttribute(roleAttr, "button");
397 m_shadowContainer->appendChild(container, ASSERT_NO_EXCEPTION);
398 root->appendChild(m_shadowContainer, ASSERT_NO_EXCEPTION);
401 bool HTMLPlugInImageElement::partOfSnapshotLabel(Node* node)
403 return node && (node == m_snapshotLabel.get() || node->isDescendantOf(m_snapshotLabel.get()));
406 void HTMLPlugInImageElement::swapRendererTimerFired(Timer<HTMLPlugInImageElement>*)
408 ASSERT(displayState() == DisplayingSnapshot);
409 if (userAgentShadowRoot())
412 // Create a shadow root, which will trigger the code to add a snapshot container
413 // and reattach, thus making a new Renderer.
414 ensureUserAgentShadowRoot();
417 void HTMLPlugInImageElement::removeSnapshotTimerFired(Timer<HTMLPlugInImageElement>*)
419 m_snapshotImage = nullptr;
420 m_restartedPlugin = false;
422 renderer()->repaint();
425 static void addPlugInsFromNodeListMatchingPlugInOrigin(HTMLPlugInImageElementList& plugInList, PassRefPtr<NodeList> collection, const String& plugInOrigin, const String& mimeType)
427 for (unsigned i = 0, length = collection->length(); i < length; i++) {
428 Node* node = collection->item(i);
429 if (node->isPluginElement()) {
430 HTMLPlugInElement* plugInElement = toHTMLPlugInElement(node);
431 if (plugInElement->isPlugInImageElement() && plugInElement->displayState() <= HTMLPlugInElement::DisplayingSnapshot) {
432 HTMLPlugInImageElement* plugInImageElement = toHTMLPlugInImageElement(node);
433 const KURL& loadedURL = plugInImageElement->loadedUrl();
434 String otherMimeType = plugInImageElement->loadedMimeType();
435 if (plugInOrigin == loadedURL.host() && mimeType == otherMimeType)
436 plugInList.append(plugInImageElement);
442 void HTMLPlugInImageElement::restartSimilarPlugIns()
444 // Restart any other snapshotted plugins in the page with the same origin. Note that they
445 // may be in different frames, so traverse from the top of the document.
447 String plugInOrigin = m_loadedUrl.host();
448 String mimeType = loadedMimeType();
449 HTMLPlugInImageElementList pluginsNeedingRestart;
451 if (!document()->page())
454 for (Frame* frame = document()->page()->mainFrame(); frame; frame = frame->tree()->traverseNext()) {
455 if (frame->loader()->subframeLoader()->containsPlugins()) {
456 if (!frame->document())
459 RefPtr<NodeList> plugins = frame->document()->getElementsByTagName(embedTag.localName());
461 addPlugInsFromNodeListMatchingPlugInOrigin(pluginsNeedingRestart, plugins, plugInOrigin, mimeType);
463 plugins = frame->document()->getElementsByTagName(objectTag.localName());
465 addPlugInsFromNodeListMatchingPlugInOrigin(pluginsNeedingRestart, plugins, plugInOrigin, mimeType);
469 for (size_t i = 0, length = pluginsNeedingRestart.size(); i < length; i++) {
470 pluginsNeedingRestart[i]->setDisplayState(Playing);
471 pluginsNeedingRestart[i]->restartSnapshottedPlugIn();
475 void HTMLPlugInImageElement::userDidClickSnapshot(PassRefPtr<MouseEvent> event, bool forwardEvent)
478 m_pendingClickEventFromSnapshot = event;
480 String plugInOrigin = m_loadedUrl.host();
481 if (document()->page() && !SchemeRegistry::shouldTreatURLSchemeAsLocal(document()->page()->mainFrame()->document()->baseURL().protocol()) && document()->page()->settings()->autostartOriginPlugInSnapshottingEnabled())
482 document()->page()->plugInClient()->didStartFromOrigin(document()->page()->mainFrame()->document()->baseURL().host(), plugInOrigin, loadedMimeType());
484 restartSnapshottedPlugIn();
485 restartSimilarPlugIns();
488 void HTMLPlugInImageElement::setIsPrimarySnapshottedPlugIn(bool isPrimarySnapshottedPlugIn)
490 if (!document()->page() || !document()->page()->settings()->primaryPlugInSnapshotDetectionEnabled() || document()->page()->settings()->snapshotAllPlugIns())
493 if (isPrimarySnapshottedPlugIn) {
494 restartSnapshottedPlugIn();
495 restartSimilarPlugIns();
499 void HTMLPlugInImageElement::restartSnapshottedPlugIn()
501 if (displayState() != RestartingWithPendingMouseClick)
502 setDisplayState(Restarting);
507 void HTMLPlugInImageElement::dispatchPendingMouseClick()
509 ASSERT(!m_simulatedMouseClickTimer.isActive());
510 m_simulatedMouseClickTimer.restart();
513 void HTMLPlugInImageElement::simulatedMouseClickTimerFired(DeferrableOneShotTimer<HTMLPlugInImageElement>*)
515 ASSERT(displayState() == RestartingWithPendingMouseClick);
516 ASSERT(m_pendingClickEventFromSnapshot);
518 dispatchSimulatedClick(m_pendingClickEventFromSnapshot.get(), SendMouseOverUpDownEvents, DoNotShowPressedLook);
520 setDisplayState(Playing);
521 m_pendingClickEventFromSnapshot = nullptr;
524 void HTMLPlugInImageElement::subframeLoaderWillCreatePlugIn(const KURL& url)
526 LOG(Plugins, "%p Plug-in URL: %s", this, m_url.utf8().data());
527 LOG(Plugins, " Loaded URL: %s", url.string().utf8().data());
531 if (!document()->page()
532 || !document()->page()->settings()->plugInSnapshottingEnabled())
535 if (displayState() == Restarting) {
536 setDisplayState(Playing);
537 LOG(Plugins, "%p Plug-in is explicitly restarting", this);
541 if (displayState() == RestartingWithPendingMouseClick) {
542 LOG(Plugins, "%p Plug-in is explicitly restarting but also waiting for a click", this);
546 bool inMainFrame = document()->frame() == document()->page()->mainFrame();
548 if (document()->isPluginDocument() && inMainFrame) {
549 LOG(Plugins, "%p Plug-in document in main frame", this);
553 if (ScriptController::processingUserGesture()) {
554 LOG(Plugins, "%p Script is currently processing user gesture, set to play", this);
558 if (m_createdDuringUserGesture) {
559 LOG(Plugins, "%p Plug-in was created when processing user gesture, set to play", this);
563 double lastKnownUserGestureTimestamp = document()->lastHandledUserGestureTimestamp();
564 if (!inMainFrame && document()->page()->mainFrame() && document()->page()->mainFrame()->document())
565 lastKnownUserGestureTimestamp = std::max(lastKnownUserGestureTimestamp, document()->page()->mainFrame()->document()->lastHandledUserGestureTimestamp());
566 if (currentTime() - lastKnownUserGestureTimestamp < autostartSoonAfterUserGestureThreshold) {
567 LOG(Plugins, "%p Plug-in was created shortly after a user gesture, set to play", this);
571 if (document()->page()->settings()->snapshotAllPlugIns()) {
572 LOG(Plugins, "%p Plug-in forced to snapshot by user preference", this);
573 setDisplayState(WaitingForSnapshot);
577 RenderBox* renderEmbeddedObject = toRenderBox(renderer());
578 Length styleWidth = renderEmbeddedObject->style()->width();
579 Length styleHeight = renderEmbeddedObject->style()->height();
580 LayoutRect contentBoxRect = renderEmbeddedObject->contentBoxRect();
581 int contentWidth = contentBoxRect.width();
582 int contentHeight = contentBoxRect.height();
583 int contentArea = contentWidth * contentHeight;
584 IntSize visibleViewSize = document()->frame()->view()->visibleSize();
585 int visibleArea = visibleViewSize.width() * visibleViewSize.height();
587 if (inMainFrame && styleWidth.isPercent() && (styleWidth.percent() == 100)
588 && styleHeight.isPercent() && (styleHeight.percent() == 100)
589 && (static_cast<float>(contentArea) / visibleArea > sizingFullPageAreaRatioThreshold)) {
590 LOG(Plugins, "%p Plug-in is top level full page, set to play", this);
594 if (contentWidth <= sizingTinyDimensionThreshold || contentHeight <= sizingTinyDimensionThreshold) {
595 LOG(Plugins, "%p Plug-in is very small %dx%d, set to play", this, contentWidth, contentHeight);
599 if (!document()->page()->plugInClient()) {
600 setDisplayState(WaitingForSnapshot);
604 if (document()->page()->settings()->autostartOriginPlugInSnapshottingEnabled() && document()->page()->plugInClient()->shouldAutoStartFromOrigin(document()->page()->mainFrame()->document()->baseURL().host(), url.host(), loadedMimeType())) {
605 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());
609 LOG(Plugins, "%p Plug-in from (%s, %s) is not auto-start, sized at %dx%d, set to wait for snapshot", this, document()->page()->mainFrame()->document()->baseURL().host().utf8().data(), url.host().utf8().data(), contentWidth, contentHeight);
610 setDisplayState(WaitingForSnapshot);
613 void HTMLPlugInImageElement::subframeLoaderDidCreatePlugIn(const Widget* widget)
615 if (!widget->isPluginViewBase()
616 || !static_cast<const PluginViewBase*>(widget)->shouldAlwaysAutoStart())
619 LOG(Plugins, "%p Plug-in should auto-start, set to play", this);
620 setDisplayState(Playing);
623 } // namespace WebCore