Unreviewed, rolling out r182247.
[WebKit-https.git] / Source / WebCore / html / HTMLImageElement.cpp
1 /*
2  * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
3  *           (C) 1999 Antti Koivisto (koivisto@kde.org)
4  * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2010 Apple Inc. All rights reserved.
5  * Copyright (C) 2010 Google 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 "HTMLImageElement.h"
25
26 #include "CSSPropertyNames.h"
27 #include "CSSValueKeywords.h"
28 #include "CachedImage.h"
29 #include "EventNames.h"
30 #include "FrameView.h"
31 #include "HTMLAnchorElement.h"
32 #include "HTMLDocument.h"
33 #include "HTMLFormElement.h"
34 #include "HTMLParserIdioms.h"
35 #include "HTMLSrcsetParser.h"
36 #include "Page.h"
37 #include "RenderImage.h"
38 #include "Settings.h"
39 #include "ShadowRoot.h"
40 #include "SourceSizeList.h"
41 #include <wtf/text/StringBuilder.h>
42
43 #if ENABLE(SERVICE_CONTROLS)
44 #include "ImageControlsRootElement.h"
45 #endif
46
47 namespace WebCore {
48
49 using namespace HTMLNames;
50
51 HTMLImageElement::HTMLImageElement(const QualifiedName& tagName, Document& document, HTMLFormElement* form)
52     : HTMLElement(tagName, document)
53     , m_imageLoader(*this)
54     , m_form(form)
55     , m_compositeOperator(CompositeSourceOver)
56     , m_imageDevicePixelRatio(1.0f)
57 #if ENABLE(SERVICE_CONTROLS)
58     , m_experimentalImageMenuEnabled(false)
59 #endif
60 {
61     ASSERT(hasTagName(imgTag));
62     setHasCustomStyleResolveCallbacks();
63     if (form)
64         form->registerImgElement(this);
65 }
66
67 Ref<HTMLImageElement> HTMLImageElement::create(Document& document)
68 {
69     return adoptRef(*new HTMLImageElement(imgTag, document));
70 }
71
72 Ref<HTMLImageElement> HTMLImageElement::create(const QualifiedName& tagName, Document& document, HTMLFormElement* form)
73 {
74     return adoptRef(*new HTMLImageElement(tagName, document, form));
75 }
76
77 HTMLImageElement::~HTMLImageElement()
78 {
79     if (m_form)
80         m_form->removeImgElement(this);
81 }
82
83 Ref<HTMLImageElement> HTMLImageElement::createForJSConstructor(Document& document, const int* optionalWidth, const int* optionalHeight)
84 {
85     Ref<HTMLImageElement> image = adoptRef(*new HTMLImageElement(imgTag, document));
86     if (optionalWidth)
87         image->setWidth(*optionalWidth);
88     if (optionalHeight)
89         image->setHeight(*optionalHeight);
90     return image;
91 }
92
93 bool HTMLImageElement::isPresentationAttribute(const QualifiedName& name) const
94 {
95     if (name == widthAttr || name == heightAttr || name == borderAttr || name == vspaceAttr || name == hspaceAttr || name == alignAttr || name == valignAttr)
96         return true;
97     return HTMLElement::isPresentationAttribute(name);
98 }
99
100 void HTMLImageElement::collectStyleForPresentationAttribute(const QualifiedName& name, const AtomicString& value, MutableStyleProperties& style)
101 {
102     if (name == widthAttr)
103         addHTMLLengthToStyle(style, CSSPropertyWidth, value);
104     else if (name == heightAttr)
105         addHTMLLengthToStyle(style, CSSPropertyHeight, value);
106     else if (name == borderAttr)
107         applyBorderAttributeToStyle(value, style);
108     else if (name == vspaceAttr) {
109         addHTMLLengthToStyle(style, CSSPropertyMarginTop, value);
110         addHTMLLengthToStyle(style, CSSPropertyMarginBottom, value);
111     } else if (name == hspaceAttr) {
112         addHTMLLengthToStyle(style, CSSPropertyMarginLeft, value);
113         addHTMLLengthToStyle(style, CSSPropertyMarginRight, value);
114     } else if (name == alignAttr)
115         applyAlignmentAttributeToStyle(value, style);
116     else if (name == valignAttr)
117         addPropertyToPresentationAttributeStyle(style, CSSPropertyVerticalAlign, value);
118     else
119         HTMLElement::collectStyleForPresentationAttribute(name, value, style);
120 }
121
122 const AtomicString& HTMLImageElement::imageSourceURL() const
123 {
124     return m_bestFitImageURL.isEmpty() ? fastGetAttribute(srcAttr) : m_bestFitImageURL;
125 }
126
127 void HTMLImageElement::setBestFitURLAndDPRFromImageCandidate(const ImageCandidate& candidate)
128 {
129     m_bestFitImageURL = candidate.string.toString();
130 #if ENABLE(PICTURE_SIZES)
131     m_currentSrc = AtomicString(document().completeURL(imageSourceURL()).string());
132 #endif
133     if (candidate.density >= 0)
134         m_imageDevicePixelRatio = 1 / candidate.density;
135     if (is<RenderImage>(renderer()))
136         downcast<RenderImage>(*renderer()).setImageDevicePixelRatio(m_imageDevicePixelRatio);
137 }
138
139 void HTMLImageElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
140 {
141     if (name == altAttr) {
142         if (is<RenderImage>(renderer()))
143             downcast<RenderImage>(*renderer()).updateAltText();
144     } else if (name == srcAttr || name == srcsetAttr) {
145         unsigned sourceSize = parseSizesAttribute(fastGetAttribute(sizesAttr).string(), document().renderView(), document().frame());
146         ImageCandidate candidate = bestFitSourceForImageAttributes(document().deviceScaleFactor(), fastGetAttribute(srcAttr), fastGetAttribute(srcsetAttr), sourceSize);
147         setBestFitURLAndDPRFromImageCandidate(candidate);
148         m_imageLoader.updateFromElementIgnoringPreviousError();
149     } else if (name == usemapAttr) {
150         if (inDocument() && !m_lowercasedUsemap.isNull())
151             document().removeImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
152
153         // The HTMLImageElement's useMap() value includes the '#' symbol at the beginning, which has to be stripped off.
154         // FIXME: We should check that the first character is '#'.
155         // FIXME: HTML5 specification says we should strip any leading string before '#'.
156         // FIXME: HTML5 specification says we should ignore usemap attributes without #.
157         if (value.length() > 1)
158             m_lowercasedUsemap = value.string().substring(1).lower();
159         else
160             m_lowercasedUsemap = nullAtom;
161
162         if (inDocument() && !m_lowercasedUsemap.isNull())
163             document().addImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
164     } else if (name == compositeAttr) {
165         // FIXME: images don't support blend modes in their compositing attribute.
166         BlendMode blendOp = BlendModeNormal;
167         if (!parseCompositeAndBlendOperator(value, m_compositeOperator, blendOp))
168             m_compositeOperator = CompositeSourceOver;
169 #if ENABLE(SERVICE_CONTROLS)
170     } else if (name == webkitimagemenuAttr) {
171         m_experimentalImageMenuEnabled = !value.isNull();
172         updateImageControls();
173 #endif
174     } else {
175         if (name == nameAttr) {
176             bool willHaveName = !value.isNull();
177             if (hasName() != willHaveName && inDocument() && is<HTMLDocument>(document())) {
178                 HTMLDocument& document = downcast<HTMLDocument>(this->document());
179                 const AtomicString& id = getIdAttribute();
180                 if (!id.isEmpty() && id != getNameAttribute()) {
181                     if (willHaveName)
182                         document.addDocumentNamedItem(*id.impl(), *this);
183                     else
184                         document.removeDocumentNamedItem(*id.impl(), *this);
185                 }
186             }
187         }
188         HTMLElement::parseAttribute(name, value);
189     }
190 }
191
192 const AtomicString& HTMLImageElement::altText() const
193 {
194     // lets figure out the alt text.. magic stuff
195     // http://www.w3.org/TR/1998/REC-html40-19980424/appendix/notes.html#altgen
196     // also heavily discussed by Hixie on bugzilla
197     const AtomicString& alt = fastGetAttribute(altAttr);
198     if (!alt.isNull())
199         return alt;
200     // fall back to title attribute
201     return fastGetAttribute(titleAttr);
202 }
203
204 RenderPtr<RenderElement> HTMLImageElement::createElementRenderer(Ref<RenderStyle>&& style)
205 {
206     if (style.get().hasContent())
207         return RenderElement::createFor(*this, WTF::move(style));
208
209     return createRenderer<RenderImage>(*this, WTF::move(style), nullptr, m_imageDevicePixelRatio);
210 }
211
212 bool HTMLImageElement::canStartSelection() const
213 {
214     if (shadowRoot())
215         return HTMLElement::canStartSelection();
216
217     return false;
218 }
219
220 void HTMLImageElement::didAttachRenderers()
221 {
222     if (!is<RenderImage>(renderer()))
223         return;
224     if (m_imageLoader.hasPendingBeforeLoadEvent())
225         return;
226
227 #if ENABLE(SERVICE_CONTROLS)
228     updateImageControls();
229 #endif
230
231     auto& renderImage = downcast<RenderImage>(*renderer());
232     RenderImageResource& renderImageResource = renderImage.imageResource();
233     if (renderImageResource.hasImage())
234         return;
235     renderImageResource.setCachedImage(m_imageLoader.image());
236
237     // If we have no image at all because we have no src attribute, set
238     // image height and width for the alt text instead.
239     if (!m_imageLoader.image() && !renderImageResource.cachedImage())
240         renderImage.setImageSizeForAltText();
241 }
242
243 Node::InsertionNotificationRequest HTMLImageElement::insertedInto(ContainerNode& insertionPoint)
244 {
245     if (!m_form) { // m_form can be non-null if it was set in constructor.
246         m_form = HTMLFormElement::findClosestFormAncestor(*this);
247         if (m_form)
248             m_form->registerImgElement(this);
249     }
250
251     // Insert needs to complete first, before we start updating the loader. Loader dispatches events which could result
252     // in callbacks back to this node.
253     Node::InsertionNotificationRequest insertNotificationRequest = HTMLElement::insertedInto(insertionPoint);
254
255     if (insertionPoint.inDocument() && !m_lowercasedUsemap.isNull())
256         document().addImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
257
258     // If we have been inserted from a renderer-less document,
259     // our loader may have not fetched the image, so do it now.
260     if (insertionPoint.inDocument() && !m_imageLoader.image())
261         m_imageLoader.updateFromElement();
262
263     return insertNotificationRequest;
264 }
265
266 void HTMLImageElement::removedFrom(ContainerNode& insertionPoint)
267 {
268     if (m_form)
269         m_form->removeImgElement(this);
270
271     if (insertionPoint.inDocument() && !m_lowercasedUsemap.isNull())
272         document().removeImageElementByLowercasedUsemap(*m_lowercasedUsemap.impl(), *this);
273
274     m_form = 0;
275     HTMLElement::removedFrom(insertionPoint);
276 }
277
278 int HTMLImageElement::width(bool ignorePendingStylesheets)
279 {
280     if (!renderer()) {
281         // check the attribute first for an explicit pixel value
282         bool ok;
283         int width = getAttribute(widthAttr).toInt(&ok);
284         if (ok)
285             return width;
286
287         // if the image is available, use its width
288         if (m_imageLoader.image())
289             return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).width();
290     }
291
292     if (ignorePendingStylesheets)
293         document().updateLayoutIgnorePendingStylesheets();
294     else
295         document().updateLayout();
296
297     RenderBox* box = renderBox();
298     return box ? adjustForAbsoluteZoom(box->contentBoxRect().pixelSnappedSize().width(), *box) : 0;
299 }
300
301 int HTMLImageElement::height(bool ignorePendingStylesheets)
302 {
303     if (!renderer()) {
304         // check the attribute first for an explicit pixel value
305         bool ok;
306         int height = getAttribute(heightAttr).toInt(&ok);
307         if (ok)
308             return height;
309
310         // if the image is available, use its height
311         if (m_imageLoader.image())
312             return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).height();
313     }
314
315     if (ignorePendingStylesheets)
316         document().updateLayoutIgnorePendingStylesheets();
317     else
318         document().updateLayout();
319
320     RenderBox* box = renderBox();
321     return box ? adjustForAbsoluteZoom(box->contentBoxRect().pixelSnappedSize().height(), *box) : 0;
322 }
323
324 int HTMLImageElement::naturalWidth() const
325 {
326     if (!m_imageLoader.image())
327         return 0;
328
329     return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).width();
330 }
331
332 int HTMLImageElement::naturalHeight() const
333 {
334     if (!m_imageLoader.image())
335         return 0;
336
337     return m_imageLoader.image()->imageSizeForRenderer(renderer(), 1.0f).height();
338 }
339
340 bool HTMLImageElement::isURLAttribute(const Attribute& attribute) const
341 {
342     return attribute.name() == srcAttr
343         || attribute.name() == lowsrcAttr
344         || attribute.name() == longdescAttr
345         || (attribute.name() == usemapAttr && attribute.value().string()[0] != '#')
346         || HTMLElement::isURLAttribute(attribute);
347 }
348
349 bool HTMLImageElement::attributeContainsURL(const Attribute& attribute) const
350 {
351     return attribute.name() == srcsetAttr
352         || HTMLElement::attributeContainsURL(attribute);
353 }
354
355 String HTMLImageElement::completeURLsInAttributeValue(const URL& base, const Attribute& attribute) const
356 {
357     if (attribute.name() == srcsetAttr) {
358         Vector<ImageCandidate> imageCandidates = parseImageCandidatesFromSrcsetAttribute(StringView(attribute.value()));
359         StringBuilder result;
360         for (const auto& candidate : imageCandidates) {
361             if (&candidate != &imageCandidates[0])
362                 result.appendLiteral(", ");
363             result.append(URL(base, candidate.string.toString()).string());
364             if (candidate.density != UninitializedDescriptor) {
365                 result.append(' ');
366                 result.appendNumber(candidate.density);
367                 result.append('x');
368             }
369             if (candidate.resourceWidth != UninitializedDescriptor) {
370                 result.append(' ');
371                 result.appendNumber(candidate.resourceWidth);
372                 result.append('x');
373             }
374         }
375         return result.toString();
376     }
377     return HTMLElement::completeURLsInAttributeValue(base, attribute);
378 }
379
380 bool HTMLImageElement::matchesLowercasedUsemap(const AtomicStringImpl& name) const
381 {
382     ASSERT(String(&const_cast<AtomicStringImpl&>(name)).lower().impl() == &name);
383     return m_lowercasedUsemap.impl() == &name;
384 }
385
386 const AtomicString& HTMLImageElement::alt() const
387 {
388     return fastGetAttribute(altAttr);
389 }
390
391 bool HTMLImageElement::draggable() const
392 {
393     // Image elements are draggable by default.
394     return !equalIgnoringCase(fastGetAttribute(draggableAttr), "false");
395 }
396
397 void HTMLImageElement::setHeight(int value)
398 {
399     setIntegralAttribute(heightAttr, value);
400 }
401
402 URL HTMLImageElement::src() const
403 {
404     return document().completeURL(fastGetAttribute(srcAttr));
405 }
406
407 void HTMLImageElement::setSrc(const String& value)
408 {
409     setAttribute(srcAttr, value);
410 }
411
412 void HTMLImageElement::setWidth(int value)
413 {
414     setIntegralAttribute(widthAttr, value);
415 }
416
417 int HTMLImageElement::x() const
418 {
419     document().updateLayoutIgnorePendingStylesheets();
420     auto renderer = this->renderer();
421     if (!renderer)
422         return 0;
423
424     // FIXME: This doesn't work correctly with transforms.
425     return renderer->localToAbsolute().x();
426 }
427
428 int HTMLImageElement::y() const
429 {
430     document().updateLayoutIgnorePendingStylesheets();
431     auto renderer = this->renderer();
432     if (!renderer)
433         return 0;
434
435     // FIXME: This doesn't work correctly with transforms.
436     return renderer->localToAbsolute().y();
437 }
438
439 bool HTMLImageElement::complete() const
440 {
441     return m_imageLoader.imageComplete();
442 }
443
444 void HTMLImageElement::addSubresourceAttributeURLs(ListHashSet<URL>& urls) const
445 {
446     HTMLElement::addSubresourceAttributeURLs(urls);
447
448     addSubresourceURL(urls, src());
449     // FIXME: What about when the usemap attribute begins with "#"?
450     addSubresourceURL(urls, document().completeURL(fastGetAttribute(usemapAttr)));
451 }
452
453 void HTMLImageElement::didMoveToNewDocument(Document* oldDocument)
454 {
455     m_imageLoader.elementDidMoveToNewDocument();
456     HTMLElement::didMoveToNewDocument(oldDocument);
457 }
458
459 bool HTMLImageElement::isServerMap() const
460 {
461     if (!fastHasAttribute(ismapAttr))
462         return false;
463
464     const AtomicString& usemap = fastGetAttribute(usemapAttr);
465     
466     // If the usemap attribute starts with '#', it refers to a map element in the document.
467     if (usemap.string()[0] == '#')
468         return false;
469
470     return document().completeURL(stripLeadingAndTrailingHTMLSpaces(usemap)).isEmpty();
471 }
472
473 #if ENABLE(SERVICE_CONTROLS)
474 void HTMLImageElement::updateImageControls()
475 {
476     // If this image element is inside a shadow tree then it is part of an image control.
477     if (isInShadowTree())
478         return;
479
480     Settings* settings = document().settings();
481     if (!settings || !settings->imageControlsEnabled())
482         return;
483
484     bool hasControls = hasImageControls();
485     if (!m_experimentalImageMenuEnabled && hasControls)
486         destroyImageControls();
487     else if (m_experimentalImageMenuEnabled && !hasControls)
488         createImageControls();
489 }
490
491 void HTMLImageElement::createImageControls()
492 {
493     ASSERT(m_experimentalImageMenuEnabled);
494     ASSERT(!hasImageControls());
495
496     RefPtr<ImageControlsRootElement> imageControls = ImageControlsRootElement::maybeCreate(document());
497     if (!imageControls)
498         return;
499
500     ensureUserAgentShadowRoot().appendChild(imageControls);
501
502     auto* renderObject = renderer();
503     if (!renderObject)
504         return;
505
506     downcast<RenderImage>(*renderObject).setHasShadowControls(true);
507 }
508
509 void HTMLImageElement::destroyImageControls()
510 {
511     ShadowRoot* shadowRoot = userAgentShadowRoot();
512     if (!shadowRoot)
513         return;
514
515     if (Node* node = shadowRoot->firstChild()) {
516         ASSERT_WITH_SECURITY_IMPLICATION(node->isImageControlsRootElement());
517         shadowRoot->removeChild(node);
518     }
519
520     auto* renderObject = renderer();
521     if (!renderObject)
522         return;
523
524     downcast<RenderImage>(*renderObject).setHasShadowControls(false);
525 }
526
527 bool HTMLImageElement::hasImageControls() const
528 {
529     if (ShadowRoot* shadowRoot = userAgentShadowRoot()) {
530         Node* node = shadowRoot->firstChild();
531         ASSERT_WITH_SECURITY_IMPLICATION(!node || node->isImageControlsRootElement());
532         return node;
533     }
534
535     return false;
536 }
537
538 bool HTMLImageElement::childShouldCreateRenderer(const Node& child) const
539 {
540     return hasShadowRootParent(child) && HTMLElement::childShouldCreateRenderer(child);
541 }
542 #endif // ENABLE(SERVICE_CONTROLS)
543
544 #if PLATFORM(IOS)
545 // FIXME: This is a workaround for <rdar://problem/7725158>. We should find a better place for the touchCalloutEnabled() logic.
546 bool HTMLImageElement::willRespondToMouseClickEvents()
547 {
548     auto renderer = this->renderer();
549     if (!renderer || renderer->style().touchCalloutEnabled())
550         return true;
551     return HTMLElement::willRespondToMouseClickEvents();
552 }
553 #endif
554
555 }