5ab457b086d7a27c6c649d2efd423c542fb8d5fc
[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 "LinkLoader.h"
37 #include "LinkRelAttribute.h"
38 #include "Logging.h"
39 #include "MIMETypeRegistry.h"
40 #include "MediaList.h"
41 #include "MediaQueryEvaluator.h"
42 #include "RenderView.h"
43 #include "SizesAttributeParser.h"
44 #include <wtf/MainThread.h>
45
46 namespace WebCore {
47
48 using namespace HTMLNames;
49
50 TokenPreloadScanner::TagId TokenPreloadScanner::tagIdFor(const HTMLToken::DataVector& data)
51 {
52     AtomicString tagName(data);
53     if (tagName == imgTag)
54         return TagId::Img;
55     if (tagName == inputTag)
56         return TagId::Input;
57     if (tagName == linkTag)
58         return TagId::Link;
59     if (tagName == scriptTag)
60         return TagId::Script;
61     if (tagName == styleTag)
62         return TagId::Style;
63     if (tagName == baseTag)
64         return TagId::Base;
65     if (tagName == templateTag)
66         return TagId::Template;
67     if (tagName == metaTag)
68         return TagId::Meta;
69     if (tagName == pictureTag)
70         return TagId::Picture;
71     if (tagName == sourceTag)
72         return TagId::Source;
73     return TagId::Unknown;
74 }
75
76 String TokenPreloadScanner::initiatorFor(TagId tagId)
77 {
78     switch (tagId) {
79     case TagId::Source:
80     case TagId::Img:
81         return ASCIILiteral("img");
82     case TagId::Input:
83         return ASCIILiteral("input");
84     case TagId::Link:
85         return ASCIILiteral("link");
86     case TagId::Script:
87         return ASCIILiteral("script");
88     case TagId::Unknown:
89     case TagId::Style:
90     case TagId::Base:
91     case TagId::Template:
92     case TagId::Meta:
93     case TagId::Picture:
94         ASSERT_NOT_REACHED();
95         return ASCIILiteral("unknown");
96     }
97     ASSERT_NOT_REACHED();
98     return ASCIILiteral("unknown");
99 }
100
101 class TokenPreloadScanner::StartTagScanner {
102 public:
103     explicit StartTagScanner(TagId tagId, float deviceScaleFactor = 1.0)
104         : m_tagId(tagId)
105         , m_linkIsStyleSheet(false)
106         , m_linkIsPreload(false)
107         , m_metaIsViewport(false)
108         , m_inputIsImage(false)
109         , m_deviceScaleFactor(deviceScaleFactor)
110     {
111     }
112
113     void processAttributes(const HTMLToken::AttributeList& attributes, Document& document, Vector<bool>& pictureState)
114     {
115         ASSERT(isMainThread());
116         if (m_tagId >= TagId::Unknown)
117             return;
118         
119         for (auto& attribute : attributes) {
120             AtomicString attributeName(attribute.name);
121             String attributeValue = StringImpl::create8BitIfPossible(attribute.value);
122             processAttribute(attributeName, attributeValue, document, pictureState);
123         }
124         
125         if (m_tagId == TagId::Source && !pictureState.isEmpty() && !pictureState.last() && m_mediaMatched && m_typeMatched && !m_srcSetAttribute.isEmpty()) {
126             
127             auto sourceSize = SizesAttributeParser(m_sizesAttribute, document).length();
128             ImageCandidate imageCandidate = bestFitSourceForImageAttributes(m_deviceScaleFactor, m_urlToLoad, m_srcSetAttribute, sourceSize);
129             if (!imageCandidate.isEmpty()) {
130                 pictureState.last() = true;
131                 setUrlToLoad(imageCandidate.string.toString(), true);
132             }
133         }
134         
135         // Resolve between src and srcSet if we have them and the tag is img.
136         if (m_tagId == TagId::Img && !m_srcSetAttribute.isEmpty()) {
137             auto sourceSize = SizesAttributeParser(m_sizesAttribute, document).length();
138             ImageCandidate imageCandidate = bestFitSourceForImageAttributes(m_deviceScaleFactor, m_urlToLoad, m_srcSetAttribute, sourceSize);
139             setUrlToLoad(imageCandidate.string.toString(), true);
140         }
141
142         if (m_metaIsViewport && !m_metaContent.isNull())
143             document.processViewport(m_metaContent, ViewportArguments::ViewportMeta);
144     }
145
146     std::unique_ptr<PreloadRequest> createPreloadRequest(const URL& predictedBaseURL)
147     {
148         if (!shouldPreload())
149             return nullptr;
150
151         auto type = resourceType();
152         if (!type)
153             return nullptr;
154
155         if (!LinkLoader::isSupportedType(type.value(), m_typeAttribute))
156             return nullptr;
157
158         auto request = std::make_unique<PreloadRequest>(initiatorFor(m_tagId), m_urlToLoad, predictedBaseURL, type.value(), m_mediaAttribute, m_moduleScript);
159         request->setCrossOriginMode(m_crossOriginMode);
160         request->setNonce(m_nonceAttribute);
161
162         // According to the spec, the module tag ignores the "charset" attribute as the same to the worker's
163         // importScript. But WebKit supports the "charset" for importScript intentionally. So to be consistent,
164         // even for the module tags, we handle the "charset" attribute.
165         request->setCharset(charset());
166         return request;
167     }
168
169     static bool match(const AtomicString& name, const QualifiedName& qName)
170     {
171         ASSERT(isMainThread());
172         return qName.localName() == name;
173     }
174
175 private:
176     void processImageAndScriptAttribute(const AtomicString& attributeName, const String& attributeValue)
177     {
178         if (match(attributeName, srcAttr))
179             setUrlToLoad(attributeValue);
180         else if (match(attributeName, crossoriginAttr))
181             m_crossOriginMode = stripLeadingAndTrailingHTMLSpaces(attributeValue);
182         else if (match(attributeName, charsetAttr))
183             m_charset = attributeValue;
184     }
185
186     void processAttribute(const AtomicString& attributeName, const String& attributeValue, Document& document, const Vector<bool>& pictureState)
187     {
188         bool inPicture = !pictureState.isEmpty();
189         bool alreadyMatchedSource = inPicture && pictureState.last();
190
191         switch (m_tagId) {
192         case TagId::Img:
193             if (inPicture && alreadyMatchedSource)
194                 break;
195             if (match(attributeName, srcsetAttr) && m_srcSetAttribute.isNull()) {
196                 m_srcSetAttribute = attributeValue;
197                 break;
198             }
199             if (match(attributeName, sizesAttr) && m_sizesAttribute.isNull()) {
200                 m_sizesAttribute = attributeValue;
201                 break;
202             }
203             processImageAndScriptAttribute(attributeName, attributeValue);
204             break;
205         case TagId::Source:
206             if (inPicture && alreadyMatchedSource)
207                 break;
208             if (match(attributeName, srcsetAttr) && m_srcSetAttribute.isNull()) {
209                 m_srcSetAttribute = attributeValue;
210                 break;
211             }
212             if (match(attributeName, sizesAttr) && m_sizesAttribute.isNull()) {
213                 m_sizesAttribute = attributeValue;
214                 break;
215             }
216             if (match(attributeName, mediaAttr) && m_mediaAttribute.isNull()) {
217                 m_mediaAttribute = attributeValue;
218                 auto mediaSet = MediaQuerySet::create(attributeValue);
219                 auto documentElement = makeRefPtr(document.documentElement());
220                 LOG(MediaQueries, "HTMLPreloadScanner %p processAttribute evaluating media queries", this);
221                 m_mediaMatched = MediaQueryEvaluator { document.printing() ? "print" : "screen", document, documentElement ? documentElement->computedStyle() : nullptr }.evaluate(mediaSet.get());
222             }
223             if (match(attributeName, typeAttr) && m_typeAttribute.isNull()) {
224                 // when multiple type attributes present: first value wins, ignore subsequent (to match ImageElement parser and Blink behaviours)
225                 m_typeAttribute = attributeValue;
226                 m_typeMatched &= MIMETypeRegistry::isSupportedImageOrSVGMIMEType(m_typeAttribute);
227             }
228             break;
229         case TagId::Script:
230             if (match(attributeName, typeAttr)) {
231                 m_moduleScript = equalLettersIgnoringASCIICase(attributeValue, "module") ? PreloadRequest::ModuleScript::Yes : PreloadRequest::ModuleScript::No;
232                 break;
233             } else if (match(attributeName, nonceAttr))
234                 m_nonceAttribute = attributeValue;
235             processImageAndScriptAttribute(attributeName, attributeValue);
236             break;
237         case TagId::Link:
238             if (match(attributeName, hrefAttr))
239                 setUrlToLoad(attributeValue);
240             else if (match(attributeName, relAttr)) {
241                 LinkRelAttribute parsedAttribute { document, attributeValue };
242                 m_linkIsStyleSheet = relAttributeIsStyleSheet(parsedAttribute);
243                 m_linkIsPreload = parsedAttribute.isLinkPreload;
244             } else if (match(attributeName, mediaAttr))
245                 m_mediaAttribute = attributeValue;
246             else if (match(attributeName, charsetAttr))
247                 m_charset = attributeValue;
248             else if (match(attributeName, crossoriginAttr))
249                 m_crossOriginMode = stripLeadingAndTrailingHTMLSpaces(attributeValue);
250             else if (match(attributeName, nonceAttr))
251                 m_nonceAttribute = attributeValue;
252             else if (match(attributeName, asAttr))
253                 m_asAttribute = attributeValue;
254             else if (match(attributeName, typeAttr))
255                 m_typeAttribute = attributeValue;
256             break;
257         case TagId::Input:
258             if (match(attributeName, srcAttr))
259                 setUrlToLoad(attributeValue);
260             else if (match(attributeName, typeAttr))
261                 m_inputIsImage = equalLettersIgnoringASCIICase(attributeValue, "image");
262             break;
263         case TagId::Meta:
264             if (match(attributeName, contentAttr))
265                 m_metaContent = attributeValue;
266             else if (match(attributeName, nameAttr))
267                 m_metaIsViewport = equalLettersIgnoringASCIICase(attributeValue, "viewport");
268             break;
269         case TagId::Base:
270         case TagId::Style:
271         case TagId::Template:
272         case TagId::Picture:
273         case TagId::Unknown:
274             break;
275         }
276     }
277
278     static bool relAttributeIsStyleSheet(const LinkRelAttribute& parsedAttribute)
279     {
280         return parsedAttribute.isStyleSheet && !parsedAttribute.isAlternate && !parsedAttribute.iconType && !parsedAttribute.isDNSPrefetch;
281     }
282
283     void setUrlToLoad(const String& value, bool allowReplacement = false)
284     {
285         // We only respect the first src/href, per HTML5:
286         // http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#attribute-name-state
287         if (!allowReplacement && !m_urlToLoad.isEmpty())
288             return;
289         String url = stripLeadingAndTrailingHTMLSpaces(value);
290         if (url.isEmpty())
291             return;
292         m_urlToLoad = url;
293     }
294
295     const String& charset() const
296     {
297         return m_charset;
298     }
299
300     std::optional<CachedResource::Type> resourceType() const
301     {
302         switch (m_tagId) {
303         case TagId::Script:
304             return CachedResource::Script;
305         case TagId::Img:
306         case TagId::Input:
307         case TagId::Source:
308             ASSERT(m_tagId != TagId::Input || m_inputIsImage);
309             return CachedResource::ImageResource;
310         case TagId::Link:
311             if (m_linkIsStyleSheet)
312                 return CachedResource::CSSStyleSheet;
313             if (m_linkIsPreload)
314                 return LinkLoader::resourceTypeFromAsAttribute(m_asAttribute);
315             break;
316         case TagId::Meta:
317         case TagId::Unknown:
318         case TagId::Style:
319         case TagId::Base:
320         case TagId::Template:
321         case TagId::Picture:
322             break;
323         }
324         ASSERT_NOT_REACHED();
325         return CachedResource::RawResource;
326     }
327
328     bool shouldPreload()
329     {
330         if (m_urlToLoad.isEmpty())
331             return false;
332
333         if (protocolIs(m_urlToLoad, "data") || protocolIs(m_urlToLoad, "about"))
334             return false;
335
336         if (m_tagId == TagId::Link && !m_linkIsStyleSheet && !m_linkIsPreload)
337             return false;
338
339         if (m_tagId == TagId::Input && !m_inputIsImage)
340             return false;
341
342         return true;
343     }
344
345     TagId m_tagId;
346     String m_urlToLoad;
347     String m_srcSetAttribute;
348     String m_sizesAttribute;
349     bool m_mediaMatched { true };
350     bool m_typeMatched { true };
351     String m_charset;
352     String m_crossOriginMode;
353     bool m_linkIsStyleSheet;
354     bool m_linkIsPreload;
355     String m_mediaAttribute;
356     String m_nonceAttribute;
357     String m_metaContent;
358     String m_asAttribute;
359     String m_typeAttribute;
360     bool m_metaIsViewport;
361     bool m_inputIsImage;
362     float m_deviceScaleFactor;
363     PreloadRequest::ModuleScript m_moduleScript { PreloadRequest::ModuleScript::No };
364 };
365
366 TokenPreloadScanner::TokenPreloadScanner(const URL& documentURL, float deviceScaleFactor)
367     : m_documentURL(documentURL)
368     , m_deviceScaleFactor(deviceScaleFactor)
369 {
370 }
371
372 void TokenPreloadScanner::scan(const HTMLToken& token, Vector<std::unique_ptr<PreloadRequest>>& requests, Document& document)
373 {
374     switch (token.type()) {
375     case HTMLToken::Character:
376         if (!m_inStyle)
377             return;
378         m_cssScanner.scan(token.characters(), requests);
379         return;
380
381     case HTMLToken::EndTag: {
382         TagId tagId = tagIdFor(token.name());
383         if (tagId == TagId::Template) {
384             if (m_templateCount)
385                 --m_templateCount;
386             return;
387         }
388         if (tagId == TagId::Style) {
389             if (m_inStyle)
390                 m_cssScanner.reset();
391             m_inStyle = false;
392         } else if (tagId == TagId::Picture && !m_pictureSourceState.isEmpty())
393             m_pictureSourceState.removeLast();
394
395         return;
396     }
397
398     case HTMLToken::StartTag: {
399         if (m_templateCount)
400             return;
401         TagId tagId = tagIdFor(token.name());
402         if (tagId == TagId::Template) {
403             ++m_templateCount;
404             return;
405         }
406         if (tagId == TagId::Style) {
407             m_inStyle = true;
408             return;
409         }
410         if (tagId == TagId::Base) {
411             // The first <base> element is the one that wins.
412             if (!m_predictedBaseElementURL.isEmpty())
413                 return;
414             updatePredictedBaseURL(token);
415             return;
416         }
417         if (tagId == TagId::Picture) {
418             m_pictureSourceState.append(false);
419             return;
420         }
421
422         StartTagScanner scanner(tagId, m_deviceScaleFactor);
423         scanner.processAttributes(token.attributes(), document, m_pictureSourceState);
424         if (auto request = scanner.createPreloadRequest(m_predictedBaseElementURL))
425             requests.append(WTFMove(request));
426         return;
427     }
428
429     default:
430         return;
431     }
432 }
433
434 void TokenPreloadScanner::updatePredictedBaseURL(const HTMLToken& token)
435 {
436     ASSERT(m_predictedBaseElementURL.isEmpty());
437     if (auto* hrefAttribute = findAttribute(token.attributes(), hrefAttr->localName().string()))
438         m_predictedBaseElementURL = URL(m_documentURL, stripLeadingAndTrailingHTMLSpaces(StringImpl::create8BitIfPossible(hrefAttribute->value))).isolatedCopy();
439 }
440
441 HTMLPreloadScanner::HTMLPreloadScanner(const HTMLParserOptions& options, const URL& documentURL, float deviceScaleFactor)
442     : m_scanner(documentURL, deviceScaleFactor)
443     , m_tokenizer(options)
444 {
445 }
446
447 void HTMLPreloadScanner::appendToEnd(const SegmentedString& source)
448 {
449     m_source.append(source);
450 }
451
452 void HTMLPreloadScanner::scan(HTMLResourcePreloader& preloader, Document& document)
453 {
454     ASSERT(isMainThread()); // HTMLTokenizer::updateStateFor only works on the main thread.
455
456     const URL& startingBaseElementURL = document.baseElementURL();
457
458     // When we start scanning, our best prediction of the baseElementURL is the real one!
459     if (!startingBaseElementURL.isEmpty())
460         m_scanner.setPredictedBaseElementURL(startingBaseElementURL);
461
462     PreloadRequestStream requests;
463
464     while (auto token = m_tokenizer.nextToken(m_source)) {
465         if (token->type() == HTMLToken::StartTag)
466             m_tokenizer.updateStateFor(AtomicString(token->name()));
467         m_scanner.scan(*token, requests, document);
468     }
469
470     preloader.preload(WTFMove(requests));
471 }
472
473 bool testPreloadScannerViewportSupport(Document* document)
474 {
475     ASSERT(document);
476     HTMLParserOptions options(*document);
477     HTMLPreloadScanner scanner(options, document->url());
478     HTMLResourcePreloader preloader(*document);
479     scanner.appendToEnd(String("<meta name=viewport content='width=400'>"));
480     scanner.scan(preloader, *document);
481     return (document->viewportArguments().width == 400);
482 }
483
484 }