URLs in srcset attributes are not made absolute upon copy and paste
[WebKit-https.git] / Source / WebCore / html / parser / HTMLSrcsetParser.cpp
1 /*
2  * Copyright (C) 2013 Apple Inc. All rights reserved.
3  * Copyright (C) 2013 Google Inc. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  *     * Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *     * Redistributions in binary form must reproduce the above
12  * copyright notice, this list of conditions and the following disclaimer
13  * in the documentation and/or other materials provided with the
14  * distribution.
15  *     * Neither the name of Google Inc. nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31
32 #include "config.h"
33 #include "HTMLSrcsetParser.h"
34
35 #include "HTMLParserIdioms.h"
36 #include "ParsingUtilities.h"
37
38 namespace WebCore {
39
40 static inline bool compareByDensity(const ImageCandidate& first, const ImageCandidate& second)
41 {
42     return first.density < second.density;
43 }
44
45 enum DescriptorTokenizerState {
46     Start,
47     InParenthesis,
48     AfterToken,
49 };
50
51 template<typename CharType>
52 static void appendDescriptorAndReset(const CharType*& descriptorStart, const CharType* position, Vector<StringView>& descriptors)
53 {
54     if (position > descriptorStart)
55         descriptors.append(StringView(descriptorStart, position - descriptorStart));
56     descriptorStart = nullptr;
57 }
58
59 // The following is called appendCharacter to match the spec's terminology.
60 template<typename CharType>
61 static void appendCharacter(const CharType* descriptorStart, const CharType* position)
62 {
63     // Since we don't copy the tokens, this just set the point where the descriptor tokens start.
64     if (!descriptorStart)
65         descriptorStart = position;
66 }
67
68 template<typename CharType>
69 static bool isEOF(const CharType* position, const CharType* end)
70 {
71     return position >= end;
72 }
73
74 template<typename CharType>
75 static void tokenizeDescriptors(const CharType*& position, const CharType* attributeEnd, Vector<StringView>& descriptors)
76 {
77     DescriptorTokenizerState state = Start;
78     const CharType* descriptorsStart = position;
79     const CharType* currentDescriptorStart = descriptorsStart;
80     for (; ; ++position) {
81         switch (state) {
82         case Start:
83             if (isEOF(position, attributeEnd)) {
84                 appendDescriptorAndReset(currentDescriptorStart, attributeEnd, descriptors);
85                 return;
86             }
87             if (isComma(*position)) {
88                 appendDescriptorAndReset(currentDescriptorStart, position, descriptors);
89                 ++position;
90                 return;
91             }
92             if (isHTMLSpace(*position)) {
93                 appendDescriptorAndReset(currentDescriptorStart, position, descriptors);
94                 currentDescriptorStart = position + 1;
95                 state = AfterToken;
96             } else if (*position == '(') {
97                 appendCharacter(currentDescriptorStart, position);
98                 state = InParenthesis;
99             } else
100                 appendCharacter(currentDescriptorStart, position);
101             break;
102         case InParenthesis:
103             if (isEOF(position, attributeEnd)) {
104                 appendDescriptorAndReset(currentDescriptorStart, attributeEnd, descriptors);
105                 return;
106             }
107             if (*position == ')') {
108                 appendCharacter(currentDescriptorStart, position);
109                 state = Start;
110             } else
111                 appendCharacter(currentDescriptorStart, position);
112             break;
113         case AfterToken:
114             if (isEOF(position, attributeEnd))
115                 return;
116             if (!isHTMLSpace(*position)) {
117                 state = Start;
118                 currentDescriptorStart = position;
119                 --position;
120             }
121             break;
122         }
123     }
124 }
125
126 static bool parseDescriptors(Vector<StringView>& descriptors, DescriptorParsingResult& result)
127 {
128     for (auto& descriptor : descriptors) {
129         if (descriptor.isEmpty())
130             continue;
131         unsigned descriptorCharPosition = descriptor.length() - 1;
132         UChar descriptorChar = descriptor[descriptorCharPosition];
133         descriptor = descriptor.substring(0, descriptorCharPosition);
134         bool isValid = false;
135         if (descriptorChar == 'x') {
136             if (result.hasDensity() || result.hasHeight() || result.hasWidth())
137                 return false;
138             float density = descriptor.toFloat(isValid);
139             if (!isValid || density < 0)
140                 return false;
141             result.setDensity(density);
142         } else if (descriptorChar == 'w') {
143             if (result.hasDensity() || result.hasWidth())
144                 return false;
145             int resourceWidth = descriptor.toInt(isValid);
146             if (!isValid || resourceWidth <= 0)
147                 return false;
148             result.setResourceWidth(resourceWidth);
149         } else if (descriptorChar == 'h') {
150             // This is here only for future compat purposes.
151             // The value of the 'h' descriptor is not used.
152             if (result.hasDensity() || result.hasHeight())
153                 return false;
154             int resourceHeight = descriptor.toInt(isValid);
155             if (!isValid || resourceHeight <= 0)
156                 return false;
157             result.setResourceHeight(resourceHeight);
158         }
159     }
160     return true;
161 }
162
163 // http://picture.responsiveimages.org/#parse-srcset-attr
164 template<typename CharType>
165 static Vector<ImageCandidate> parseImageCandidatesFromSrcsetAttribute(const CharType* attributeStart, unsigned length)
166 {
167     Vector<ImageCandidate> imageCandidates;
168
169     const CharType* attributeEnd = attributeStart + length;
170
171     for (const CharType* position = attributeStart; position < attributeEnd;) {
172         // 4. Splitting loop: Collect a sequence of characters that are space characters or U+002C COMMA characters.
173         skipWhile<CharType, isHTMLSpaceOrComma<CharType> >(position, attributeEnd);
174         if (position == attributeEnd) {
175             // Contrary to spec language - descriptor parsing happens on each candidate, so when we reach the attributeEnd, we can exit.
176             break;
177         }
178         const CharType* imageURLStart = position;
179         // 6. Collect a sequence of characters that are not space characters, and let that be url.
180
181         skipUntil<CharType, isHTMLSpace<CharType> >(position, attributeEnd);
182         const CharType* imageURLEnd = position;
183
184         DescriptorParsingResult result;
185
186         // 8. If url ends with a U+002C COMMA character (,)
187         if (isComma(*(position - 1))) {
188             // Remove all trailing U+002C COMMA characters from url.
189             imageURLEnd = position - 1;
190             reverseSkipWhile<CharType, isComma>(imageURLEnd, imageURLStart);
191             ++imageURLEnd;
192             // If url is empty, then jump to the step labeled splitting loop.
193             if (imageURLStart == imageURLEnd)
194                 continue;
195         } else {
196             // Advancing position here (contrary to spec) to avoid an useless extra state machine step.
197             // Filed a spec bug: https://github.com/ResponsiveImagesCG/picture-element/issues/189
198             ++position;
199             Vector<StringView> descriptorTokens;
200             tokenizeDescriptors(position, attributeEnd, descriptorTokens);
201             // Contrary to spec language - descriptor parsing happens on each candidate.
202             // This is a black-box equivalent, to avoid storing descriptor lists for each candidate.
203             if (!parseDescriptors(descriptorTokens, result))
204                 continue;
205         }
206
207         ASSERT(imageURLEnd > imageURLStart);
208         unsigned imageURLLength = imageURLEnd - imageURLStart;
209         imageCandidates.append(ImageCandidate(StringView(imageURLStart, imageURLLength), result, ImageCandidate::SrcsetOrigin));
210         // 11. Return to the step labeled splitting loop.
211     }
212     return imageCandidates;
213 }
214
215 Vector<ImageCandidate> parseImageCandidatesFromSrcsetAttribute(StringView attribute)
216 {
217     // FIXME: We should consider replacing the direct pointers in the parsing process with StringView and positions.
218     if (attribute.is8Bit())
219         return parseImageCandidatesFromSrcsetAttribute<LChar>(attribute.characters8(), attribute.length());
220     else
221         return parseImageCandidatesFromSrcsetAttribute<UChar>(attribute.characters16(), attribute.length());
222 }
223
224 static ImageCandidate pickBestImageCandidate(float deviceScaleFactor, Vector<ImageCandidate>& imageCandidates
225 #if ENABLE(PICTURE_SIZES)
226     , unsigned sourceSize
227 #endif
228     )
229 {
230     bool ignoreSrc = false;
231     if (imageCandidates.isEmpty())
232         return ImageCandidate();
233
234     // http://picture.responsiveimages.org/#normalize-source-densities
235     for (auto& candidate : imageCandidates) {
236 #if ENABLE(PICTURE_SIZES)
237         if (candidate.resourceWidth > 0) {
238             candidate.density = static_cast<float>(candidate.resourceWidth) / static_cast<float>(sourceSize);
239             ignoreSrc = true;
240         } else
241 #endif
242         if (candidate.density < 0)
243             candidate.density = DefaultDensityValue;
244     }
245
246     std::stable_sort(imageCandidates.begin(), imageCandidates.end(), compareByDensity);
247
248     unsigned i;
249     for (i = 0; i < imageCandidates.size() - 1; ++i) {
250         if ((imageCandidates[i].density >= deviceScaleFactor) && (!ignoreSrc || !imageCandidates[i].srcOrigin()))
251             break;
252     }
253
254     if (imageCandidates[i].srcOrigin() && ignoreSrc) {
255         ASSERT(i > 0);
256         --i;
257     }
258     float winningDensity = imageCandidates[i].density;
259
260     unsigned winner = i;
261     // 16. If an entry b in candidates has the same associated ... pixel density as an earlier entry a in candidates,
262     // then remove entry b
263     while ((i > 0) && (imageCandidates[--i].density == winningDensity))
264         winner = i;
265
266     return imageCandidates[winner];
267 }
268
269 ImageCandidate bestFitSourceForImageAttributes(float deviceScaleFactor, const AtomicString& srcAttribute, const AtomicString& srcsetAttribute
270 #if ENABLE(PICTURE_SIZES)
271     , unsigned sourceSize
272 #endif
273     )
274 {
275     if (srcsetAttribute.isNull()) {
276         if (srcAttribute.isNull())
277             return ImageCandidate();
278         return ImageCandidate(StringView(srcAttribute), DescriptorParsingResult(), ImageCandidate::SrcOrigin);
279     }
280
281     Vector<ImageCandidate> imageCandidates = parseImageCandidatesFromSrcsetAttribute(StringView(srcsetAttribute));
282
283     if (!srcAttribute.isEmpty())
284         imageCandidates.append(ImageCandidate(StringView(srcAttribute), DescriptorParsingResult(), ImageCandidate::SrcOrigin));
285
286     return pickBestImageCandidate(deviceScaleFactor, imageCandidates
287 #if ENABLE(PICTURE_SIZES)
288         , sourceSize
289 #endif
290         );
291 }
292
293 } // namespace WebCore