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