PreloadScanner preloads external CSS with non-matching media attribute
[WebKit-https.git] / Source / WebCore / html / parser / HTMLPreloadScanner.cpp
1 /*
2  * Copyright (C) 2008 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 "HTMLParserOptions.h"
34 #include "HTMLTokenizer.h"
35 #include "InputTypeNames.h"
36 #include "LinkRelAttribute.h"
37 #include <wtf/MainThread.h>
38
39 namespace WebCore {
40
41 using namespace HTMLNames;
42
43 TokenPreloadScanner::TagId TokenPreloadScanner::tagIdFor(const HTMLToken::DataVector& data)
44 {
45     AtomicString tagName(data);
46     if (tagName == imgTag)
47         return ImgTagId;
48     if (tagName == inputTag)
49         return InputTagId;
50     if (tagName == linkTag)
51         return LinkTagId;
52     if (tagName == scriptTag)
53         return ScriptTagId;
54     if (tagName == styleTag)
55         return StyleTagId;
56     if (tagName == baseTag)
57         return BaseTagId;
58     if (tagName == templateTag)
59         return TemplateTagId;
60     return UnknownTagId;
61 }
62
63 #if ENABLE(THREADED_HTML_PARSER)
64 TokenPreloadScanner::TagId TokenPreloadScanner::tagIdFor(const HTMLIdentifier& tagName)
65 {
66     if (threadSafeHTMLNamesMatch(tagName, imgTag))
67         return ImgTagId;
68     if (threadSafeHTMLNamesMatch(tagName, inputTag))
69         return InputTagId;
70     if (threadSafeHTMLNamesMatch(tagName, linkTag))
71         return LinkTagId;
72     if (threadSafeHTMLNamesMatch(tagName, scriptTag))
73         return ScriptTagId;
74     if (threadSafeHTMLNamesMatch(tagName, styleTag))
75         return StyleTagId;
76     if (threadSafeHTMLNamesMatch(tagName, baseTag))
77         return BaseTagId;
78     if (threadSafeHTMLNamesMatch(tagName, templateTag))
79         return TemplateTagId;
80     return UnknownTagId;
81 }
82 #endif
83
84 String TokenPreloadScanner::initiatorFor(TagId tagId)
85 {
86     switch (tagId) {
87     case ImgTagId:
88         return "img";
89     case InputTagId:
90         return "input";
91     case LinkTagId:
92         return "link";
93     case ScriptTagId:
94         return "script";
95     case UnknownTagId:
96     case StyleTagId:
97     case BaseTagId:
98     case TemplateTagId:
99         ASSERT_NOT_REACHED();
100         return "unknown";
101     }
102     ASSERT_NOT_REACHED();
103     return "unknown";
104 }
105
106 class TokenPreloadScanner::StartTagScanner {
107 public:
108     explicit StartTagScanner(TagId tagId)
109         : m_tagId(tagId)
110         , m_linkIsStyleSheet(false)
111         , m_inputIsImage(false)
112     {
113     }
114
115     void processAttributes(const HTMLToken::AttributeList& attributes)
116     {
117         ASSERT(isMainThread());
118         if (m_tagId >= UnknownTagId)
119             return;
120         for (HTMLToken::AttributeList::const_iterator iter = attributes.begin(); iter != attributes.end(); ++iter) {
121             AtomicString attributeName(iter->name);
122             String attributeValue = StringImpl::create8BitIfPossible(iter->value);
123             processAttribute(attributeName, attributeValue);
124         }
125     }
126
127 #if ENABLE(THREADED_HTML_PARSER)
128     void processAttributes(const Vector<CompactHTMLToken::Attribute>& attributes)
129     {
130         if (m_tagId >= UnknownTagId)
131             return;
132         for (Vector<CompactHTMLToken::Attribute>::const_iterator iter = attributes.begin(); iter != attributes.end(); ++iter)
133             processAttribute(iter->name, iter->value);
134     }
135 #endif
136
137     PassOwnPtr<PreloadRequest> createPreloadRequest(const KURL& predictedBaseURL)
138     {
139         if (!shouldPreload())
140             return nullptr;
141
142         OwnPtr<PreloadRequest> request = PreloadRequest::create(initiatorFor(m_tagId), m_urlToLoad, predictedBaseURL, resourceType(), m_mediaAttribute);
143         request->setCrossOriginModeAllowsCookies(crossOriginModeAllowsCookies());
144         request->setCharset(charset());
145         return request.release();
146     }
147
148 static bool match(const AtomicString& name, const QualifiedName& qName)
149 {
150     ASSERT(isMainThread());
151     return qName.localName() == name;
152 }
153
154 #if ENABLE(THREADED_HTML_PARSER)
155 static bool match(const HTMLIdentifier& name, const QualifiedName& qName)
156 {
157     return threadSafeHTMLNamesMatch(name, qName);
158 }
159 #endif
160
161 private:
162     template<typename NameType>
163     void processAttribute(const NameType& attributeName, const String& attributeValue)
164     {
165         if (match(attributeName, charsetAttr))
166             m_charset = attributeValue;
167
168         if (m_tagId == ScriptTagId || m_tagId == ImgTagId) {
169             if (match(attributeName, srcAttr))
170                 setUrlToLoad(attributeValue);
171             else if (match(attributeName, crossoriginAttr) && !attributeValue.isNull())
172                 m_crossOriginMode = stripLeadingAndTrailingHTMLSpaces(attributeValue);
173         } else if (m_tagId == LinkTagId) {
174             if (match(attributeName, hrefAttr))
175                 setUrlToLoad(attributeValue);
176             else if (match(attributeName, relAttr))
177                 m_linkIsStyleSheet = relAttributeIsStyleSheet(attributeValue);
178             else if (match(attributeName, mediaAttr))
179                 m_mediaAttribute = attributeValue;
180         } else if (m_tagId == InputTagId) {
181             if (match(attributeName, srcAttr))
182                 setUrlToLoad(attributeValue);
183             else if (match(attributeName, typeAttr))
184                 m_inputIsImage = equalIgnoringCase(attributeValue, InputTypeNames::image());
185         }
186     }
187
188     static bool relAttributeIsStyleSheet(const String& attributeValue)
189     {
190         LinkRelAttribute rel(attributeValue);
191         return rel.m_isStyleSheet && !rel.m_isAlternate && rel.m_iconType == InvalidIcon && !rel.m_isDNSPrefetch;
192     }
193
194     void setUrlToLoad(const String& attributeValue)
195     {
196         // We only respect the first src/href, per HTML5:
197         // http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#attribute-name-state
198         if (!m_urlToLoad.isEmpty())
199             return;
200         m_urlToLoad = stripLeadingAndTrailingHTMLSpaces(attributeValue);
201     }
202
203     const String& charset() const
204     {
205         // FIXME: Its not clear that this if is needed, the loader probably ignores charset for image requests anyway.
206         if (m_tagId == ImgTagId)
207             return emptyString();
208         return m_charset;
209     }
210
211     CachedResource::Type resourceType() const
212     {
213         if (m_tagId == ScriptTagId)
214             return CachedResource::Script;
215         if (m_tagId == ImgTagId || (m_tagId == InputTagId && m_inputIsImage))
216             return CachedResource::ImageResource;
217         if (m_tagId == LinkTagId && m_linkIsStyleSheet)
218             return CachedResource::CSSStyleSheet;
219         ASSERT_NOT_REACHED();
220         return CachedResource::RawResource;
221     }
222
223     bool shouldPreload()
224     {
225         if (m_urlToLoad.isEmpty())
226             return false;
227
228         if (m_tagId == LinkTagId && !m_linkIsStyleSheet)
229             return false;
230
231         if (m_tagId == InputTagId && !m_inputIsImage)
232             return false;
233
234         return true;
235     }
236
237     bool crossOriginModeAllowsCookies()
238     {
239         return m_crossOriginMode.isNull() || equalIgnoringCase(m_crossOriginMode, "use-credentials");
240     }
241
242     TagId m_tagId;
243     String m_urlToLoad;
244     String m_charset;
245     String m_crossOriginMode;
246     bool m_linkIsStyleSheet;
247     String m_mediaAttribute;
248     bool m_inputIsImage;
249 };
250
251 TokenPreloadScanner::TokenPreloadScanner(const KURL& documentURL)
252     : m_documentURL(documentURL)
253     , m_inStyle(false)
254 #if ENABLE(TEMPLATE_ELEMENT)
255     , m_templateCount(0)
256 #endif
257 {
258 }
259
260 TokenPreloadScanner::~TokenPreloadScanner()
261 {
262 }
263
264 TokenPreloadScannerCheckpoint TokenPreloadScanner::createCheckpoint()
265 {
266     TokenPreloadScannerCheckpoint checkpoint = m_checkpoints.size();
267     m_checkpoints.append(Checkpoint(m_predictedBaseElementURL, m_inStyle
268 #if ENABLE(TEMPLATE_ELEMENT)
269                                     , m_templateCount
270 #endif
271                                     ));
272     return checkpoint;
273 }
274
275 void TokenPreloadScanner::rewindTo(TokenPreloadScannerCheckpoint checkpointIndex)
276 {
277     ASSERT(checkpointIndex < m_checkpoints.size()); // If this ASSERT fires, checkpointIndex is invalid.
278     const Checkpoint& checkpoint = m_checkpoints[checkpointIndex];
279     m_predictedBaseElementURL = checkpoint.predictedBaseElementURL;
280     m_inStyle = checkpoint.inStyle;
281 #if ENABLE(TEMPLATE_ELEMENT)
282     m_templateCount = checkpoint.templateCount;
283 #endif
284     m_cssScanner.reset();
285     m_checkpoints.clear();
286 }
287
288 void TokenPreloadScanner::scan(const HTMLToken& token, Vector<OwnPtr<PreloadRequest> >& requests)
289 {
290     scanCommon(token, requests);
291 }
292
293 #if ENABLE(THREADED_HTML_PARSER)
294 void TokenPreloadScanner::scan(const CompactHTMLToken& token, Vector<OwnPtr<PreloadRequest> >& requests)
295 {
296     scanCommon(token, requests);
297 }
298 #endif
299
300 template<typename Token>
301 void TokenPreloadScanner::scanCommon(const Token& token, Vector<OwnPtr<PreloadRequest> >& requests)
302 {
303     switch (token.type()) {
304     case HTMLToken::Character: {
305         if (!m_inStyle)
306             return;
307         m_cssScanner.scan(token.data(), requests);
308         return;
309     }
310     case HTMLToken::EndTag: {
311         TagId tagId = tagIdFor(token.data());
312 #if ENABLE(TEMPLATE_ELEMENT)
313         if (tagId == TemplateTagId) {
314             if (m_templateCount)
315                 --m_templateCount;
316             return;
317         }
318 #endif
319         if (tagId == StyleTagId) {
320             if (m_inStyle)
321                 m_cssScanner.reset();
322             m_inStyle = false;
323         }
324         return;
325     }
326     case HTMLToken::StartTag: {
327 #if ENABLE(TEMPLATE_ELEMENT)
328         if (m_templateCount)
329             return;
330 #endif
331         TagId tagId = tagIdFor(token.data());
332 #if ENABLE(TEMPLATE_ELEMENT)
333         if (tagId == TemplateTagId) {
334             ++m_templateCount;
335             return;
336         }
337 #endif
338         if (tagId == StyleTagId) {
339             m_inStyle = true;
340             return;
341         }
342         if (tagId == BaseTagId) {
343             // The first <base> element is the one that wins.
344             if (!m_predictedBaseElementURL.isEmpty())
345                 return;
346             updatePredictedBaseURL(token);
347             return;
348         }
349
350         StartTagScanner scanner(tagId);
351         scanner.processAttributes(token.attributes());
352         OwnPtr<PreloadRequest> request = scanner.createPreloadRequest(m_predictedBaseElementURL);
353         if (request)
354             requests.append(request.release());
355         return;
356     }
357     default: {
358         return;
359     }
360     }
361 }
362
363 template<typename Token>
364 void TokenPreloadScanner::updatePredictedBaseURL(const Token& token)
365 {
366     ASSERT(m_predictedBaseElementURL.isEmpty());
367     if (const typename Token::Attribute* hrefAttribute = token.getAttributeItem(hrefAttr))
368         m_predictedBaseElementURL = KURL(m_documentURL, stripLeadingAndTrailingHTMLSpaces(hrefAttribute->value)).copy();
369 }
370
371 HTMLPreloadScanner::HTMLPreloadScanner(const HTMLParserOptions& options, const KURL& documentURL)
372     : m_scanner(documentURL)
373     , m_tokenizer(HTMLTokenizer::create(options))
374 {
375 }
376
377 HTMLPreloadScanner::~HTMLPreloadScanner()
378 {
379 }
380
381 void HTMLPreloadScanner::appendToEnd(const SegmentedString& source)
382 {
383     m_source.append(source);
384 }
385
386 void HTMLPreloadScanner::scan(HTMLResourcePreloader* preloader, const KURL& startingBaseElementURL)
387 {
388     ASSERT(isMainThread()); // HTMLTokenizer::updateStateFor only works on the main thread.
389
390     // When we start scanning, our best prediction of the baseElementURL is the real one!
391     if (!startingBaseElementURL.isEmpty())
392         m_scanner.setPredictedBaseElementURL(startingBaseElementURL);
393
394     PreloadRequestStream requests;
395
396     while (m_tokenizer->nextToken(m_source, m_token)) {
397         if (m_token.type() == HTMLToken::StartTag)
398             m_tokenizer->updateStateFor(AtomicString(m_token.name()));
399         m_scanner.scan(m_token, requests);
400         m_token.clear();
401     }
402
403     preloader->takeAndPreload(requests);
404 }
405
406 }