data: URLs should not be preloaded
[WebKit-https.git] / Source / WebCore / html / parser / HTMLPreloadScanner.cpp
1 /*
2  * Copyright (C) 2008, 2014 Apple Inc. All Rights Reserved.
3  * Copyright (C) 2009 Torch Mobile, Inc. http://www.torchmobile.com/
4  * Copyright (C) 2010 Google Inc. All Rights Reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
16  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
18  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
19  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
23  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
26  */
27
28 #include "config.h"
29 #include "HTMLPreloadScanner.h"
30
31 #include "HTMLNames.h"
32 #include "HTMLParserIdioms.h"
33 #include "HTMLSrcsetParser.h"
34 #include "HTMLTokenizer.h"
35 #include "InputTypeNames.h"
36 #include "LinkRelAttribute.h"
37 #include "SourceSizeList.h"
38 #include <wtf/MainThread.h>
39
40 namespace WebCore {
41
42 using namespace HTMLNames;
43
44 TokenPreloadScanner::TagId TokenPreloadScanner::tagIdFor(const HTMLToken::DataVector& data)
45 {
46     AtomicString tagName(data);
47     if (tagName == imgTag)
48         return TagId::Img;
49     if (tagName == inputTag)
50         return TagId::Input;
51     if (tagName == linkTag)
52         return TagId::Link;
53     if (tagName == scriptTag)
54         return TagId::Script;
55     if (tagName == styleTag)
56         return TagId::Style;
57     if (tagName == baseTag)
58         return TagId::Base;
59     if (tagName == templateTag)
60         return TagId::Template;
61     if (tagName == metaTag)
62         return TagId::Meta;
63     return TagId::Unknown;
64 }
65
66 String TokenPreloadScanner::initiatorFor(TagId tagId)
67 {
68     switch (tagId) {
69     case TagId::Img:
70         return "img";
71     case TagId::Input:
72         return "input";
73     case TagId::Link:
74         return "link";
75     case TagId::Script:
76         return "script";
77     case TagId::Unknown:
78     case TagId::Style:
79     case TagId::Base:
80     case TagId::Template:
81     case TagId::Meta:
82         ASSERT_NOT_REACHED();
83         return "unknown";
84     }
85     ASSERT_NOT_REACHED();
86     return "unknown";
87 }
88
89 class TokenPreloadScanner::StartTagScanner {
90 public:
91     explicit StartTagScanner(TagId tagId, float deviceScaleFactor = 1.0)
92         : m_tagId(tagId)
93         , m_linkIsStyleSheet(false)
94         , m_metaIsViewport(false)
95         , m_inputIsImage(false)
96         , m_deviceScaleFactor(deviceScaleFactor)
97     {
98     }
99
100     void processAttributes(const HTMLToken::AttributeList& attributes, Document& document)
101     {
102         ASSERT(isMainThread());
103         if (m_tagId >= TagId::Unknown)
104             return;
105
106         for (auto& attribute : attributes) {
107             AtomicString attributeName(attribute.name);
108             String attributeValue = StringImpl::create8BitIfPossible(attribute.value);
109             processAttribute(attributeName, attributeValue);
110         }
111
112         // Resolve between src and srcSet if we have them and the tag is img.
113         if (m_tagId == TagId::Img && !m_srcSetAttribute.isEmpty()) {
114             float sourceSize = 0;
115             sourceSize = parseSizesAttribute(m_sizesAttribute, document.renderView(), document.frame());
116             ImageCandidate imageCandidate = bestFitSourceForImageAttributes(m_deviceScaleFactor, m_urlToLoad, m_srcSetAttribute, sourceSize);
117             setUrlToLoad(imageCandidate.string.toString(), true);
118         }
119
120         if (m_metaIsViewport && !m_metaContent.isNull())
121             document.processViewport(m_metaContent, ViewportArguments::ViewportMeta);
122     }
123
124     std::unique_ptr<PreloadRequest> createPreloadRequest(const URL& predictedBaseURL)
125     {
126         if (!shouldPreload())
127             return nullptr;
128
129         auto request = std::make_unique<PreloadRequest>(initiatorFor(m_tagId), m_urlToLoad, predictedBaseURL, resourceType(), m_mediaAttribute);
130
131         request->setCrossOriginModeAllowsCookies(crossOriginModeAllowsCookies());
132         request->setCharset(charset());
133         return request;
134     }
135
136     static bool match(const AtomicString& name, const QualifiedName& qName)
137     {
138         ASSERT(isMainThread());
139         return qName.localName() == name;
140     }
141
142 private:
143     void processImageAndScriptAttribute(const AtomicString& attributeName, const String& attributeValue)
144     {
145         if (match(attributeName, srcAttr))
146             setUrlToLoad(attributeValue);
147         else if (match(attributeName, crossoriginAttr) && !attributeValue.isNull())
148             m_crossOriginMode = stripLeadingAndTrailingHTMLSpaces(attributeValue);
149         else if (match(attributeName, charsetAttr))
150             m_charset = attributeValue;
151     }
152
153     void processAttribute(const AtomicString& attributeName, const String& attributeValue)
154     {
155         switch (m_tagId) {
156         case TagId::Img:
157             if (match(attributeName, srcsetAttr) && m_srcSetAttribute.isNull()) {
158                 m_srcSetAttribute = attributeValue;
159                 break;
160             }
161             if (match(attributeName, sizesAttr) && m_sizesAttribute.isNull()) {
162                 m_sizesAttribute = attributeValue;
163                 break;
164             }
165             processImageAndScriptAttribute(attributeName, attributeValue);
166             break;
167         case TagId::Script:
168             processImageAndScriptAttribute(attributeName, attributeValue);
169             break;
170         case TagId::Link:
171             if (match(attributeName, hrefAttr))
172                 setUrlToLoad(attributeValue);
173             else if (match(attributeName, relAttr))
174                 m_linkIsStyleSheet = relAttributeIsStyleSheet(attributeValue);
175             else if (match(attributeName, mediaAttr))
176                 m_mediaAttribute = attributeValue;
177             else if (match(attributeName, charsetAttr))
178                 m_charset = attributeValue;
179             break;
180         case TagId::Input:
181             if (match(attributeName, srcAttr))
182                 setUrlToLoad(attributeValue);
183             else if (match(attributeName, typeAttr))
184                 m_inputIsImage = equalIgnoringCase(attributeValue, InputTypeNames::image());
185             break;
186         case TagId::Meta:
187             if (match(attributeName, contentAttr))
188                 m_metaContent = attributeValue;
189             else if (match(attributeName, nameAttr))
190                 m_metaIsViewport = equalIgnoringCase(attributeValue, "viewport");
191             break;
192         case TagId::Base:
193         case TagId::Style:
194         case TagId::Template:
195         case TagId::Unknown:
196             break;
197         }
198     }
199
200     static bool relAttributeIsStyleSheet(const String& attributeValue)
201     {
202         LinkRelAttribute parsedAttribute { attributeValue };
203         return parsedAttribute.isStyleSheet && !parsedAttribute.isAlternate && parsedAttribute.iconType == InvalidIcon && !parsedAttribute.isDNSPrefetch;
204     }
205
206     void setUrlToLoad(const String& value, bool allowReplacement = false)
207     {
208         // We only respect the first src/href, per HTML5:
209         // http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#attribute-name-state
210         if (!allowReplacement && !m_urlToLoad.isEmpty())
211             return;
212         String url = stripLeadingAndTrailingHTMLSpaces(value);
213         if (url.isEmpty())
214             return;
215         m_urlToLoad = url;
216     }
217
218     const String& charset() const
219     {
220         return m_charset;
221     }
222
223     CachedResource::Type resourceType() const
224     {
225         if (m_tagId == TagId::Script)
226             return CachedResource::Script;
227         if (m_tagId == TagId::Img || (m_tagId == TagId::Input && m_inputIsImage))
228             return CachedResource::ImageResource;
229         if (m_tagId == TagId::Link && m_linkIsStyleSheet)
230             return CachedResource::CSSStyleSheet;
231         ASSERT_NOT_REACHED();
232         return CachedResource::RawResource;
233     }
234
235     bool shouldPreload()
236     {
237         if (m_urlToLoad.isEmpty())
238             return false;
239
240         if (protocolIs(m_urlToLoad, "data"))
241             return false;
242
243         if (m_tagId == TagId::Link && !m_linkIsStyleSheet)
244             return false;
245
246         if (m_tagId == TagId::Input && !m_inputIsImage)
247             return false;
248
249         return true;
250     }
251
252     bool crossOriginModeAllowsCookies()
253     {
254         return m_crossOriginMode.isNull() || equalIgnoringCase(m_crossOriginMode, "use-credentials");
255     }
256
257     TagId m_tagId;
258     String m_urlToLoad;
259     String m_srcSetAttribute;
260     String m_sizesAttribute;
261     String m_charset;
262     String m_crossOriginMode;
263     bool m_linkIsStyleSheet;
264     String m_mediaAttribute;
265     String m_metaContent;
266     bool m_metaIsViewport;
267     bool m_inputIsImage;
268     float m_deviceScaleFactor;
269 };
270
271 TokenPreloadScanner::TokenPreloadScanner(const URL& documentURL, float deviceScaleFactor)
272     : m_documentURL(documentURL)
273     , m_deviceScaleFactor(deviceScaleFactor)
274 {
275 }
276
277 void TokenPreloadScanner::scan(const HTMLToken& token, Vector<std::unique_ptr<PreloadRequest>>& requests, Document& document)
278 {
279     switch (token.type()) {
280     case HTMLToken::Character:
281         if (!m_inStyle)
282             return;
283         m_cssScanner.scan(token.characters(), requests);
284         return;
285
286     case HTMLToken::EndTag: {
287         TagId tagId = tagIdFor(token.name());
288 #if ENABLE(TEMPLATE_ELEMENT)
289         if (tagId == TagId::Template) {
290             if (m_templateCount)
291                 --m_templateCount;
292             return;
293         }
294 #endif
295         if (tagId == TagId::Style) {
296             if (m_inStyle)
297                 m_cssScanner.reset();
298             m_inStyle = false;
299         }
300         return;
301     }
302
303     case HTMLToken::StartTag: {
304 #if ENABLE(TEMPLATE_ELEMENT)
305         if (m_templateCount)
306             return;
307 #endif
308         TagId tagId = tagIdFor(token.name());
309 #if ENABLE(TEMPLATE_ELEMENT)
310         if (tagId == TagId::Template) {
311             ++m_templateCount;
312             return;
313         }
314 #endif
315         if (tagId == TagId::Style) {
316             m_inStyle = true;
317             return;
318         }
319         if (tagId == TagId::Base) {
320             // The first <base> element is the one that wins.
321             if (!m_predictedBaseElementURL.isEmpty())
322                 return;
323             updatePredictedBaseURL(token);
324             return;
325         }
326
327         StartTagScanner scanner(tagId, m_deviceScaleFactor);
328         scanner.processAttributes(token.attributes(), document);
329         if (auto request = scanner.createPreloadRequest(m_predictedBaseElementURL))
330             requests.append(WTF::move(request));
331         return;
332     }
333
334     default:
335         return;
336     }
337 }
338
339 void TokenPreloadScanner::updatePredictedBaseURL(const HTMLToken& token)
340 {
341     ASSERT(m_predictedBaseElementURL.isEmpty());
342     if (auto* hrefAttribute = findAttribute(token.attributes(), hrefAttr.localName().string()))
343         m_predictedBaseElementURL = URL(m_documentURL, stripLeadingAndTrailingHTMLSpaces(StringImpl::create8BitIfPossible(hrefAttribute->value))).isolatedCopy();
344 }
345
346 HTMLPreloadScanner::HTMLPreloadScanner(const HTMLParserOptions& options, const URL& documentURL, float deviceScaleFactor)
347     : m_scanner(documentURL, deviceScaleFactor)
348     , m_tokenizer(options)
349 {
350 }
351
352 void HTMLPreloadScanner::appendToEnd(const SegmentedString& source)
353 {
354     m_source.append(source);
355 }
356
357 void HTMLPreloadScanner::scan(HTMLResourcePreloader& preloader, Document& document)
358 {
359     ASSERT(isMainThread()); // HTMLTokenizer::updateStateFor only works on the main thread.
360
361     const URL& startingBaseElementURL = document.baseElementURL();
362
363     // When we start scanning, our best prediction of the baseElementURL is the real one!
364     if (!startingBaseElementURL.isEmpty())
365         m_scanner.setPredictedBaseElementURL(startingBaseElementURL);
366
367     PreloadRequestStream requests;
368
369     while (auto token = m_tokenizer.nextToken(m_source)) {
370         if (token->type() == HTMLToken::StartTag)
371             m_tokenizer.updateStateFor(AtomicString(token->name()));
372         m_scanner.scan(*token, requests, document);
373     }
374
375     preloader.preload(WTF::move(requests));
376 }
377
378 bool testPreloadScannerViewportSupport(Document* document)
379 {
380     ASSERT(document);
381     HTMLParserOptions options(*document);
382     HTMLPreloadScanner scanner(options, document->url());
383     HTMLResourcePreloader preloader(*document);
384     scanner.appendToEnd(String("<meta name=viewport content='width=400'>"));
385     scanner.scan(preloader, *document);
386     return (document->viewportArguments().width == 400);
387 }
388
389 }