Load event must be fired only for the SVG structurally external elements and the...
[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-2017 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 "EventNames.h"
35 #include "Frame.h"
36 #include "FrameLoader.h"
37 #include "HTMLNames.h"
38 #include "HTMLParserIdioms.h"
39 #include "IgnoreDestructiveWriteCountIncrementer.h"
40 #include "InlineClassicScript.h"
41 #include "LoadableClassicScript.h"
42 #include "LoadableModuleScript.h"
43 #include "MIMETypeRegistry.h"
44 #include "PendingScript.h"
45 #include "RuntimeApplicationChecks.h"
46 #include "SVGScriptElement.h"
47 #include "ScriptController.h"
48 #include "ScriptDisallowedScope.h"
49 #include "ScriptRunner.h"
50 #include "ScriptSourceCode.h"
51 #include "ScriptableDocumentParser.h"
52 #include "Settings.h"
53 #include "TextNodeTraversal.h"
54 #include <wtf/StdLibExtras.h>
55 #include <wtf/text/StringBuilder.h>
56 #include <wtf/text/StringHash.h>
57
58 namespace WebCore {
59
60 static const auto maxUserGesturePropagationTime = 1_s;
61
62 ScriptElement::ScriptElement(Element& element, bool parserInserted, bool alreadyStarted)
63     : m_element(element)
64     , m_startLineNumber(WTF::OrdinalNumber::beforeFirst())
65     , m_parserInserted(parserInserted)
66     , m_isExternalScript(false)
67     , m_alreadyStarted(alreadyStarted)
68     , m_haveFiredLoad(false)
69     , m_errorOccurred(false)
70     , m_willBeParserExecuted(false)
71     , m_readyToBeParserExecuted(false)
72     , m_willExecuteWhenDocumentFinishedParsing(false)
73     , m_forceAsync(!parserInserted)
74     , m_willExecuteInOrder(false)
75     , m_isModuleScript(false)
76     , m_creationTime(MonotonicTime::now())
77     , m_userGestureToken(UserGestureIndicator::currentUserGesture())
78 {
79     if (parserInserted && m_element.document().scriptableDocumentParser() && !m_element.document().isInDocumentWrite())
80         m_startLineNumber = m_element.document().scriptableDocumentParser()->textPosition().m_line;
81 }
82
83 void ScriptElement::didFinishInsertingNode()
84 {
85     ASSERT(!m_parserInserted);
86     prepareScript(); // FIXME: Provide a real starting line number here.
87 }
88
89 void ScriptElement::childrenChanged(const ContainerNode::ChildChange& childChange)
90 {
91     if (!m_parserInserted && childChange.isInsertion() && m_element.isConnected())
92         prepareScript(); // FIXME: Provide a real starting line number here.
93 }
94
95 void ScriptElement::handleSourceAttribute(const String& sourceURL)
96 {
97     if (ignoresLoadRequest() || sourceURL.isEmpty())
98         return;
99
100     prepareScript(); // FIXME: Provide a real starting line number here.
101 }
102
103 void ScriptElement::handleAsyncAttribute()
104 {
105     m_forceAsync = false;
106 }
107
108 static bool isLegacySupportedJavaScriptLanguage(const String& language)
109 {
110     static const auto languages = makeNeverDestroyed(HashSet<String, ASCIICaseInsensitiveHash> {
111         "javascript",
112         "javascript1.0",
113         "javascript1.1",
114         "javascript1.2",
115         "javascript1.3",
116         "javascript1.4",
117         "javascript1.5",
118         "javascript1.6",
119         "javascript1.7",
120         "livescript",
121         "ecmascript",
122         "jscript",
123     });
124     return languages.get().contains(language);
125 }
126
127 void ScriptElement::dispatchErrorEvent()
128 {
129     m_element.dispatchEvent(Event::create(eventNames().errorEvent, Event::CanBubble::No, Event::IsCancelable::No));
130 }
131
132 Optional<ScriptElement::ScriptType> ScriptElement::determineScriptType(LegacyTypeSupport supportLegacyTypes) const
133 {
134     // FIXME: isLegacySupportedJavaScriptLanguage() is not valid HTML5. It is used here to maintain backwards compatibility with existing layout tests. The specific violations are:
135     // - Allowing type=javascript. type= should only support MIME types, such as text/javascript.
136     // - Allowing a different set of languages for language= and type=. language= supports Javascript 1.1 and 1.4-1.6, but type= does not.
137     String type = typeAttributeValue();
138     String language = languageAttributeValue();
139     if (type.isEmpty()) {
140         if (language.isEmpty())
141             return ScriptType::Classic; // Assume text/javascript.
142         if (MIMETypeRegistry::isSupportedJavaScriptMIMEType("text/" + language))
143             return ScriptType::Classic;
144         if (isLegacySupportedJavaScriptLanguage(language))
145             return ScriptType::Classic;
146         return WTF::nullopt;
147     }
148     if (MIMETypeRegistry::isSupportedJavaScriptMIMEType(type.stripWhiteSpace()))
149         return ScriptType::Classic;
150     if (supportLegacyTypes == AllowLegacyTypeInTypeAttribute && isLegacySupportedJavaScriptLanguage(type))
151         return ScriptType::Classic;
152
153     // FIXME: XHTML spec defines "defer" attribute. But WebKit does not implement it for a long time.
154     // And module tag also uses defer attribute semantics. We disable script type="module" for non HTML document.
155     // Once "defer" is implemented, we can reconsider enabling modules in XHTML.
156     // https://bugs.webkit.org/show_bug.cgi?id=123387
157     if (!m_element.document().isHTMLDocument())
158         return WTF::nullopt;
159
160     // https://html.spec.whatwg.org/multipage/scripting.html#attr-script-type
161     // Setting the attribute to an ASCII case-insensitive match for the string "module" means that the script is a module script.
162     if (equalLettersIgnoringASCIICase(type, "module"))
163         return ScriptType::Module;
164     return WTF::nullopt;
165 }
166
167 // http://dev.w3.org/html5/spec/Overview.html#prepare-a-script
168 bool ScriptElement::prepareScript(const TextPosition& scriptStartPosition, LegacyTypeSupport supportLegacyTypes)
169 {
170     if (m_alreadyStarted)
171         return false;
172
173     bool wasParserInserted;
174     if (m_parserInserted) {
175         wasParserInserted = true;
176         m_parserInserted = false;
177     } else
178         wasParserInserted = false;
179
180     if (wasParserInserted && !hasAsyncAttribute())
181         m_forceAsync = true;
182
183     // FIXME: HTML5 spec says we should check that all children are either comments or empty text nodes.
184     if (!hasSourceAttribute() && !m_element.firstChild())
185         return false;
186
187     if (!m_element.isConnected())
188         return false;
189
190     ScriptType scriptType = ScriptType::Classic;
191     if (Optional<ScriptType> result = determineScriptType(supportLegacyTypes))
192         scriptType = result.value();
193     else
194         return false;
195     m_isModuleScript = scriptType == ScriptType::Module;
196
197     if (wasParserInserted) {
198         m_parserInserted = true;
199         m_forceAsync = false;
200     }
201
202     m_alreadyStarted = true;
203
204     // FIXME: If script is parser inserted, verify it's still in the original document.
205     Document& document = m_element.document();
206
207     // FIXME: Eventually we'd like to evaluate scripts which are inserted into a
208     // viewless document but this'll do for now.
209     // See http://bugs.webkit.org/show_bug.cgi?id=5727
210     if (!document.frame())
211         return false;
212
213     if (scriptType == ScriptType::Classic && hasNoModuleAttribute())
214         return false;
215
216     if (!document.frame()->script().canExecuteScripts(AboutToExecuteScript))
217         return false;
218
219     if (scriptType == ScriptType::Classic && !isScriptForEventSupported())
220         return false;
221
222     // According to the spec, the module tag ignores the "charset" attribute as the same to the worker's
223     // importScript. But WebKit supports the "charset" for importScript intentionally. So to be consistent,
224     // even for the module tags, we handle the "charset" attribute.
225     if (!charsetAttributeValue().isEmpty())
226         m_characterEncoding = charsetAttributeValue();
227     else
228         m_characterEncoding = document.charset();
229
230     if (scriptType == ScriptType::Classic) {
231         if (hasSourceAttribute()) {
232             if (!requestClassicScript(sourceAttributeValue()))
233                 return false;
234         }
235     } else {
236         ASSERT(scriptType == ScriptType::Module);
237         if (!requestModuleScript(scriptStartPosition))
238             return false;
239     }
240
241     // All the inlined module script is handled by requestModuleScript. It produces LoadableModuleScript and inlined module script
242     // is handled as the same to the external module script.
243
244     bool isClassicExternalScript = scriptType == ScriptType::Classic && hasSourceAttribute();
245     bool isParserInsertedDeferredScript = ((isClassicExternalScript && hasDeferAttribute()) || scriptType == ScriptType::Module)
246         && m_parserInserted && !hasAsyncAttribute();
247     if (isParserInsertedDeferredScript) {
248         m_willExecuteWhenDocumentFinishedParsing = true;
249         m_willBeParserExecuted = true;
250     } else if (isClassicExternalScript && m_parserInserted && !hasAsyncAttribute()) {
251         ASSERT(scriptType == ScriptType::Classic);
252         m_willBeParserExecuted = true;
253     } else if ((isClassicExternalScript || scriptType == ScriptType::Module) && !hasAsyncAttribute() && !m_forceAsync) {
254         m_willExecuteInOrder = true;
255         ASSERT(m_loadableScript);
256         document.scriptRunner().queueScriptForExecution(*this, *m_loadableScript, ScriptRunner::IN_ORDER_EXECUTION);
257     } else if (hasSourceAttribute() || scriptType == ScriptType::Module) {
258         ASSERT(m_loadableScript);
259         ASSERT(hasAsyncAttribute() || m_forceAsync);
260         document.scriptRunner().queueScriptForExecution(*this, *m_loadableScript, ScriptRunner::ASYNC_EXECUTION);
261     } else if (!hasSourceAttribute() && m_parserInserted && !document.haveStylesheetsLoaded()) {
262         ASSERT(scriptType == ScriptType::Classic);
263         m_willBeParserExecuted = true;
264         m_readyToBeParserExecuted = true;
265     } else {
266         ASSERT(scriptType == ScriptType::Classic);
267         TextPosition position = document.isInDocumentWrite() ? TextPosition() : scriptStartPosition;
268         executeClassicScript(ScriptSourceCode(scriptContent(), URL(document.url()), position, JSC::SourceProviderSourceType::Program, InlineClassicScript::create(*this)));
269     }
270
271     return true;
272 }
273
274 bool ScriptElement::requestClassicScript(const String& sourceURL)
275 {
276     Ref<Document> originalDocument(m_element.document());
277     if (!m_element.dispatchBeforeLoadEvent(sourceURL))
278         return false;
279     bool didEventListenerDisconnectThisElement = !m_element.isConnected() || &m_element.document() != originalDocument.ptr();
280     if (didEventListenerDisconnectThisElement)
281         return false;
282
283     ASSERT(!m_loadableScript);
284     if (!stripLeadingAndTrailingHTMLSpaces(sourceURL).isEmpty()) {
285         auto script = LoadableClassicScript::create(
286             m_element.attributeWithoutSynchronization(HTMLNames::nonceAttr),
287             m_element.document().settings().subresourceIntegrityEnabled() ? m_element.attributeWithoutSynchronization(HTMLNames::integrityAttr).string() : emptyString(),
288             referrerPolicy(),
289             m_element.attributeWithoutSynchronization(HTMLNames::crossoriginAttr),
290             scriptCharset(),
291             m_element.localName(),
292             m_element.isInUserAgentShadowTree());
293         if (script->load(m_element.document(), m_element.document().completeURL(sourceURL))) {
294             m_loadableScript = WTFMove(script);
295             m_isExternalScript = true;
296         }
297     }
298
299     if (m_loadableScript)
300         return true;
301
302     callOnMainThread([this, element = Ref<Element>(m_element)] {
303         dispatchErrorEvent();
304     });
305     return false;
306 }
307
308 bool ScriptElement::requestModuleScript(const TextPosition& scriptStartPosition)
309 {
310     String nonce = m_element.attributeWithoutSynchronization(HTMLNames::nonceAttr);
311     String crossOriginMode = m_element.attributeWithoutSynchronization(HTMLNames::crossoriginAttr);
312     if (crossOriginMode.isNull())
313         crossOriginMode = "omit"_s;
314
315     if (hasSourceAttribute()) {
316         String sourceURL = sourceAttributeValue();
317         Ref<Document> originalDocument(m_element.document());
318         if (!m_element.dispatchBeforeLoadEvent(sourceURL))
319             return false;
320
321         bool didEventListenerDisconnectThisElement = !m_element.isConnected() || &m_element.document() != originalDocument.ptr();
322         if (didEventListenerDisconnectThisElement)
323             return false;
324
325         if (stripLeadingAndTrailingHTMLSpaces(sourceURL).isEmpty()) {
326             dispatchErrorEvent();
327             return false;
328         }
329
330         auto moduleScriptRootURL = m_element.document().completeURL(sourceURL);
331         if (!moduleScriptRootURL.isValid()) {
332             dispatchErrorEvent();
333             return false;
334         }
335
336         m_isExternalScript = true;
337         auto script = LoadableModuleScript::create(
338             nonce,
339             m_element.document().settings().subresourceIntegrityEnabled() ? m_element.attributeWithoutSynchronization(HTMLNames::integrityAttr).string() : emptyString(),
340             referrerPolicy(),
341             crossOriginMode,
342             scriptCharset(),
343             m_element.localName(),
344             m_element.isInUserAgentShadowTree());
345         script->load(m_element.document(), moduleScriptRootURL);
346         m_loadableScript = WTFMove(script);
347         return true;
348     }
349
350     auto script = LoadableModuleScript::create(nonce, emptyString(), referrerPolicy(), crossOriginMode, scriptCharset(), m_element.localName(), m_element.isInUserAgentShadowTree());
351
352     TextPosition position = m_element.document().isInDocumentWrite() ? TextPosition() : scriptStartPosition;
353     ScriptSourceCode sourceCode(scriptContent(), URL(m_element.document().url()), position, JSC::SourceProviderSourceType::Module, script.copyRef());
354
355     ASSERT(m_element.document().contentSecurityPolicy());
356     const auto& contentSecurityPolicy = *m_element.document().contentSecurityPolicy();
357     bool hasKnownNonce = contentSecurityPolicy.allowScriptWithNonce(nonce, m_element.isInUserAgentShadowTree());
358     if (!contentSecurityPolicy.allowInlineScript(m_element.document().url(), m_startLineNumber, sourceCode.source().toStringWithoutCopying(), hasKnownNonce))
359         return false;
360
361     script->load(m_element.document(), sourceCode);
362     m_loadableScript = WTFMove(script);
363     return true;
364 }
365
366 void ScriptElement::executeClassicScript(const ScriptSourceCode& sourceCode)
367 {
368     RELEASE_ASSERT_WITH_SECURITY_IMPLICATION(ScriptDisallowedScope::InMainThread::isScriptAllowed() || !isInWebProcess());
369     ASSERT(m_alreadyStarted);
370
371     if (sourceCode.isEmpty())
372         return;
373
374     if (!m_isExternalScript) {
375         ASSERT(m_element.document().contentSecurityPolicy());
376         const ContentSecurityPolicy& contentSecurityPolicy = *m_element.document().contentSecurityPolicy();
377         bool hasKnownNonce = contentSecurityPolicy.allowScriptWithNonce(m_element.attributeWithoutSynchronization(HTMLNames::nonceAttr), m_element.isInUserAgentShadowTree());
378         if (!contentSecurityPolicy.allowInlineScript(m_element.document().url(), m_startLineNumber, sourceCode.source().toStringWithoutCopying(), hasKnownNonce))
379             return;
380     }
381
382     auto& document = m_element.document();
383     auto* frame = document.frame();
384     if (!frame)
385         return;
386
387     IgnoreDestructiveWriteCountIncrementer ignoreDesctructiveWriteCountIncrementer(m_isExternalScript ? &document : nullptr);
388     CurrentScriptIncrementer currentScriptIncrementer(document, m_element);
389
390     frame->script().evaluate(sourceCode);
391 }
392
393 void ScriptElement::executeModuleScript(LoadableModuleScript& loadableModuleScript)
394 {
395     // https://html.spec.whatwg.org/multipage/scripting.html#execute-the-script-block
396
397     ASSERT(!loadableModuleScript.error());
398
399     auto& document = m_element.document();
400     auto* frame = document.frame();
401     if (!frame)
402         return;
403
404     IgnoreDestructiveWriteCountIncrementer ignoreDesctructiveWriteCountIncrementer(&document);
405     CurrentScriptIncrementer currentScriptIncrementer(document, m_element);
406
407     frame->script().linkAndEvaluateModuleScript(loadableModuleScript);
408 }
409
410 void ScriptElement::dispatchLoadEventRespectingUserGestureIndicator()
411 {
412     if (MonotonicTime::now() - m_creationTime > maxUserGesturePropagationTime) {
413         dispatchLoadEvent();
414         return;
415     }
416
417     UserGestureIndicator indicator(m_userGestureToken);
418     dispatchLoadEvent();
419 }
420
421 void ScriptElement::executeScriptAndDispatchEvent(LoadableScript& loadableScript)
422 {
423     if (Optional<LoadableScript::Error> error = loadableScript.error()) {
424         if (Optional<LoadableScript::ConsoleMessage> message = error->consoleMessage)
425             m_element.document().addConsoleMessage(message->source, message->level, message->message);
426         dispatchErrorEvent();
427     } else if (!loadableScript.wasCanceled()) {
428         ASSERT(!loadableScript.error());
429         loadableScript.execute(*this);
430         dispatchLoadEventRespectingUserGestureIndicator();
431     }
432 }
433
434 void ScriptElement::executePendingScript(PendingScript& pendingScript)
435 {
436     if (auto* loadableScript = pendingScript.loadableScript())
437         executeScriptAndDispatchEvent(*loadableScript);
438     else {
439         ASSERT(!pendingScript.error());
440         ASSERT_WITH_MESSAGE(scriptType() == ScriptType::Classic, "Module script always have a loadableScript pointer.");
441         executeClassicScript(ScriptSourceCode(scriptContent(), URL(m_element.document().url()), pendingScript.startingPosition(), JSC::SourceProviderSourceType::Program, InlineClassicScript::create(*this)));
442         dispatchLoadEventRespectingUserGestureIndicator();
443     }
444 }
445
446 bool ScriptElement::ignoresLoadRequest() const
447 {
448     return m_alreadyStarted || m_isExternalScript || m_parserInserted || !m_element.isConnected();
449 }
450
451 bool ScriptElement::isScriptForEventSupported() const
452 {
453     String eventAttribute = eventAttributeValue();
454     String forAttribute = forAttributeValue();
455     if (!eventAttribute.isNull() && !forAttribute.isNull()) {
456         forAttribute = stripLeadingAndTrailingHTMLSpaces(forAttribute);
457         if (!equalLettersIgnoringASCIICase(forAttribute, "window"))
458             return false;
459
460         eventAttribute = stripLeadingAndTrailingHTMLSpaces(eventAttribute);
461         if (!equalLettersIgnoringASCIICase(eventAttribute, "onload") && !equalLettersIgnoringASCIICase(eventAttribute, "onload()"))
462             return false;
463     }
464     return true;
465 }
466
467 String ScriptElement::scriptContent() const
468 {
469     return TextNodeTraversal::childTextContent(m_element);
470 }
471
472 void ScriptElement::ref()
473 {
474     m_element.ref();
475 }
476
477 void ScriptElement::deref()
478 {
479     m_element.deref();
480 }
481
482 bool isScriptElement(Element& element)
483 {
484     return is<HTMLScriptElement>(element) || is<SVGScriptElement>(element);
485 }
486
487 ScriptElement& downcastScriptElement(Element& element)
488 {
489     if (is<HTMLScriptElement>(element))
490         return downcast<HTMLScriptElement>(element);
491     return downcast<SVGScriptElement>(element);
492 }
493
494 }