StartTagScanner should be thread-safe
[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 "MediaList.h"
38 #include "MediaQueryEvaluator.h"
39 #include <wtf/Functional.h>
40 #include <wtf/MainThread.h>
41
42 namespace WebCore {
43
44 using namespace HTMLNames;
45
46 static bool isStartTag(const HTMLToken& token)
47 {
48     return token.type() == HTMLToken::StartTag;
49 }
50
51 static bool isStartOrEndTag(const HTMLToken& token)
52 {
53     return token.type() == HTMLToken::EndTag || isStartTag(token);
54 }
55
56 enum HTMLTagIdentifier {
57     ImgTagId,
58     InputTagId,
59     LinkTagId,
60     ScriptTagId,
61     UnknownTagId,
62 };
63
64 static HTMLTagIdentifier identifierFor(const AtomicString& tagName)
65 {
66     if (tagName == imgTag)
67         return ImgTagId;
68     if (tagName == inputTag)
69         return InputTagId;
70     if (tagName == linkTag)
71         return LinkTagId;
72     if (tagName == scriptTag)
73         return ScriptTagId;
74     return UnknownTagId;
75 }
76
77 static String inititatorFor(HTMLTagIdentifier tagId)
78 {
79     switch (tagId) {
80     case ImgTagId:
81         return "img";
82     case InputTagId:
83         return "input";
84     case LinkTagId:
85         return "link";
86     case ScriptTagId:
87         return "script";
88     case UnknownTagId:
89         ASSERT_NOT_REACHED();
90         return "unknown";
91     }
92     ASSERT_NOT_REACHED();
93     return "unknown";
94 }
95
96 class StartTagScanner {
97 public:
98     explicit StartTagScanner(HTMLTagIdentifier tagId)
99         : m_tagId(tagId)
100         , m_linkIsStyleSheet(false)
101         , m_linkMediaAttributeIsScreen(true)
102         , m_inputIsImage(false)
103     {
104     }
105
106     void processAttributes(const HTMLToken::AttributeList& attributes)
107     {
108         ASSERT(isMainThread());
109
110         if (m_tagId == UnknownTagId)
111             return;
112
113         for (HTMLToken::AttributeList::const_iterator iter = attributes.begin(); iter != attributes.end(); ++iter) {
114             AtomicString attributeName(iter->name);
115             String attributeValue = StringImpl::create8BitIfPossible(iter->value);
116             processAttribute(attributeName, attributeValue);
117         }
118     }
119
120     PassOwnPtr<PreloadRequest> createPreloadRequest(const KURL& predictedBaseURL)
121     {
122         if (!shouldPreload())
123             return nullptr;
124
125         OwnPtr<PreloadRequest> request = PreloadRequest::create(inititatorFor(m_tagId), m_urlToLoad, predictedBaseURL, resourceType());
126         request->setCrossOriginModeAllowsCookies(crossOriginModeAllowsCookies());
127         request->setCharset(charset());
128         return request.release();
129     }
130
131 private:
132     void processAttribute(const String& attributeName, const String& attributeValue)
133     {
134         if (threadSafeMatch(attributeName, charsetAttr))
135             m_charset = attributeValue;
136
137         if (m_tagId == ScriptTagId || m_tagId == ImgTagId) {
138             if (threadSafeMatch(attributeName, srcAttr))
139                 setUrlToLoad(attributeValue);
140             else if (threadSafeMatch(attributeName, crossoriginAttr) && !attributeValue.isNull())
141                 m_crossOriginMode = stripLeadingAndTrailingHTMLSpaces(attributeValue);
142         } else if (m_tagId == LinkTagId) {
143             if (threadSafeMatch(attributeName, hrefAttr))
144                 setUrlToLoad(attributeValue);
145             else if (threadSafeMatch(attributeName, relAttr))
146                 m_linkIsStyleSheet = relAttributeIsStyleSheet(attributeValue);
147             else if (threadSafeMatch(attributeName, mediaAttr))
148                 m_linkMediaAttributeIsScreen = linkMediaAttributeIsScreen(attributeValue);
149         } else if (m_tagId == InputTagId) {
150             if (threadSafeMatch(attributeName, srcAttr))
151                 setUrlToLoad(attributeValue);
152             else if (threadSafeMatch(attributeName, typeAttr))
153                 m_inputIsImage = equalIgnoringCase(attributeValue, InputTypeNames::image());
154         }
155     }
156
157     static bool relAttributeIsStyleSheet(const String& attributeValue)
158     {
159         LinkRelAttribute rel(attributeValue);
160         return rel.m_isStyleSheet && !rel.m_isAlternate && rel.m_iconType == InvalidIcon && !rel.m_isDNSPrefetch;
161     }
162
163     static bool linkMediaAttributeIsScreen(const String& attributeValue)
164     {
165         if (attributeValue.isEmpty())
166             return true;
167         RefPtr<MediaQuerySet> mediaQueries = MediaQuerySet::createAllowingDescriptionSyntax(attributeValue);
168     
169         // Only preload screen media stylesheets. Used this way, the evaluator evaluates to true for any 
170         // rules containing complex queries (full evaluation is possible but it requires a frame and a style selector which
171         // may be problematic here).
172         MediaQueryEvaluator mediaQueryEvaluator("screen");
173         return mediaQueryEvaluator.eval(mediaQueries.get());
174     }
175
176     void setUrlToLoad(const String& attributeValue)
177     {
178         // We only respect the first src/href, per HTML5:
179         // http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#attribute-name-state
180         if (!m_urlToLoad.isEmpty())
181             return;
182         m_urlToLoad = stripLeadingAndTrailingHTMLSpaces(attributeValue);
183     }
184
185     const String& charset() const
186     {
187         // FIXME: Its not clear that this if is needed, the loader probably ignores charset for image requests anyway.
188         if (m_tagId == ImgTagId)
189             return emptyString();
190         return m_charset;
191     }
192
193     CachedResource::Type resourceType() const
194     {
195         if (m_tagId == ScriptTagId)
196             return CachedResource::Script;
197         if (m_tagId == ImgTagId || (m_tagId == InputTagId && m_inputIsImage))
198             return CachedResource::ImageResource;
199         if (m_tagId == LinkTagId && m_linkIsStyleSheet && m_linkMediaAttributeIsScreen)
200             return CachedResource::CSSStyleSheet;
201         ASSERT_NOT_REACHED();
202         return CachedResource::RawResource;
203     }
204
205     bool shouldPreload()
206     {
207         if (m_urlToLoad.isEmpty())
208             return false;
209
210         if (m_tagId == LinkTagId && (!m_linkIsStyleSheet || !m_linkMediaAttributeIsScreen))
211             return false;
212
213         if (m_tagId == InputTagId && !m_inputIsImage)
214             return false;
215
216         return true;
217     }
218
219     bool crossOriginModeAllowsCookies()
220     {
221         return m_crossOriginMode.isNull() || equalIgnoringCase(m_crossOriginMode, "use-credentials");
222     }
223
224     HTMLTagIdentifier m_tagId;
225     String m_urlToLoad;
226     String m_charset;
227     String m_crossOriginMode;
228     bool m_linkIsStyleSheet;
229     bool m_linkMediaAttributeIsScreen;
230     bool m_inputIsImage;
231 };
232
233 TokenPreloadScanner::TokenPreloadScanner(const KURL& documentURL)
234     : m_documentURL(documentURL)
235     , m_inStyle(false)
236 #if ENABLE(TEMPLATE_ELEMENT)
237     , m_templateCount(0)
238 #endif
239 {
240 }
241
242 TokenPreloadScanner::~TokenPreloadScanner()
243 {
244 }
245
246 #if ENABLE(TEMPLATE_ELEMENT)
247 bool TokenPreloadScanner::processPossibleTemplateTag(const AtomicString& tagName, const HTMLToken& token)
248 {
249     if (isStartOrEndTag(token) && tagName == templateTag) {
250         if (isStartTag(token))
251             m_templateCount++;
252         else
253             m_templateCount--;
254         return true; // Twas our token.
255     }
256     // If we're in a template we "consume" all tokens.
257     return m_templateCount > 0;
258 }
259 #endif
260
261 bool TokenPreloadScanner::processPossibleStyleTag(const AtomicString& tagName, const HTMLToken& token)
262 {
263     ASSERT(isStartOrEndTag(token));
264     if (tagName != styleTag)
265         return false;
266
267     m_inStyle = isStartTag(token);
268
269     if (!m_inStyle)
270         m_cssScanner.reset();
271
272     return true;
273 }
274
275 bool TokenPreloadScanner::processPossibleBaseTag(const AtomicString& tagName, const HTMLToken& token)
276 {
277     ASSERT(isStartTag(token));
278     if (tagName != baseTag)
279         return false;
280
281     // The first <base> element is the one that wins.
282     if (!m_predictedBaseElementURL.isEmpty())
283         return true;
284
285     for (HTMLToken::AttributeList::const_iterator iter = token.attributes().begin(); iter != token.attributes().end(); ++iter) {
286         AtomicString attributeName(iter->name);
287         if (attributeName == hrefAttr) {
288             String hrefValue = StringImpl::create8BitIfPossible(iter->value);
289             m_predictedBaseElementURL = KURL(m_documentURL, stripLeadingAndTrailingHTMLSpaces(hrefValue));
290             break;
291         }
292     }
293     return true;
294 }
295
296 void TokenPreloadScanner::scan(const HTMLToken& token, Vector<OwnPtr<PreloadRequest> >& requests)
297 {
298     // <style> is the only place we search for urls in non-start/end-tag tokens.
299     if (m_inStyle) {
300         if (token.type() != HTMLToken::Character)
301             return;
302         const HTMLToken::DataVector& characters = token.characters();
303         return m_cssScanner.scan(characters.begin(), characters.end(), requests);
304     }
305
306     if (!isStartOrEndTag(token))
307         return;
308
309     AtomicString tagName(token.name());
310 #if ENABLE(TEMPLATE_ELEMENT)
311     if (processPossibleTemplateTag(tagName, token))
312         return;
313 #endif
314     if (processPossibleStyleTag(tagName, token))
315         return;
316     if (!isStartTag(token))
317         return;
318     if (processPossibleBaseTag(tagName, token))
319         return;
320
321     StartTagScanner scanner(identifierFor(tagName));
322     scanner.processAttributes(token.attributes());
323     OwnPtr<PreloadRequest> request =  scanner.createPreloadRequest(m_predictedBaseElementURL);
324     if (request)
325         requests.append(request.release());
326 }
327
328 HTMLPreloadScanner::HTMLPreloadScanner(const HTMLParserOptions& options, const KURL& documentURL)
329     : m_scanner(documentURL)
330     , m_tokenizer(HTMLTokenizer::create(options))
331 {
332 }
333
334 HTMLPreloadScanner::~HTMLPreloadScanner()
335 {
336 }
337
338 void HTMLPreloadScanner::appendToEnd(const SegmentedString& source)
339 {
340     m_source.append(source);
341 }
342
343 void HTMLPreloadScanner::scan(HTMLResourcePreloader* preloader, const KURL& startingBaseElementURL)
344 {
345     ASSERT(isMainThread()); // HTMLTokenizer::updateStateFor only works on the main thread.
346
347     // When we start scanning, our best prediction of the baseElementURL is the real one!
348     if (!startingBaseElementURL.isEmpty())
349         m_scanner.setPredictedBaseElementURL(startingBaseElementURL);
350
351     Vector<OwnPtr<PreloadRequest> > requests;
352     while (m_tokenizer->nextToken(m_source, m_token)) {
353         if (isStartTag(m_token))
354             m_tokenizer->updateStateFor(AtomicString(m_token.name()));
355         m_scanner.scan(m_token, requests);
356         m_token.clear();
357     }
358     for (size_t i = 0; i < requests.size(); i++)
359         preloader->preload(requests[i].release());
360 }
361
362 }