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