63f593fd74c499557f59aa53a8d3404ffbfd3efe
[WebKit-https.git] / Source / WebCore / dom / ScriptElement.cpp
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 "ScriptElement.h"
26
27 #include "CachedResourceLoader.h"
28 #include "CachedResourceRequest.h"
29 #include "CachedScript.h"
30 #include "ContentSecurityPolicy.h"
31 #include "CrossOriginAccessControl.h"
32 #include "CurrentScriptIncrementer.h"
33 #include "Event.h"
34 #include "Frame.h"
35 #include "FrameLoader.h"
36 #include "HTMLNames.h"
37 #include "HTMLParserIdioms.h"
38 #include "IgnoreDestructiveWriteCountIncrementer.h"
39 #include "MIMETypeRegistry.h"
40 #include "Page.h"
41 #include "SVGNames.h"
42 #include "SVGScriptElement.h"
43 #include "ScriptController.h"
44 #include "ScriptRunner.h"
45 #include "ScriptSourceCode.h"
46 #include "ScriptableDocumentParser.h"
47 #include "SecurityOrigin.h"
48 #include "Settings.h"
49 #include "TextNodeTraversal.h"
50 #include <bindings/ScriptValue.h>
51 #include <inspector/ScriptCallStack.h>
52 #include <wtf/StdLibExtras.h>
53 #include <wtf/text/StringBuilder.h>
54 #include <wtf/text/StringHash.h>
55
56 namespace WebCore {
57
58 ScriptElement::ScriptElement(Element& element, bool parserInserted, bool alreadyStarted)
59     : m_element(element)
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     , m_requestUsesAccessControl(false)
71 {
72     if (parserInserted && m_element.document().scriptableDocumentParser() && !m_element.document().isInDocumentWrite())
73         m_startLineNumber = m_element.document().scriptableDocumentParser()->textPosition().m_line;
74 }
75
76 ScriptElement::~ScriptElement()
77 {
78     stopLoadRequest();
79 }
80
81 bool ScriptElement::shouldCallFinishedInsertingSubtree(ContainerNode& insertionPoint)
82 {
83     return insertionPoint.inDocument() && !m_parserInserted;
84 }
85
86 void ScriptElement::finishedInsertingSubtree()
87 {
88     ASSERT(!m_parserInserted);
89     prepareScript(); // FIXME: Provide a real starting line number here.
90 }
91
92 void ScriptElement::childrenChanged()
93 {
94     if (!m_parserInserted && m_element.inDocument())
95         prepareScript(); // FIXME: Provide a real starting line number here.
96 }
97
98 void ScriptElement::handleSourceAttribute(const String& sourceUrl)
99 {
100     if (ignoresLoadRequest() || sourceUrl.isEmpty())
101         return;
102
103     prepareScript(); // FIXME: Provide a real starting line number here.
104 }
105
106 void ScriptElement::handleAsyncAttribute()
107 {
108     m_forceAsync = false;
109 }
110
111 // Helper function
112 static bool isLegacySupportedJavaScriptLanguage(const String& language)
113 {
114     // Mozilla 1.8 accepts javascript1.0 - javascript1.7, but WinIE 7 accepts only javascript1.1 - javascript1.3.
115     // Mozilla 1.8 and WinIE 7 both accept javascript and livescript.
116     // WinIE 7 accepts ecmascript and jscript, but Mozilla 1.8 doesn't.
117     // Neither Mozilla 1.8 nor WinIE 7 accept leading or trailing whitespace.
118     // We want to accept all the values that either of these browsers accept, but not other values.
119
120     // FIXME: This function is not HTML5 compliant. These belong in the MIME registry as "text/javascript<version>" entries.
121     typedef HashSet<String, CaseFoldingHash> LanguageSet;
122     DEPRECATED_DEFINE_STATIC_LOCAL(LanguageSet, languages, ());
123     if (languages.isEmpty()) {
124         languages.add("javascript");
125         languages.add("javascript");
126         languages.add("javascript1.0");
127         languages.add("javascript1.1");
128         languages.add("javascript1.2");
129         languages.add("javascript1.3");
130         languages.add("javascript1.4");
131         languages.add("javascript1.5");
132         languages.add("javascript1.6");
133         languages.add("javascript1.7");
134         languages.add("livescript");
135         languages.add("ecmascript");
136         languages.add("jscript");
137     }
138
139     return languages.contains(language);
140 }
141
142 void ScriptElement::dispatchErrorEvent()
143 {
144     m_element.dispatchEvent(Event::create(eventNames().errorEvent, false, false));
145 }
146
147 bool ScriptElement::isScriptTypeSupported(LegacyTypeSupport supportLegacyTypes) const
148 {
149     // FIXME: isLegacySupportedJavaScriptLanguage() is not valid HTML5. It is used here to maintain backwards compatibility with existing layout tests. The specific violations are:
150     // - Allowing type=javascript. type= should only support MIME types, such as text/javascript.
151     // - Allowing a different set of languages for language= and type=. language= supports Javascript 1.1 and 1.4-1.6, but type= does not.
152
153     String type = typeAttributeValue();
154     String language = languageAttributeValue();
155     if (type.isEmpty() && language.isEmpty())
156         return true; // Assume text/javascript.
157     if (type.isEmpty()) {
158         type = "text/" + language.lower();
159         if (MIMETypeRegistry::isSupportedJavaScriptMIMEType(type) || isLegacySupportedJavaScriptLanguage(language))
160             return true;
161     } else if (MIMETypeRegistry::isSupportedJavaScriptMIMEType(type.stripWhiteSpace().lower()) || (supportLegacyTypes == AllowLegacyTypeInTypeAttribute && isLegacySupportedJavaScriptLanguage(type)))
162         return true;
163     return false;
164 }
165
166 // http://dev.w3.org/html5/spec/Overview.html#prepare-a-script
167 bool ScriptElement::prepareScript(const TextPosition& scriptStartPosition, LegacyTypeSupport supportLegacyTypes)
168 {
169     if (m_alreadyStarted)
170         return false;
171
172     bool wasParserInserted;
173     if (m_parserInserted) {
174         wasParserInserted = true;
175         m_parserInserted = false;
176     } else
177         wasParserInserted = false;
178
179     if (wasParserInserted && !asyncAttributeValue())
180         m_forceAsync = true;
181
182     // FIXME: HTML5 spec says we should check that all children are either comments or empty text nodes.
183     if (!hasSourceAttribute() && !m_element.firstChild())
184         return false;
185
186     if (!m_element.inDocument())
187         return false;
188
189     if (!isScriptTypeSupported(supportLegacyTypes))
190         return false;
191
192     if (wasParserInserted) {
193         m_parserInserted = true;
194         m_forceAsync = false;
195     }
196
197     m_alreadyStarted = true;
198
199     // FIXME: If script is parser inserted, verify it's still in the original document.
200     Document& document = m_element.document();
201
202     // FIXME: Eventually we'd like to evaluate scripts which are inserted into a
203     // viewless document but this'll do for now.
204     // See http://bugs.webkit.org/show_bug.cgi?id=5727
205     if (!document.frame())
206         return false;
207
208     if (!document.frame()->script().canExecuteScripts(AboutToExecuteScript))
209         return false;
210
211     if (!isScriptForEventSupported())
212         return false;
213
214     if (!charsetAttributeValue().isEmpty())
215         m_characterEncoding = charsetAttributeValue();
216     else
217         m_characterEncoding = document.charset();
218
219     if (hasSourceAttribute())
220         if (!requestScript(sourceAttributeValue()))
221             return false;
222
223     if (hasSourceAttribute() && deferAttributeValue() && m_parserInserted && !asyncAttributeValue()) {
224         m_willExecuteWhenDocumentFinishedParsing = true;
225         m_willBeParserExecuted = true;
226     } else if (hasSourceAttribute() && m_parserInserted && !asyncAttributeValue())
227         m_willBeParserExecuted = true;
228     else if (!hasSourceAttribute() && m_parserInserted && !document.haveStylesheetsLoaded()) {
229         m_willBeParserExecuted = true;
230         m_readyToBeParserExecuted = true;
231     } else if (hasSourceAttribute() && !asyncAttributeValue() && !m_forceAsync) {
232         m_willExecuteInOrder = true;
233         document.scriptRunner()->queueScriptForExecution(this, m_cachedScript, ScriptRunner::IN_ORDER_EXECUTION);
234         m_cachedScript->addClient(this);
235     } else if (hasSourceAttribute()) {
236         m_element.document().scriptRunner()->queueScriptForExecution(this, m_cachedScript, ScriptRunner::ASYNC_EXECUTION);
237         m_cachedScript->addClient(this);
238     } else {
239         // Reset line numbering for nested writes.
240         TextPosition position = document.isInDocumentWrite() ? TextPosition() : scriptStartPosition;
241         executeScript(ScriptSourceCode(scriptContent(), document.url(), position));
242     }
243
244     return true;
245 }
246
247 bool ScriptElement::requestScript(const String& sourceUrl)
248 {
249     Ref<Document> originalDocument(m_element.document());
250     if (!m_element.dispatchBeforeLoadEvent(sourceUrl))
251         return false;
252     if (!m_element.inDocument() || &m_element.document() != originalDocument.ptr())
253         return false;
254
255     ASSERT(!m_cachedScript);
256     if (!stripLeadingAndTrailingHTMLSpaces(sourceUrl).isEmpty()) {
257         ResourceLoaderOptions options = CachedResourceLoader::defaultCachedResourceOptions();
258         options.setContentSecurityPolicyImposition(m_element.isInUserAgentShadowTree() ? ContentSecurityPolicyImposition::SkipPolicyCheck : ContentSecurityPolicyImposition::DoPolicyCheck);
259
260         CachedResourceRequest request(ResourceRequest(m_element.document().completeURL(sourceUrl)), options);
261
262         String crossOriginMode = m_element.fastGetAttribute(HTMLNames::crossoriginAttr);
263         if (!crossOriginMode.isNull()) {
264             m_requestUsesAccessControl = true;
265             StoredCredentials allowCredentials = equalIgnoringCase(crossOriginMode, "use-credentials") ? AllowStoredCredentials : DoNotAllowStoredCredentials;
266             updateRequestForAccessControl(request.mutableResourceRequest(), m_element.document().securityOrigin(), allowCredentials);
267         }
268         request.setCharset(scriptCharset());
269         request.setInitiator(&element());
270
271         m_cachedScript = m_element.document().cachedResourceLoader().requestScript(request);
272         m_isExternalScript = true;
273     }
274
275     if (m_cachedScript)
276         return true;
277
278     RefPtr<Element> element = &m_element;
279     callOnMainThread([this, element] {
280         dispatchErrorEvent();
281     });
282     return false;
283 }
284
285 void ScriptElement::executeScript(const ScriptSourceCode& sourceCode)
286 {
287     ASSERT(m_alreadyStarted);
288
289     if (sourceCode.isEmpty())
290         return;
291
292     if (!m_isExternalScript && !m_element.document().contentSecurityPolicy()->allowInlineScript(m_element.document().url(), m_startLineNumber, m_element.isInUserAgentShadowTree()))
293         return;
294
295 #if ENABLE(NOSNIFF)
296     if (m_isExternalScript && m_cachedScript && !m_cachedScript->mimeTypeAllowedByNosniff()) {
297         m_element.document().addConsoleMessage(MessageSource::Security, MessageLevel::Error, "Refused to execute script from '" + m_cachedScript->url().stringCenterEllipsizedToLength() + "' because its MIME type ('" + m_cachedScript->mimeType() + "') is not executable, and strict MIME type checking is enabled.");
298         return;
299     }
300 #endif
301
302     Ref<Document> document(m_element.document());
303     if (Frame* frame = document->frame()) {
304         IgnoreDestructiveWriteCountIncrementer ignoreDesctructiveWriteCountIncrementer(m_isExternalScript ? document.ptr() : nullptr);
305         CurrentScriptIncrementer currentScriptIncrementer(document, &m_element);
306
307         // Create a script from the script element node, using the script
308         // block's source and the script block's type.
309         // Note: This is where the script is compiled and actually executed.
310         frame->script().evaluate(sourceCode);
311     }
312 }
313
314 void ScriptElement::stopLoadRequest()
315 {
316     if (m_cachedScript) {
317         if (!m_willBeParserExecuted)
318             m_cachedScript->removeClient(this);
319         m_cachedScript = nullptr;
320     }
321 }
322
323 void ScriptElement::execute(CachedScript* cachedScript)
324 {
325     ASSERT(!m_willBeParserExecuted);
326     ASSERT(cachedScript);
327     if (cachedScript->errorOccurred())
328         dispatchErrorEvent();
329     else if (!cachedScript->wasCanceled()) {
330         executeScript(ScriptSourceCode(cachedScript));
331         dispatchLoadEvent();
332     }
333     cachedScript->removeClient(this);
334 }
335
336 void ScriptElement::notifyFinished(CachedResource* resource)
337 {
338     ASSERT(!m_willBeParserExecuted);
339
340     // CachedResource possibly invokes this notifyFinished() more than
341     // once because ScriptElement doesn't unsubscribe itself from
342     // CachedResource here and does it in execute() instead.
343     // We use m_cachedScript to check if this function is already called.
344     ASSERT_UNUSED(resource, resource == m_cachedScript);
345     if (!m_cachedScript)
346         return;
347
348     if (m_requestUsesAccessControl && !m_cachedScript->passesSameOriginPolicyCheck(*m_element.document().securityOrigin())) {
349         dispatchErrorEvent();
350         DEPRECATED_DEFINE_STATIC_LOCAL(String, consoleMessage, (ASCIILiteral("Cross-origin script load denied by Cross-Origin Resource Sharing policy.")));
351         m_element.document().addConsoleMessage(MessageSource::JS, MessageLevel::Error, consoleMessage);
352         return;
353     }
354
355     if (m_willExecuteInOrder)
356         m_element.document().scriptRunner()->notifyScriptReady(this, ScriptRunner::IN_ORDER_EXECUTION);
357     else
358         m_element.document().scriptRunner()->notifyScriptReady(this, ScriptRunner::ASYNC_EXECUTION);
359
360     m_cachedScript = nullptr;
361 }
362
363 bool ScriptElement::ignoresLoadRequest() const
364 {
365     return m_alreadyStarted || m_isExternalScript || m_parserInserted || !m_element.inDocument();
366 }
367
368 bool ScriptElement::isScriptForEventSupported() const
369 {
370     String eventAttribute = eventAttributeValue();
371     String forAttribute = forAttributeValue();
372     if (!eventAttribute.isNull() && !forAttribute.isNull()) {
373         forAttribute = stripLeadingAndTrailingHTMLSpaces(forAttribute);
374         if (!equalIgnoringCase(forAttribute, "window"))
375             return false;
376
377         eventAttribute = stripLeadingAndTrailingHTMLSpaces(eventAttribute);
378         if (!equalIgnoringCase(eventAttribute, "onload") && !equalIgnoringCase(eventAttribute, "onload()"))
379             return false;
380     }
381     return true;
382 }
383
384 String ScriptElement::scriptContent() const
385 {
386     StringBuilder result;
387     for (auto* text = TextNodeTraversal::firstChild(m_element); text; text = TextNodeTraversal::nextSibling(*text))
388         result.append(text->data());
389     return result.toString();
390 }
391
392 ScriptElement* toScriptElementIfPossible(Element* element)
393 {
394     if (is<HTMLScriptElement>(*element))
395         return downcast<HTMLScriptElement>(element);
396
397     if (is<SVGScriptElement>(*element))
398         return downcast<SVGScriptElement>(element);
399
400     return nullptr;
401 }
402
403 }