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