Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ScriptElement.h"
#include "ScriptLoader.h"
#include "mozilla/BasicEvents.h"
#include "mozilla/CycleCollectedJSContext.h"
#include "mozilla/EventDispatcher.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/TrustedTypeUtils.h"
#include "mozilla/dom/TrustedTypesConstants.h"
#include "mozilla/extensions/WebExtensionPolicy.h"
#include "nsContentSink.h"
#include "nsContentUtils.h"
#include "nsGkAtoms.h"
#include "nsIMutationObserver.h"
#include "nsIParser.h"
#include "nsPresContext.h"
#include "nsThreadUtils.h"
using namespace mozilla;
using namespace mozilla::dom;
NS_IMETHODIMP
ScriptElement::ScriptAvailable(nsresult aResult, nsIScriptElement* aElement,
bool aIsInlineClassicScript, nsIURI* aURI,
uint32_t aLineNo) {
if (!aIsInlineClassicScript && NS_FAILED(aResult)) {
nsCOMPtr<nsIParser> parser = do_QueryReferent(mCreatorParser);
if (parser) {
nsCOMPtr<nsIContentSink> sink = parser->GetContentSink();
if (sink) {
nsCOMPtr<Document> parserDoc = do_QueryInterface(sink->GetTarget());
if (GetAsContent()->OwnerDoc() != parserDoc) {
// Suppress errors when we've moved between docs.
// /html/semantics/scripting-1/the-script-element/moving-between-documents/move-back-iframe-fetch-error-external-module.html
return NS_OK;
}
}
}
if (parser) {
parser->IncrementScriptNestingLevel();
}
nsresult rv = FireErrorEvent();
if (parser) {
parser->DecrementScriptNestingLevel();
}
return rv;
}
return NS_OK;
}
/* virtual */
nsresult ScriptElement::FireErrorEvent() {
nsIContent* cont = GetAsContent();
return nsContentUtils::DispatchTrustedEvent(
cont->OwnerDoc(), cont, u"error"_ns, CanBubble::eNo, Cancelable::eNo);
}
NS_IMETHODIMP
ScriptElement::ScriptEvaluated(nsresult aResult, nsIScriptElement* aElement,
bool aIsInline) {
nsresult rv = NS_OK;
if (!aIsInline) {
nsCOMPtr<nsIContent> cont = GetAsContent();
RefPtr<nsPresContext> presContext =
nsContentUtils::GetContextForContent(cont);
nsEventStatus status = nsEventStatus_eIgnore;
EventMessage message = NS_SUCCEEDED(aResult) ? eLoad : eLoadError;
WidgetEvent event(true, message);
// Load event doesn't bubble.
event.mFlags.mBubbles = (message != eLoad);
EventDispatcher::Dispatch(cont, presContext, &event, nullptr, &status);
}
return rv;
}
void ScriptElement::CharacterDataChanged(nsIContent* aContent,
const CharacterDataChangeInfo& aInfo) {
if (!nsContentUtils::IsInSameAnonymousTree(GetAsContent(), aContent)) {
return;
}
UpdateTrustWorthiness(aInfo.mMutationEffectOnScript);
MaybeProcessScript(nullptr /* aParser */);
}
void ScriptElement::AttributeChanged(Element* aElement, int32_t aNameSpaceID,
nsAtom* aAttribute, AttrModType aModType,
const nsAttrValue* aOldValue) {
if (aElement != GetAsContent()) {
return;
}
// When a script element el that is not parser-inserted experiences one of the
// events listed in the following list, the user agent must immediately
// prepare the script element el:
// - The script element is connected and has a src attribute set where
// previously the element had no such attribute.
if (aElement->IsSVGElement() && ((aNameSpaceID != kNameSpaceID_XLink &&
aNameSpaceID != kNameSpaceID_None) ||
aAttribute != nsGkAtoms::href)) {
return;
}
if (aElement->IsHTMLElement() &&
(aNameSpaceID != kNameSpaceID_None || aAttribute != nsGkAtoms::src)) {
return;
}
if (mParserCreated == NOT_FROM_PARSER && aModType == AttrModType::Addition &&
aElement->IsInComposedDoc()) {
MaybeProcessScript(nullptr /* aParser */);
}
}
void ScriptElement::ContentAppended(nsIContent* aFirstNewContent,
const ContentAppendInfo& aInfo) {
if (!nsContentUtils::IsInSameAnonymousTree(GetAsContent(),
aFirstNewContent)) {
return;
}
UpdateTrustWorthiness(aInfo.mMutationEffectOnScript);
MaybeProcessScript(nullptr /* aParser */);
}
void ScriptElement::ContentInserted(nsIContent* aChild,
const ContentInsertInfo& aInfo) {
if (!nsContentUtils::IsInSameAnonymousTree(GetAsContent(), aChild)) {
return;
}
UpdateTrustWorthiness(aInfo.mMutationEffectOnScript);
MaybeProcessScript(nullptr /* aParser */);
}
void ScriptElement::ContentWillBeRemoved(nsIContent* aChild,
const ContentRemoveInfo& aInfo) {
if (!nsContentUtils::IsInSameAnonymousTree(GetAsContent(), aChild)) {
return;
}
UpdateTrustWorthiness(aInfo.mMutationEffectOnScript);
}
bool ScriptElement::MaybeProcessScript(nsCOMPtr<nsIParser> aParser) {
nsIContent* cont = GetAsContent();
NS_ASSERTION(cont->DebugGetSlots()->mMutationObservers.contains(this),
"You forgot to add self as observer");
if (mAlreadyStarted || !mDoneAddingChildren || !cont->GetComposedDoc() ||
mMalformed) {
return false;
}
// The spec says we should calculate "source text" of inline scripts at the
// beginning of the "Prepare the script element" algorithm.
if (HasExternalScriptContent() || mIsTrusted ||
TrustedTypeUtils::CanSkipTrustedTypesEnforcement(
*GetAsContent()->AsElement())) {
// - If it is an inline script that is trusted, we will actually retrieve
// the "source text" lazily for performance reasons (see bug 1376651) so we
// just pass a void string to MaybeProcessScript().
// - If it is an external script, we actually don't need the "source text"
// and can similarly pass a void string to MaybeProcessScript().
bool block = MaybeProcessScript(VoidString());
if (block && aParser) {
aParser->BlockParser();
}
return block;
}
// This is an inline script that is not trusted (i.e. we must execute the
// Trusted Type default policy callback to obtain a trusted "source text").
if (nsContentUtils::IsSafeToRunScript()) {
// - If it is safe to run script in this context, we run the default policy
// callback and pass the returned "source text" to MaybeProcessScript().
bool block =
([self = RefPtr<nsIScriptElement>(this)]()
MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA {
nsString sourceText;
self->GetTrustedTypesCompliantInlineScriptText(sourceText);
return static_cast<ScriptElement*>(self.get())
->MaybeProcessScript(sourceText);
})();
if (block && aParser) {
aParser->BlockParser();
}
return block;
}
// - The default policy callback must be wrapped in a script runner. So we
// need to block the parser at least until we can get the "source text".
nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
"ScriptElement::MaybeProcessScript",
[self = RefPtr<nsIScriptElement>(this), aParser]()
MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA {
nsString sourceText;
self->GetTrustedTypesCompliantInlineScriptText(sourceText);
bool block = static_cast<ScriptElement*>(self.get())
->MaybeProcessScript(sourceText);
if (!block && aParser) {
aParser->UnblockParser();
}
}));
if (aParser) {
aParser->BlockParser();
}
return true;
}
bool ScriptElement::MaybeProcessScript(const nsAString& aSourceText) {
nsIContent* cont = GetAsContent();
if (!HasExternalScriptContent()) {
// If el has no src attribute, and source text is the empty string, then
//
// A void aSourceText means we want to retrieve it lazily (bug 1376651), in
// that case we browse the subtree to try and find a non-empty text node.
bool hasInlineScriptContent =
aSourceText.IsVoid() ? nsContentUtils::HasNonEmptyTextContent(cont)
: !aSourceText.IsEmpty();
if (!hasInlineScriptContent) {
// In the case of an empty, non-external classic script, there is nothing
// to process. However, we must perform a microtask checkpoint afterwards,
if (mKind == JS::loader::ScriptKind::eClassic && !mExternal) {
nsContentUtils::AddScriptRunner(NS_NewRunnableFunction(
"ScriptElement::MaybeProcessScript", []() { nsAutoMicroTask mt; }));
}
return false;
}
}
// Check the type attribute to determine language and version. If type exists,
// it trumps the deprecated 'language='.
nsAutoString type;
bool hasType = GetScriptType(type);
if (!type.IsEmpty()) {
if (!nsContentUtils::IsJavascriptMIMEType(type) &&
!type.LowerCaseEqualsASCII("module") &&
!type.LowerCaseEqualsASCII("importmap")) {
#ifdef DEBUG
// There is a WebGL convention to store strings they need inside script
// tags with these specific unknown script types, so don't warn for them.
// "text/something-not-javascript" only seems to be used in the WebGL
// conformance tests, but it is also clearly deliberately invalid, so
// skip warning for it, too, to reduce warning spam.
if (!type.LowerCaseEqualsASCII("x-shader/x-vertex") &&
!type.LowerCaseEqualsASCII("x-shader/x-fragment") &&
!type.LowerCaseEqualsASCII("text/something-not-javascript")) {
NS_WARNING(nsPrintfCString("Unknown script type '%s'",
NS_ConvertUTF16toUTF8(type).get())
.get());
}
#endif // #ifdef DEBUG
return false;
}
} else if (!hasType) {
// "language" is a deprecated attribute of HTML, so we check it only for
// HTML script elements.
if (cont->IsHTMLElement()) {
nsAutoString language;
cont->AsElement()->GetAttr(nsGkAtoms::language, language);
if (!language.IsEmpty() &&
!nsContentUtils::IsJavaScriptLanguage(language)) {
return false;
}
}
}
Document* ownerDoc = cont->OwnerDoc();
FreezeExecutionAttrs(ownerDoc);
mAlreadyStarted = true;
nsCOMPtr<nsIParser> parser = ((nsIScriptElement*)this)->GetCreatorParser();
if (parser) {
nsCOMPtr<nsIContentSink> sink = parser->GetContentSink();
if (sink) {
nsCOMPtr<Document> parserDoc = do_QueryInterface(sink->GetTarget());
if (ownerDoc != parserDoc) {
return false;
}
}
}
RefPtr<ScriptLoader> loader = ownerDoc->GetScriptLoader();
if (!loader) {
return false;
}
return loader->ProcessScriptElement(this, aSourceText);
}
bool ScriptElement::GetScriptType(nsAString& aType) {
Element* element = GetAsContent()->AsElement();
nsAutoString type;
if (!element->GetAttr(nsGkAtoms::type, type)) {
return false;
}
// U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, or U+0020 SPACE.
static const char kASCIIWhitespace[] = "\t\n\f\r ";
const bool wasEmptyBeforeTrim = type.IsEmpty();
type.Trim(kASCIIWhitespace);
// If the value before trim was not empty and the value is now empty, do not
// trim as we want to retain pure whitespace (by restoring original value)
// because we need to treat "" and " " (etc) differently.
if (!wasEmptyBeforeTrim && type.IsEmpty()) {
return element->GetAttr(nsGkAtoms::type, aType);
}
aType.Assign(type);
return true;
}
void ScriptElement::UpdateTrustWorthiness(
MutationEffectOnScript aMutationEffectOnScript) {
if (aMutationEffectOnScript == MutationEffectOnScript::DropTrustWorthiness &&
StaticPrefs::dom_security_trusted_types_enabled()) {
nsCOMPtr<nsIPrincipal> subjectPrincipal;
if (JSContext* cx = nsContentUtils::GetCurrentJSContext()) {
subjectPrincipal = nsContentUtils::SubjectPrincipal(cx);
if (auto* principal = BasePrincipal::Cast(subjectPrincipal)) {
if (principal->IsSystemPrincipal() ||
principal->ContentScriptAddonPolicyCore()) {
// This script was modified by a priviledged scripts, so continue to
// consider it as trusted.
return;
}
}
}
mIsTrusted = false;
}
}
nsresult ScriptElement::GetTrustedTypesCompliantInlineScriptText(
nsString& aSourceText) {
MOZ_ASSERT(!mIsTrusted);
RefPtr<Element> element = GetAsContent()->AsElement();
nsAutoString sourceText;
GetScriptText(sourceText);
MOZ_ASSERT(element->IsHTMLElement() || element->IsSVGElement());
Maybe<nsAutoString> compliantStringHolder;
constexpr nsLiteralString htmlSinkName = u"HTMLScriptElement text"_ns;
constexpr nsLiteralString svgSinkName = u"SVGScriptElement text"_ns;
ErrorResult error;
const nsAString* compliantString =
TrustedTypeUtils::GetTrustedTypesCompliantStringForTrustedScript(
sourceText, element->IsHTMLElement() ? htmlSinkName : svgSinkName,
kTrustedTypesOnlySinkGroup, *element, compliantStringHolder, error);
if (!error.Failed()) {
aSourceText.Assign(*compliantString);
}
return error.StealNSResult();
}