1/*
2 * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
3 *           (C) 1999 Antti Koivisto (koivisto@kde.org)
4 *           (C) 2001 Dirk Mueller (mueller@kde.org)
5 * Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008 Apple Inc. All rights reserved.
6 * Copyright (C) 2008 Nikolas Zimmermann <zimmermann@kde.org>
7 *
8 * This library is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Library General Public
10 * License as published by the Free Software Foundation; either
11 * version 2 of the License, or (at your option) any later version.
12 *
13 * This library is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 * Library General Public License for more details.
17 *
18 * You should have received a copy of the GNU Library General Public License
19 * along with this library; see the file COPYING.LIB.  If not, write to
20 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301, USA.
22 */
23
24#include "config.h"
25#include "core/dom/ScriptLoader.h"
26
27#include "bindings/core/v8/ScriptController.h"
28#include "bindings/core/v8/ScriptSourceCode.h"
29#include "core/HTMLNames.h"
30#include "core/SVGNames.h"
31#include "core/dom/Document.h"
32#include "core/events/Event.h"
33#include "core/dom/IgnoreDestructiveWriteCountIncrementer.h"
34#include "core/dom/ScriptLoaderClient.h"
35#include "core/dom/ScriptRunner.h"
36#include "core/dom/ScriptableDocumentParser.h"
37#include "core/dom/Text.h"
38#include "core/fetch/FetchRequest.h"
39#include "core/fetch/ResourceFetcher.h"
40#include "core/fetch/ScriptResource.h"
41#include "core/html/HTMLScriptElement.h"
42#include "core/html/imports/HTMLImport.h"
43#include "core/html/parser/HTMLParserIdioms.h"
44#include "core/frame/LocalFrame.h"
45#include "core/frame/SubresourceIntegrity.h"
46#include "core/frame/csp/ContentSecurityPolicy.h"
47#include "core/inspector/ConsoleMessage.h"
48#include "core/svg/SVGScriptElement.h"
49#include "platform/MIMETypeRegistry.h"
50#include "platform/weborigin/SecurityOrigin.h"
51#include "wtf/StdLibExtras.h"
52#include "wtf/text/StringBuilder.h"
53#include "wtf/text/StringHash.h"
54
55namespace blink {
56
57ScriptLoader::ScriptLoader(Element* element, bool parserInserted, bool alreadyStarted)
58    : m_element(element)
59    , m_resource(0)
60    , m_startLineNumber(WTF::OrdinalNumber::beforeFirst())
61    , m_parserInserted(parserInserted)
62    , m_isExternalScript(false)
63    , m_alreadyStarted(alreadyStarted)
64    , m_haveFiredLoad(false)
65    , m_willBeParserExecuted(false)
66    , m_readyToBeParserExecuted(false)
67    , m_willExecuteWhenDocumentFinishedParsing(false)
68    , m_forceAsync(!parserInserted)
69    , m_willExecuteInOrder(false)
70{
71    ASSERT(m_element);
72    if (parserInserted && element->document().scriptableDocumentParser() && !element->document().isInDocumentWrite())
73        m_startLineNumber = element->document().scriptableDocumentParser()->lineNumber();
74}
75
76ScriptLoader::~ScriptLoader()
77{
78    stopLoadRequest();
79}
80
81void ScriptLoader::didNotifySubtreeInsertionsToDocument()
82{
83    if (!m_parserInserted)
84        prepareScript(); // FIXME: Provide a real starting line number here.
85}
86
87void ScriptLoader::childrenChanged()
88{
89    if (!m_parserInserted && m_element->inDocument())
90        prepareScript(); // FIXME: Provide a real starting line number here.
91}
92
93void ScriptLoader::handleSourceAttribute(const String& sourceUrl)
94{
95    if (ignoresLoadRequest() || sourceUrl.isEmpty())
96        return;
97
98    prepareScript(); // FIXME: Provide a real starting line number here.
99}
100
101void ScriptLoader::handleAsyncAttribute()
102{
103    m_forceAsync = false;
104}
105
106// Helper function
107static bool isLegacySupportedJavaScriptLanguage(const String& language)
108{
109    // Mozilla 1.8 accepts javascript1.0 - javascript1.7, but WinIE 7 accepts only javascript1.1 - javascript1.3.
110    // Mozilla 1.8 and WinIE 7 both accept javascript and livescript.
111    // WinIE 7 accepts ecmascript and jscript, but Mozilla 1.8 doesn't.
112    // Neither Mozilla 1.8 nor WinIE 7 accept leading or trailing whitespace.
113    // We want to accept all the values that either of these browsers accept, but not other values.
114
115    // FIXME: This function is not HTML5 compliant. These belong in the MIME registry as "text/javascript<version>" entries.
116    typedef HashSet<String, CaseFoldingHash> LanguageSet;
117    DEFINE_STATIC_LOCAL(LanguageSet, languages, ());
118    if (languages.isEmpty()) {
119        languages.add("javascript");
120        languages.add("javascript1.0");
121        languages.add("javascript1.1");
122        languages.add("javascript1.2");
123        languages.add("javascript1.3");
124        languages.add("javascript1.4");
125        languages.add("javascript1.5");
126        languages.add("javascript1.6");
127        languages.add("javascript1.7");
128        languages.add("livescript");
129        languages.add("ecmascript");
130        languages.add("jscript");
131    }
132
133    return languages.contains(language);
134}
135
136void ScriptLoader::dispatchErrorEvent()
137{
138    m_element->dispatchEvent(Event::create(EventTypeNames::error));
139}
140
141void ScriptLoader::dispatchLoadEvent()
142{
143    if (ScriptLoaderClient* client = this->client())
144        client->dispatchLoadEvent();
145    setHaveFiredLoadEvent(true);
146}
147
148bool ScriptLoader::isScriptTypeSupported(LegacyTypeSupport supportLegacyTypes) const
149{
150    // FIXME: isLegacySupportedJavaScriptLanguage() is not valid HTML5. It is used here to maintain backwards compatibility with existing layout tests. The specific violations are:
151    // - Allowing type=javascript. type= should only support MIME types, such as text/javascript.
152    // - Allowing a different set of languages for language= and type=. language= supports Javascript 1.1 and 1.4-1.6, but type= does not.
153
154    String type = client()->typeAttributeValue();
155    String language = client()->languageAttributeValue();
156    if (type.isEmpty() && language.isEmpty())
157        return true; // Assume text/javascript.
158    if (type.isEmpty()) {
159        type = "text/" + language.lower();
160        if (MIMETypeRegistry::isSupportedJavaScriptMIMEType(type) || isLegacySupportedJavaScriptLanguage(language))
161            return true;
162    } else if (MIMETypeRegistry::isSupportedJavaScriptMIMEType(type.stripWhiteSpace()) || (supportLegacyTypes == AllowLegacyTypeInTypeAttribute && isLegacySupportedJavaScriptLanguage(type))) {
163        return true;
164    }
165
166    return false;
167}
168
169// http://dev.w3.org/html5/spec/Overview.html#prepare-a-script
170bool ScriptLoader::prepareScript(const TextPosition& scriptStartPosition, LegacyTypeSupport supportLegacyTypes)
171{
172    if (m_alreadyStarted)
173        return false;
174
175    ScriptLoaderClient* client = this->client();
176
177    bool wasParserInserted;
178    if (m_parserInserted) {
179        wasParserInserted = true;
180        m_parserInserted = false;
181    } else {
182        wasParserInserted = false;
183    }
184
185    if (wasParserInserted && !client->asyncAttributeValue())
186        m_forceAsync = true;
187
188    // FIXME: HTML5 spec says we should check that all children are either comments or empty text nodes.
189    if (!client->hasSourceAttribute() && !m_element->hasChildren())
190        return false;
191
192    if (!m_element->inDocument())
193        return false;
194
195    if (!isScriptTypeSupported(supportLegacyTypes))
196        return false;
197
198    if (wasParserInserted) {
199        m_parserInserted = true;
200        m_forceAsync = false;
201    }
202
203    m_alreadyStarted = true;
204
205    // FIXME: If script is parser inserted, verify it's still in the original document.
206    Document& elementDocument = m_element->document();
207    Document* contextDocument = elementDocument.contextDocument().get();
208
209    if (!contextDocument || !contextDocument->allowExecutingScripts(m_element))
210        return false;
211
212    if (!isScriptForEventSupported())
213        return false;
214
215    if (!client->charsetAttributeValue().isEmpty())
216        m_characterEncoding = client->charsetAttributeValue();
217    else
218        m_characterEncoding = elementDocument.charset();
219
220    if (client->hasSourceAttribute()) {
221        FetchRequest::DeferOption defer = FetchRequest::NoDefer;
222        if (!m_parserInserted || client->asyncAttributeValue() || client->deferAttributeValue())
223            defer = FetchRequest::LazyLoad;
224        if (!fetchScript(client->sourceAttributeValue(), defer))
225            return false;
226    }
227
228    if (client->hasSourceAttribute() && client->deferAttributeValue() && m_parserInserted && !client->asyncAttributeValue()) {
229        m_willExecuteWhenDocumentFinishedParsing = true;
230        m_willBeParserExecuted = true;
231    } else if (client->hasSourceAttribute() && m_parserInserted && !client->asyncAttributeValue()) {
232        m_willBeParserExecuted = true;
233    } else if (!client->hasSourceAttribute() && m_parserInserted && !elementDocument.isRenderingReady()) {
234        m_willBeParserExecuted = true;
235        m_readyToBeParserExecuted = true;
236    } else if (client->hasSourceAttribute() && !client->asyncAttributeValue() && !m_forceAsync) {
237        m_willExecuteInOrder = true;
238        contextDocument->scriptRunner()->queueScriptForExecution(this, m_resource, ScriptRunner::IN_ORDER_EXECUTION);
239        m_resource->addClient(this);
240    } else if (client->hasSourceAttribute()) {
241        contextDocument->scriptRunner()->queueScriptForExecution(this, m_resource, ScriptRunner::ASYNC_EXECUTION);
242        m_resource->addClient(this);
243    } else {
244        // Reset line numbering for nested writes.
245        TextPosition position = elementDocument.isInDocumentWrite() ? TextPosition() : scriptStartPosition;
246        KURL scriptURL = (!elementDocument.isInDocumentWrite() && m_parserInserted) ? elementDocument.url() : KURL();
247        executeScript(ScriptSourceCode(scriptContent(), scriptURL, position));
248    }
249
250    return true;
251}
252
253bool ScriptLoader::fetchScript(const String& sourceUrl, FetchRequest::DeferOption defer)
254{
255    ASSERT(m_element);
256
257    RefPtrWillBeRawPtr<Document> elementDocument(m_element->document());
258    if (!m_element->inDocument() || m_element->document() != elementDocument)
259        return false;
260
261    ASSERT(!m_resource);
262    if (!stripLeadingAndTrailingHTMLSpaces(sourceUrl).isEmpty()) {
263        FetchRequest request(ResourceRequest(elementDocument->completeURL(sourceUrl)), m_element->localName());
264
265        AtomicString crossOriginMode = m_element->fastGetAttribute(HTMLNames::crossoriginAttr);
266        if (!crossOriginMode.isNull())
267            request.setCrossOriginAccessControl(elementDocument->securityOrigin(), crossOriginMode);
268        request.setCharset(scriptCharset());
269
270        bool scriptPassesCSP = elementDocument->contentSecurityPolicy()->allowScriptWithNonce(m_element->fastGetAttribute(HTMLNames::nonceAttr));
271        if (scriptPassesCSP)
272            request.setContentSecurityCheck(DoNotCheckContentSecurityPolicy);
273        request.setDefer(defer);
274
275        m_resource = elementDocument->fetcher()->fetchScript(request);
276        m_isExternalScript = true;
277    }
278
279    if (m_resource)
280        return true;
281
282    dispatchErrorEvent();
283    return false;
284}
285
286bool isHTMLScriptLoader(Element* element)
287{
288    ASSERT(element);
289    return isHTMLScriptElement(*element);
290}
291
292bool isSVGScriptLoader(Element* element)
293{
294    ASSERT(element);
295    return isSVGScriptElement(*element);
296}
297
298void ScriptLoader::executeScript(const ScriptSourceCode& sourceCode, double* compilationFinishTime)
299{
300    ASSERT(m_alreadyStarted);
301
302    if (sourceCode.isEmpty())
303        return;
304
305    RefPtrWillBeRawPtr<Document> elementDocument(m_element->document());
306    RefPtrWillBeRawPtr<Document> contextDocument = elementDocument->contextDocument().get();
307    if (!contextDocument)
308        return;
309
310    LocalFrame* frame = contextDocument->frame();
311
312    const ContentSecurityPolicy* csp = elementDocument->contentSecurityPolicy();
313    bool shouldBypassMainWorldCSP = (frame && frame->script().shouldBypassMainWorldCSP())
314        || csp->allowScriptWithNonce(m_element->fastGetAttribute(HTMLNames::nonceAttr))
315        || csp->allowScriptWithHash(sourceCode.source());
316
317    if (!m_isExternalScript && (!shouldBypassMainWorldCSP && !csp->allowInlineScript(elementDocument->url(), m_startLineNumber)))
318        return;
319
320    if (m_isExternalScript) {
321        ScriptResource* resource = m_resource ? m_resource.get() : sourceCode.resource();
322        if (resource && !resource->mimeTypeAllowedByNosniff()) {
323            contextDocument->addConsoleMessage(ConsoleMessage::create(SecurityMessageSource, ErrorMessageLevel, "Refused to execute script from '" + resource->url().elidedString() + "' because its MIME type ('" + resource->mimeType() + "') is not executable, and strict MIME type checking is enabled."));
324            return;
325        }
326
327        // FIXME: On failure, SRI should probably provide an error message for the console.
328        if (!SubresourceIntegrity::CheckSubresourceIntegrity(*m_element, sourceCode.source(), sourceCode.resource()->url()))
329            return;
330    }
331
332    // FIXME: Can this be moved earlier in the function?
333    // Why are we ever attempting to execute scripts without a frame?
334    if (!frame)
335        return;
336
337    const bool isImportedScript = contextDocument != elementDocument;
338    // http://www.whatwg.org/specs/web-apps/current-work/#execute-the-script-block step 2.3
339    // with additional support for HTML imports.
340    IgnoreDestructiveWriteCountIncrementer ignoreDestructiveWriteCountIncrementer(m_isExternalScript || isImportedScript ? contextDocument.get() : 0);
341
342    if (isHTMLScriptLoader(m_element))
343        contextDocument->pushCurrentScript(toHTMLScriptElement(m_element));
344
345    AccessControlStatus corsCheck = NotSharableCrossOrigin;
346    if (!m_isExternalScript || (sourceCode.resource() && sourceCode.resource()->passesAccessControlCheck(m_element->document().securityOrigin())))
347        corsCheck = SharableCrossOrigin;
348
349    // Create a script from the script element node, using the script
350    // block's source and the script block's type.
351    // Note: This is where the script is compiled and actually executed.
352    frame->script().executeScriptInMainWorld(sourceCode, corsCheck, compilationFinishTime);
353
354    if (isHTMLScriptLoader(m_element)) {
355        ASSERT(contextDocument->currentScript() == m_element);
356        contextDocument->popCurrentScript();
357    }
358}
359
360void ScriptLoader::stopLoadRequest()
361{
362    if (m_resource) {
363        if (!m_willBeParserExecuted)
364            m_resource->removeClient(this);
365        m_resource = 0;
366    }
367}
368
369void ScriptLoader::execute(ScriptResource* resource)
370{
371    ASSERT(!m_willBeParserExecuted);
372    ASSERT(resource);
373    if (resource->errorOccurred()) {
374        dispatchErrorEvent();
375    } else if (!resource->wasCanceled()) {
376        executeScript(ScriptSourceCode(resource));
377        dispatchLoadEvent();
378    }
379    resource->removeClient(this);
380}
381
382void ScriptLoader::notifyFinished(Resource* resource)
383{
384    ASSERT(!m_willBeParserExecuted);
385
386    RefPtrWillBeRawPtr<Document> elementDocument(m_element->document());
387    RefPtrWillBeRawPtr<Document> contextDocument = elementDocument->contextDocument().get();
388    if (!contextDocument)
389        return;
390
391    // Resource possibly invokes this notifyFinished() more than
392    // once because ScriptLoader doesn't unsubscribe itself from
393    // Resource here and does it in execute() instead.
394    // We use m_resource to check if this function is already called.
395    ASSERT_UNUSED(resource, resource == m_resource);
396    if (!m_resource)
397        return;
398    if (m_resource->errorOccurred()) {
399        dispatchErrorEvent();
400        contextDocument->scriptRunner()->notifyScriptLoadError(this, m_willExecuteInOrder ? ScriptRunner::IN_ORDER_EXECUTION : ScriptRunner::ASYNC_EXECUTION);
401        return;
402    }
403    if (m_willExecuteInOrder)
404        contextDocument->scriptRunner()->notifyScriptReady(this, ScriptRunner::IN_ORDER_EXECUTION);
405    else
406        contextDocument->scriptRunner()->notifyScriptReady(this, ScriptRunner::ASYNC_EXECUTION);
407
408    m_resource = 0;
409}
410
411bool ScriptLoader::ignoresLoadRequest() const
412{
413    return m_alreadyStarted || m_isExternalScript || m_parserInserted || !element() || !element()->inDocument();
414}
415
416bool ScriptLoader::isScriptForEventSupported() const
417{
418    String eventAttribute = client()->eventAttributeValue();
419    String forAttribute = client()->forAttributeValue();
420    if (!eventAttribute.isEmpty() && !forAttribute.isEmpty()) {
421        forAttribute = forAttribute.stripWhiteSpace();
422        if (!equalIgnoringCase(forAttribute, "window"))
423            return false;
424
425        eventAttribute = eventAttribute.stripWhiteSpace();
426        if (!equalIgnoringCase(eventAttribute, "onload") && !equalIgnoringCase(eventAttribute, "onload()"))
427            return false;
428    }
429    return true;
430}
431
432String ScriptLoader::scriptContent() const
433{
434    return m_element->textFromChildren();
435}
436
437ScriptLoaderClient* ScriptLoader::client() const
438{
439    if (isHTMLScriptLoader(m_element))
440        return toHTMLScriptElement(m_element);
441
442    if (isSVGScriptLoader(m_element))
443        return toSVGScriptElement(m_element);
444
445    ASSERT_NOT_REACHED();
446    return 0;
447}
448
449ScriptLoader* toScriptLoaderIfPossible(Element* element)
450{
451    if (isHTMLScriptLoader(element))
452        return toHTMLScriptElement(element)->loader();
453
454    if (isSVGScriptLoader(element))
455        return toSVGScriptElement(element)->loader();
456
457    return 0;
458}
459
460}
461