diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /dom/script | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/script')
-rw-r--r-- | dom/script/LoadedScript.cpp | 201 | ||||
-rw-r--r-- | dom/script/LoadedScript.h | 119 | ||||
-rw-r--r-- | dom/script/ModuleLoadRequest.cpp | 218 | ||||
-rw-r--r-- | dom/script/ModuleLoadRequest.h | 122 | ||||
-rw-r--r-- | dom/script/ScriptDecoding.h | 96 | ||||
-rw-r--r-- | dom/script/ScriptElement.cpp | 119 | ||||
-rw-r--r-- | dom/script/ScriptElement.h | 54 | ||||
-rw-r--r-- | dom/script/ScriptKind.h | 18 | ||||
-rw-r--r-- | dom/script/ScriptLoadHandler.cpp | 472 | ||||
-rw-r--r-- | dom/script/ScriptLoadHandler.h | 117 | ||||
-rw-r--r-- | dom/script/ScriptLoadRequest.cpp | 319 | ||||
-rw-r--r-- | dom/script/ScriptLoadRequest.h | 409 | ||||
-rw-r--r-- | dom/script/ScriptLoader.cpp | 4343 | ||||
-rw-r--r-- | dom/script/ScriptLoader.h | 780 | ||||
-rw-r--r-- | dom/script/ScriptSettings.cpp | 841 | ||||
-rw-r--r-- | dom/script/ScriptSettings.h | 527 | ||||
-rw-r--r-- | dom/script/ScriptTrace.cpp | 33 | ||||
-rw-r--r-- | dom/script/ScriptTrace.h | 47 | ||||
-rw-r--r-- | dom/script/moz.build | 48 | ||||
-rw-r--r-- | dom/script/nsIScriptElement.cpp | 53 | ||||
-rw-r--r-- | dom/script/nsIScriptElement.h | 343 | ||||
-rw-r--r-- | dom/script/nsIScriptLoaderObserver.idl | 47 |
22 files changed, 9326 insertions, 0 deletions
diff --git a/dom/script/LoadedScript.cpp b/dom/script/LoadedScript.cpp new file mode 100644 index 0000000000..35fec0dd25 --- /dev/null +++ b/dom/script/LoadedScript.cpp @@ -0,0 +1,201 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "LoadedScript.h" + +#include "mozilla/HoldDropJSObjects.h" + +#include "jsfriendapi.h" +#include "js/Modules.h" // JS::{Get,Set}ModulePrivate +#include "ScriptLoader.h" + +namespace mozilla { +namespace dom { + +////////////////////////////////////////////////////////////// +// LoadedScript +////////////////////////////////////////////////////////////// + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LoadedScript) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_CLASS(LoadedScript) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(LoadedScript) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFetchOptions) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mBaseURL) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(LoadedScript) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFetchOptions) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(LoadedScript) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(LoadedScript) +NS_IMPL_CYCLE_COLLECTING_RELEASE(LoadedScript) + +LoadedScript::LoadedScript(ScriptKind aKind, ScriptFetchOptions* aFetchOptions, + nsIURI* aBaseURL) + : mKind(aKind), mFetchOptions(aFetchOptions), mBaseURL(aBaseURL) { + MOZ_ASSERT(mFetchOptions); + MOZ_ASSERT(mBaseURL); +} + +LoadedScript::~LoadedScript() { DropJSObjects(this); } + +void LoadedScript::AssociateWithScript(JSScript* aScript) { + // Set a JSScript's private value to point to this object. The JS engine will + // increment our reference count by calling HostAddRefTopLevelScript(). This + // is decremented by HostReleaseTopLevelScript() below when the JSScript dies. + + MOZ_ASSERT(JS::GetScriptPrivate(aScript).isUndefined()); + JS::SetScriptPrivate(aScript, JS::PrivateValue(this)); +} + +inline void CheckModuleScriptPrivate(LoadedScript* script, + const JS::Value& aPrivate) { +#ifdef DEBUG + if (script->IsModuleScript()) { + JSObject* module = script->AsModuleScript()->mModuleRecord.unbarrieredGet(); + MOZ_ASSERT_IF(module, JS::GetModulePrivate(module) == aPrivate); + } +#endif +} + +void HostAddRefTopLevelScript(const JS::Value& aPrivate) { + // Increment the reference count of a LoadedScript object that is now pointed + // to by a JSScript. The reference count is decremented by + // HostReleaseTopLevelScript() below. + + auto script = static_cast<LoadedScript*>(aPrivate.toPrivate()); + CheckModuleScriptPrivate(script, aPrivate); + script->AddRef(); +} + +void HostReleaseTopLevelScript(const JS::Value& aPrivate) { + // Decrement the reference count of a LoadedScript object that was pointed to + // by a JSScript. The reference count was originally incremented by + // HostAddRefTopLevelScript() above. + + auto script = static_cast<LoadedScript*>(aPrivate.toPrivate()); + CheckModuleScriptPrivate(script, aPrivate); + script->Release(); +} + +////////////////////////////////////////////////////////////// +// EventScript +////////////////////////////////////////////////////////////// + +EventScript::EventScript(ScriptFetchOptions* aFetchOptions, nsIURI* aBaseURL) + : LoadedScript(ScriptKind::eEvent, aFetchOptions, aBaseURL) {} + +////////////////////////////////////////////////////////////// +// ClassicScript +////////////////////////////////////////////////////////////// + +ClassicScript::ClassicScript(ScriptFetchOptions* aFetchOptions, + nsIURI* aBaseURL) + : LoadedScript(ScriptKind::eClassic, aFetchOptions, aBaseURL) {} + +////////////////////////////////////////////////////////////// +// ModuleScript +////////////////////////////////////////////////////////////// + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ModuleScript) +NS_INTERFACE_MAP_END_INHERITING(LoadedScript) + +NS_IMPL_CYCLE_COLLECTION_CLASS(ModuleScript) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ModuleScript, LoadedScript) + tmp->UnlinkModuleRecord(); + tmp->mParseError.setUndefined(); + tmp->mErrorToRethrow.setUndefined(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(ModuleScript, LoadedScript) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(ModuleScript, LoadedScript) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mModuleRecord) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mParseError) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mErrorToRethrow) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_ADDREF_INHERITED(ModuleScript, LoadedScript) +NS_IMPL_RELEASE_INHERITED(ModuleScript, LoadedScript) + +ModuleScript::ModuleScript(ScriptFetchOptions* aFetchOptions, nsIURI* aBaseURL) + : LoadedScript(ScriptKind::eModule, aFetchOptions, aBaseURL), + mDebuggerDataInitialized(false) { + MOZ_ASSERT(!ModuleRecord()); + MOZ_ASSERT(!HasParseError()); + MOZ_ASSERT(!HasErrorToRethrow()); +} + +void ModuleScript::UnlinkModuleRecord() { + // Remove the module record's pointer to this object if present and + // decrement our reference count. The reference is added by + // SetModuleRecord() below. + if (mModuleRecord) { + MOZ_ASSERT(JS::GetModulePrivate(mModuleRecord).toPrivate() == this); + JS::SetModulePrivate(mModuleRecord, JS::UndefinedValue()); + mModuleRecord = nullptr; + } +} + +ModuleScript::~ModuleScript() { + // The object may be destroyed without being unlinked first. + UnlinkModuleRecord(); +} + +void ModuleScript::SetModuleRecord(JS::Handle<JSObject*> aModuleRecord) { + MOZ_ASSERT(!mModuleRecord); + MOZ_ASSERT_IF(IsModuleScript(), !AsModuleScript()->HasParseError()); + MOZ_ASSERT_IF(IsModuleScript(), !AsModuleScript()->HasErrorToRethrow()); + + mModuleRecord = aModuleRecord; + + // Make module's host defined field point to this object. The JS engine will + // increment our reference count by calling HostAddRefTopLevelScript(). This + // is decremented when the field is cleared in UnlinkModuleRecord() above or + // when the module record dies. + MOZ_ASSERT(JS::GetModulePrivate(mModuleRecord).isUndefined()); + JS::SetModulePrivate(mModuleRecord, JS::PrivateValue(this)); + + HoldJSObjects(this); +} + +void ModuleScript::SetParseError(const JS::Value& aError) { + MOZ_ASSERT(!aError.isUndefined()); + MOZ_ASSERT(!HasParseError()); + MOZ_ASSERT(!HasErrorToRethrow()); + + UnlinkModuleRecord(); + mParseError = aError; + HoldJSObjects(this); +} + +void ModuleScript::SetErrorToRethrow(const JS::Value& aError) { + MOZ_ASSERT(!aError.isUndefined()); + + // This is only called after SetModuleRecord() or SetParseError() so we don't + // need to call HoldJSObjects() here. + MOZ_ASSERT(ModuleRecord() || HasParseError()); + + mErrorToRethrow = aError; +} + +void ModuleScript::SetDebuggerDataInitialized() { + MOZ_ASSERT(ModuleRecord()); + MOZ_ASSERT(!mDebuggerDataInitialized); + + mDebuggerDataInitialized = true; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/script/LoadedScript.h b/dom/script/LoadedScript.h new file mode 100644 index 0000000000..76d512ab68 --- /dev/null +++ b/dom/script/LoadedScript.h @@ -0,0 +1,119 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_LoadedScript_h +#define mozilla_dom_LoadedScript_h + +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "jsapi.h" +#include "ScriptLoadRequest.h" + +class nsIURI; + +namespace mozilla { +namespace dom { + +class ScriptLoader; + +void HostAddRefTopLevelScript(const JS::Value& aPrivate); +void HostReleaseTopLevelScript(const JS::Value& aPrivate); + +class ClassicScript; +class ModuleScript; +class EventScript; + +class LoadedScript : public nsISupports { + ScriptKind mKind; + RefPtr<ScriptFetchOptions> mFetchOptions; + nsCOMPtr<nsIURI> mBaseURL; + + protected: + LoadedScript(ScriptKind aKind, ScriptFetchOptions* aFetchOptions, + nsIURI* aBaseURL); + + virtual ~LoadedScript(); + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(LoadedScript) + + bool IsModuleScript() const { return mKind == ScriptKind::eModule; } + bool IsEventScript() const { return mKind == ScriptKind::eEvent; } + + inline ClassicScript* AsClassicScript(); + inline ModuleScript* AsModuleScript(); + inline EventScript* AsEventScript(); + + ScriptFetchOptions* GetFetchOptions() const { return mFetchOptions; } + nsIURI* BaseURL() const { return mBaseURL; } + + void AssociateWithScript(JSScript* aScript); +}; + +class ClassicScript final : public LoadedScript { + ~ClassicScript() = default; + + public: + ClassicScript(ScriptFetchOptions* aFetchOptions, nsIURI* aBaseURL); +}; + +class EventScript final : public LoadedScript { + ~EventScript() = default; + + public: + EventScript(ScriptFetchOptions* aFetchOptions, nsIURI* aBaseURL); +}; + +// A single module script. May be used to satisfy multiple load requests. + +class ModuleScript final : public LoadedScript { + JS::Heap<JSObject*> mModuleRecord; + JS::Heap<JS::Value> mParseError; + JS::Heap<JS::Value> mErrorToRethrow; + bool mDebuggerDataInitialized; + + ~ModuleScript(); + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(ModuleScript, + LoadedScript) + + ModuleScript(ScriptFetchOptions* aFetchOptions, nsIURI* aBaseURL); + + void SetModuleRecord(JS::Handle<JSObject*> aModuleRecord); + void SetParseError(const JS::Value& aError); + void SetErrorToRethrow(const JS::Value& aError); + void SetDebuggerDataInitialized(); + + JSObject* ModuleRecord() const { return mModuleRecord; } + + JS::Value ParseError() const { return mParseError; } + JS::Value ErrorToRethrow() const { return mErrorToRethrow; } + bool HasParseError() const { return !mParseError.isUndefined(); } + bool HasErrorToRethrow() const { return !mErrorToRethrow.isUndefined(); } + bool DebuggerDataInitialized() const { return mDebuggerDataInitialized; } + + void UnlinkModuleRecord(); + + friend void CheckModuleScriptPrivate(LoadedScript*, const JS::Value&); +}; + +ClassicScript* LoadedScript::AsClassicScript() { + MOZ_ASSERT(!IsModuleScript()); + return static_cast<ClassicScript*>(this); +} + +ModuleScript* LoadedScript::AsModuleScript() { + MOZ_ASSERT(IsModuleScript()); + return static_cast<ModuleScript*>(this); +} + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_LoadedScript_h diff --git a/dom/script/ModuleLoadRequest.cpp b/dom/script/ModuleLoadRequest.cpp new file mode 100644 index 0000000000..c2771e6d46 --- /dev/null +++ b/dom/script/ModuleLoadRequest.cpp @@ -0,0 +1,218 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "ModuleLoadRequest.h" + +#include "mozilla/HoldDropJSObjects.h" + +#include "LoadedScript.h" +#include "ScriptLoader.h" + +namespace mozilla { +namespace dom { + +#undef LOG +#define LOG(args) \ + MOZ_LOG(ScriptLoader::gScriptLoaderLog, mozilla::LogLevel::Debug, args) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ModuleLoadRequest) +NS_INTERFACE_MAP_END_INHERITING(ScriptLoadRequest) + +NS_IMPL_CYCLE_COLLECTION_MULTI_ZONE_JSHOLDER_CLASS(ModuleLoadRequest) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ModuleLoadRequest, + ScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mLoader, mModuleScript, mImports) + tmp->ClearDynamicImport(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(ModuleLoadRequest, + ScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLoader, mModuleScript, mImports) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(ModuleLoadRequest, + ScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mDynamicReferencingPrivate) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mDynamicSpecifier) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mDynamicPromise) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_ADDREF_INHERITED(ModuleLoadRequest, ScriptLoadRequest) +NS_IMPL_RELEASE_INHERITED(ModuleLoadRequest, ScriptLoadRequest) + +static VisitedURLSet* NewVisitedSetForTopLevelImport(nsIURI* aURI) { + auto set = new VisitedURLSet(); + set->PutEntry(aURI); + return set; +} + +/* static */ +ModuleLoadRequest* ModuleLoadRequest::CreateTopLevel( + nsIURI* aURI, ScriptFetchOptions* aFetchOptions, + const SRIMetadata& aIntegrity, nsIURI* aReferrer, ScriptLoader* aLoader) { + return new ModuleLoadRequest(aURI, aFetchOptions, aIntegrity, aReferrer, + true, /* is top level */ + false, /* is dynamic import */ + aLoader, NewVisitedSetForTopLevelImport(aURI)); +} + +/* static */ +ModuleLoadRequest* ModuleLoadRequest::CreateStaticImport( + nsIURI* aURI, ModuleLoadRequest* aParent) { + auto request = + new ModuleLoadRequest(aURI, aParent->mFetchOptions, SRIMetadata(), + aParent->mURI, false, /* is top level */ + false, /* is dynamic import */ + aParent->mLoader, aParent->mVisitedSet); + + request->mIsInline = false; + request->mScriptMode = aParent->mScriptMode; + + return request; +} + +/* static */ +ModuleLoadRequest* ModuleLoadRequest::CreateDynamicImport( + nsIURI* aURI, ScriptFetchOptions* aFetchOptions, nsIURI* aBaseURL, + ScriptLoader* aLoader, JS::Handle<JS::Value> aReferencingPrivate, + JS::Handle<JSString*> aSpecifier, JS::Handle<JSObject*> aPromise) { + MOZ_ASSERT(aSpecifier); + MOZ_ASSERT(aPromise); + + auto request = new ModuleLoadRequest( + aURI, aFetchOptions, SRIMetadata(), aBaseURL, true, /* is top level */ + true, /* is dynamic import */ + aLoader, NewVisitedSetForTopLevelImport(aURI)); + + request->mIsInline = false; + request->mScriptMode = ScriptMode::eAsync; + request->mDynamicReferencingPrivate = aReferencingPrivate; + request->mDynamicSpecifier = aSpecifier; + request->mDynamicPromise = aPromise; + + HoldJSObjects(request); + + return request; +} + +ModuleLoadRequest::ModuleLoadRequest( + nsIURI* aURI, ScriptFetchOptions* aFetchOptions, + const SRIMetadata& aIntegrity, nsIURI* aReferrer, bool aIsTopLevel, + bool aIsDynamicImport, ScriptLoader* aLoader, VisitedURLSet* aVisitedSet) + : ScriptLoadRequest(ScriptKind::eModule, aURI, aFetchOptions, aIntegrity, + aReferrer), + mIsTopLevel(aIsTopLevel), + mIsDynamicImport(aIsDynamicImport), + mLoader(aLoader), + mVisitedSet(aVisitedSet) {} + +void ModuleLoadRequest::Cancel() { + ScriptLoadRequest::Cancel(); + mModuleScript = nullptr; + mProgress = ScriptLoadRequest::Progress::eReady; + CancelImports(); + mReady.RejectIfExists(NS_ERROR_DOM_ABORT_ERR, __func__); +} + +void ModuleLoadRequest::CancelImports() { + for (size_t i = 0; i < mImports.Length(); i++) { + mImports[i]->Cancel(); + } +} + +void ModuleLoadRequest::SetReady() { + // Mark a module as ready to execute. This means that this module and all it + // dependencies have had their source loaded, parsed as a module and the + // modules instantiated. + // + // The mReady promise is used to ensure that when all dependencies of a module + // have become ready, DependenciesLoaded is called on that module + // request. This is set up in StartFetchingModuleDependencies. + +#ifdef DEBUG + for (size_t i = 0; i < mImports.Length(); i++) { + MOZ_ASSERT(mImports[i]->IsReadyToRun()); + } +#endif + + ScriptLoadRequest::SetReady(); + mReady.ResolveIfExists(true, __func__); +} + +void ModuleLoadRequest::ModuleLoaded() { + // A module that was found to be marked as fetching in the module map has now + // been loaded. + + LOG(("ScriptLoadRequest (%p): Module loaded", this)); + + mModuleScript = mLoader->GetFetchedModule(mURI); + if (!mModuleScript || mModuleScript->HasParseError()) { + ModuleErrored(); + return; + } + + mLoader->StartFetchingModuleDependencies(this); +} + +void ModuleLoadRequest::ModuleErrored() { + if (IsCanceled()) { + return; + } + + LOG(("ScriptLoadRequest (%p): Module errored", this)); + + mLoader->CheckModuleDependenciesLoaded(this); + MOZ_ASSERT(!mModuleScript || mModuleScript->HasParseError()); + + CancelImports(); + SetReady(); + LoadFinished(); +} + +void ModuleLoadRequest::DependenciesLoaded() { + if (IsCanceled()) { + return; + } + + // The module and all of its dependencies have been successfully fetched and + // compiled. + + LOG(("ScriptLoadRequest (%p): Module dependencies loaded", this)); + + MOZ_ASSERT(mModuleScript); + + mLoader->CheckModuleDependenciesLoaded(this); + SetReady(); + LoadFinished(); +} + +void ModuleLoadRequest::LoadFailed() { + // We failed to load the source text or an error occurred unrelated to the + // content of the module (e.g. OOM). + + LOG(("ScriptLoadRequest (%p): Module load failed", this)); + + MOZ_ASSERT(!mModuleScript); + + Cancel(); + LoadFinished(); +} + +void ModuleLoadRequest::LoadFinished() { + mLoader->ProcessLoadedModuleTree(this); + + mLoader = nullptr; +} + +void ModuleLoadRequest::ClearDynamicImport() { + mDynamicReferencingPrivate = JS::UndefinedValue(); + mDynamicSpecifier = nullptr; + mDynamicPromise = nullptr; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/script/ModuleLoadRequest.h b/dom/script/ModuleLoadRequest.h new file mode 100644 index 0000000000..8faff57cac --- /dev/null +++ b/dom/script/ModuleLoadRequest.h @@ -0,0 +1,122 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_ModuleLoadRequest_h +#define mozilla_dom_ModuleLoadRequest_h + +#include "ScriptLoadRequest.h" +#include "js/RootingAPI.h" +#include "js/Value.h" +#include "nsURIHashKey.h" +#include "mozilla/MozPromise.h" +#include "nsTHashtable.h" + +namespace mozilla { +namespace dom { + +class ModuleScript; +class ScriptLoader; + +// A reference counted set of URLs we have visited in the process of loading a +// module graph. +class VisitedURLSet : public nsTHashtable<nsURIHashKey> { + NS_INLINE_DECL_REFCOUNTING(VisitedURLSet) + + private: + ~VisitedURLSet() = default; +}; + +// A load request for a module, created for every top level module script and +// every module import. Load request can share an ModuleScript if there are +// multiple imports of the same module. + +class ModuleLoadRequest final : public ScriptLoadRequest { + ~ModuleLoadRequest() = default; + + ModuleLoadRequest(const ModuleLoadRequest& aOther) = delete; + ModuleLoadRequest(ModuleLoadRequest&& aOther) = delete; + + ModuleLoadRequest(nsIURI* aURI, ScriptFetchOptions* aFetchOptions, + const SRIMetadata& aIntegrity, nsIURI* aReferrer, + bool aIsTopLevel, bool aIsDynamicImport, + ScriptLoader* aLoader, VisitedURLSet* aVisitedSet); + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(ModuleLoadRequest, + ScriptLoadRequest) + + // Create a top-level module load request. + static ModuleLoadRequest* CreateTopLevel(nsIURI* aURI, + ScriptFetchOptions* aFetchOptions, + const SRIMetadata& aIntegrity, + nsIURI* aReferrer, + ScriptLoader* aLoader); + + // Create a module load request for a static module import. + static ModuleLoadRequest* CreateStaticImport(nsIURI* aURI, + ModuleLoadRequest* aParent); + + // Create a module load request for dynamic module import. + static ModuleLoadRequest* CreateDynamicImport( + nsIURI* aURI, ScriptFetchOptions* aFetchOptions, nsIURI* aBaseURL, + ScriptLoader* aLoader, JS::Handle<JS::Value> aReferencingPrivate, + JS::Handle<JSString*> aSpecifier, JS::Handle<JSObject*> aPromise); + + bool IsTopLevel() const override { return mIsTopLevel; } + + bool IsDynamicImport() const { return mIsDynamicImport; } + + void SetReady() override; + void Cancel() override; + void ClearDynamicImport(); + + void ModuleLoaded(); + void ModuleErrored(); + void DependenciesLoaded(); + void LoadFailed(); + + private: + void LoadFinished(); + void CancelImports(); + + public: + // Is this a request for a top level module script or an import? + const bool mIsTopLevel; + + // Is this the top level request for a dynamic module import? + const bool mIsDynamicImport; + + // Pointer to the script loader, used to trigger actions when the module load + // finishes. + RefPtr<ScriptLoader> mLoader; + + // Set to a module script object after a successful load or nullptr on + // failure. + RefPtr<ModuleScript> mModuleScript; + + // A promise that is completed on successful load of this module and all of + // its dependencies, indicating that the module is ready for instantiation and + // evaluation. + MozPromiseHolder<GenericPromise> mReady; + + // Array of imported modules. + nsTArray<RefPtr<ModuleLoadRequest>> mImports; + + // Set of module URLs visited while fetching the module graph this request is + // part of. + RefPtr<VisitedURLSet> mVisitedSet; + + // For dynamic imports, the details to pass to FinishDynamicImport. + JS::Heap<JS::Value> mDynamicReferencingPrivate; + JS::Heap<JSString*> mDynamicSpecifier; + JS::Heap<JSObject*> mDynamicPromise; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ModuleLoadRequest_h diff --git a/dom/script/ScriptDecoding.h b/dom/script/ScriptDecoding.h new file mode 100644 index 0000000000..a54d120240 --- /dev/null +++ b/dom/script/ScriptDecoding.h @@ -0,0 +1,96 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +/* Script decoding templates for processing byte data as UTF-8 or UTF-16. */ + +#ifndef mozilla_dom_ScriptDecoding_h +#define mozilla_dom_ScriptDecoding_h + +#include "mozilla/Assertions.h" // MOZ_ASSERT +#include "mozilla/CheckedInt.h" // mozilla::CheckedInt +#include "mozilla/Encoding.h" // mozilla::Decoder +#include "mozilla/Span.h" // mozilla::Span +#include "mozilla/Tuple.h" // mozilla::Tie +#include "mozilla/UniquePtr.h" // mozilla::UniquePtr +#include "mozilla/Unused.h" // mozilla::Unused + +#include <stddef.h> // size_t +#include <stdint.h> // uint8_t, uint32_t +#include <type_traits> // std::is_same + +namespace mozilla { +namespace dom { + +template <typename Unit> +struct ScriptDecoding { + static_assert(std::is_same<Unit, char16_t>::value || + std::is_same<Unit, Utf8Unit>::value, + "must be either UTF-8 or UTF-16"); +}; + +template <> +struct ScriptDecoding<char16_t> { + static CheckedInt<size_t> MaxBufferLength(const UniquePtr<Decoder>& aDecoder, + uint32_t aByteLength) { + return aDecoder->MaxUTF16BufferLength(aByteLength); + } + + static size_t DecodeInto(const UniquePtr<Decoder>& aDecoder, + const Span<const uint8_t>& aSrc, + Span<char16_t> aDest, bool aEndOfSource) { + uint32_t result; + size_t read; + size_t written; + bool hadErrors; + Tie(result, read, written, hadErrors) = + aDecoder->DecodeToUTF16(aSrc, aDest, aEndOfSource); + MOZ_ASSERT(result == kInputEmpty); + MOZ_ASSERT(read == aSrc.Length()); + MOZ_ASSERT(written <= aDest.Length()); + Unused << hadErrors; + + return written; + } +}; + +template <> +struct ScriptDecoding<Utf8Unit> { + static CheckedInt<size_t> MaxBufferLength(const UniquePtr<Decoder>& aDecoder, + uint32_t aByteLength) { + return aDecoder->MaxUTF8BufferLength(aByteLength); + } + + static size_t DecodeInto(const UniquePtr<Decoder>& aDecoder, + const Span<const uint8_t>& aSrc, + Span<Utf8Unit> aDest, bool aEndOfSource) { + uint32_t result; + size_t read; + size_t written; + bool hadErrors; + // Until C++ char8_t happens, our decoder APIs deal in |uint8_t| while + // |Utf8Unit| internally deals with |char|, so there's inevitable impedance + // mismatch between |aDest| as |Utf8Unit| and |AsWritableBytes(aDest)| as + // |Span<uint8_t>|. :-( The written memory will be interpreted through + // |char Utf8Unit::mValue| which is *permissible* because any object's + // memory can be interpreted as |char|. Unfortunately, until + // twos-complement is mandated, we have to play fast and loose and *hope* + // interpreting memory storing |uint8_t| as |char| will pick up the desired + // wrapped-around value. ¯\_(ツ)_/¯ + Tie(result, read, written, hadErrors) = + aDecoder->DecodeToUTF8(aSrc, AsWritableBytes(aDest), aEndOfSource); + MOZ_ASSERT(result == kInputEmpty); + MOZ_ASSERT(read == aSrc.Length()); + MOZ_ASSERT(written <= aDest.Length()); + Unused << hadErrors; + + return written; + } +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ScriptDecoding_h diff --git a/dom/script/ScriptElement.cpp b/dom/script/ScriptElement.cpp new file mode 100644 index 0000000000..66c86fc9a3 --- /dev/null +++ b/dom/script/ScriptElement.cpp @@ -0,0 +1,119 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/EventDispatcher.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "nsContentUtils.h" +#include "nsPresContext.h" +#include "nsIParser.h" +#include "nsGkAtoms.h" +#include "nsContentSink.h" + +using namespace mozilla; +using namespace mozilla::dom; + +NS_IMETHODIMP +ScriptElement::ScriptAvailable(nsresult aResult, nsIScriptElement* aElement, + bool aIsInlineClassicScript, nsIURI* aURI, + int32_t aLineNo) { + if (!aIsInlineClassicScript && NS_FAILED(aResult)) { + nsCOMPtr<nsIParser> parser = do_QueryReferent(mCreatorParser); + if (parser) { + parser->IncrementScriptNestingLevel(); + } + nsresult rv = FireErrorEvent(); + if (parser) { + parser->DecrementScriptNestingLevel(); + } + return rv; + } + return NS_OK; +} + +/* virtual */ +nsresult ScriptElement::FireErrorEvent() { + nsCOMPtr<nsIContent> cont = do_QueryInterface((nsIScriptElement*)this); + + 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 = do_QueryInterface((nsIScriptElement*)this); + + 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&) { + MaybeProcessScript(); +} + +void ScriptElement::AttributeChanged(Element* aElement, int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType, + const nsAttrValue* aOldValue) { + MaybeProcessScript(); +} + +void ScriptElement::ContentAppended(nsIContent* aFirstNewContent) { + MaybeProcessScript(); +} + +void ScriptElement::ContentInserted(nsIContent* aChild) { + MaybeProcessScript(); +} + +bool ScriptElement::MaybeProcessScript() { + nsCOMPtr<nsIContent> cont = do_QueryInterface((nsIScriptElement*)this); + + NS_ASSERTION(cont->DebugGetSlots()->mMutationObservers.Contains(this), + "You forgot to add self as observer"); + + if (mAlreadyStarted || !mDoneAddingChildren || !cont->GetComposedDoc() || + mMalformed || !HasScriptContent()) { + 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) { + // Willful violation of HTML5 as of 2010-12-01 + return false; + } + } + } + + RefPtr<ScriptLoader> loader = ownerDoc->ScriptLoader(); + return loader->ProcessScriptElement(this); +} diff --git a/dom/script/ScriptElement.h b/dom/script/ScriptElement.h new file mode 100644 index 0000000000..6f0e69f7bd --- /dev/null +++ b/dom/script/ScriptElement.h @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_ScriptElement_h +#define mozilla_dom_ScriptElement_h + +#include "mozilla/Attributes.h" +#include "nsIScriptLoaderObserver.h" +#include "nsIScriptElement.h" +#include "nsStubMutationObserver.h" + +namespace mozilla { +namespace dom { + +/** + * Baseclass useful for script elements (such as <xhtml:script> and + * <svg:script>). Currently the class assumes that only the 'src' + * attribute and the children of the class affect what script to execute. + */ + +class ScriptElement : public nsIScriptElement, public nsStubMutationObserver { + public: + // nsIScriptLoaderObserver + NS_DECL_NSISCRIPTLOADEROBSERVER + + // nsIMutationObserver + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + + explicit ScriptElement(FromParser aFromParser) + : nsIScriptElement(aFromParser) {} + + virtual nsresult FireErrorEvent() override; + + protected: + // Internal methods + + /** + * Check if this element contains any script, linked or inline + */ + virtual bool HasScriptContent() = 0; + + virtual bool MaybeProcessScript() override; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ScriptElement_h diff --git a/dom/script/ScriptKind.h b/dom/script/ScriptKind.h new file mode 100644 index 0000000000..7ae6c6c465 --- /dev/null +++ b/dom/script/ScriptKind.h @@ -0,0 +1,18 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_ScriptKind_h +#define mozilla_dom_ScriptKind_h + +namespace mozilla { +namespace dom { + +enum class ScriptKind { eClassic, eModule, eEvent }; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/script/ScriptLoadHandler.cpp b/dom/script/ScriptLoadHandler.cpp new file mode 100644 index 0000000000..2d5f2bfbf6 --- /dev/null +++ b/dom/script/ScriptLoadHandler.cpp @@ -0,0 +1,472 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "ScriptLoadHandler.h" + +#include <stdlib.h> +#include <utility> +#include "ScriptLoader.h" +#include "ScriptTrace.h" +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Encoding.h" +#include "mozilla/Logging.h" +#include "mozilla/NotNull.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/Tuple.h" +#include "mozilla/Utf8.h" +#include "mozilla/Vector.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/SRICheck.h" +#include "mozilla/dom/ScriptDecoding.h" +#include "mozilla/dom/ScriptLoadRequest.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsICacheInfoChannel.h" +#include "nsIChannel.h" +#include "nsIHttpChannel.h" +#include "nsIRequest.h" +#include "nsIScriptElement.h" +#include "nsIURI.h" +#include "nsJSUtils.h" +#include "nsMimeTypes.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla { +namespace dom { + +#undef LOG +#define LOG(args) \ + MOZ_LOG(ScriptLoader::gScriptLoaderLog, mozilla::LogLevel::Debug, args) + +#define LOG_ENABLED() \ + MOZ_LOG_TEST(ScriptLoader::gScriptLoaderLog, mozilla::LogLevel::Debug) + +ScriptLoadHandler::ScriptLoadHandler( + ScriptLoader* aScriptLoader, ScriptLoadRequest* aRequest, + UniquePtr<SRICheckDataVerifier>&& aSRIDataVerifier) + : mScriptLoader(aScriptLoader), + mRequest(aRequest), + mSRIDataVerifier(std::move(aSRIDataVerifier)), + mSRIStatus(NS_OK), + mDecoder() { + MOZ_ASSERT(mRequest->IsUnknownDataType()); + MOZ_ASSERT(mRequest->IsLoading()); +} + +ScriptLoadHandler::~ScriptLoadHandler() = default; + +NS_IMPL_ISUPPORTS(ScriptLoadHandler, nsIIncrementalStreamLoaderObserver) + +template <typename Unit> +nsresult ScriptLoadHandler::DecodeRawDataHelper(const uint8_t* aData, + uint32_t aDataLength, + bool aEndOfStream) { + CheckedInt<size_t> needed = + ScriptDecoding<Unit>::MaxBufferLength(mDecoder, aDataLength); + if (!needed.isValid()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Reference to the script source buffer which we will update. + ScriptLoadRequest::ScriptTextBuffer<Unit>& scriptText = + mRequest->ScriptText<Unit>(); + + uint32_t haveRead = scriptText.length(); + + CheckedInt<uint32_t> capacity = haveRead; + capacity += needed.value(); + + if (!capacity.isValid() || !scriptText.resize(capacity.value())) { + return NS_ERROR_OUT_OF_MEMORY; + } + + size_t written = ScriptDecoding<Unit>::DecodeInto( + mDecoder, Span(aData, aDataLength), + Span(scriptText.begin() + haveRead, needed.value()), aEndOfStream); + MOZ_ASSERT(written <= needed.value()); + + haveRead += written; + MOZ_ASSERT(haveRead <= capacity.value(), + "mDecoder produced more data than expected"); + MOZ_ALWAYS_TRUE(scriptText.resize(haveRead)); + mRequest->mScriptTextLength = scriptText.length(); + + return NS_OK; +} + +nsresult ScriptLoadHandler::DecodeRawData(const uint8_t* aData, + uint32_t aDataLength, + bool aEndOfStream) { + if (mRequest->IsUTF16Text()) { + return DecodeRawDataHelper<char16_t>(aData, aDataLength, aEndOfStream); + } + + return DecodeRawDataHelper<Utf8Unit>(aData, aDataLength, aEndOfStream); +} + +NS_IMETHODIMP +ScriptLoadHandler::OnIncrementalData(nsIIncrementalStreamLoader* aLoader, + nsISupports* aContext, + uint32_t aDataLength, const uint8_t* aData, + uint32_t* aConsumedLength) { + nsCOMPtr<nsIRequest> channelRequest; + aLoader->GetRequest(getter_AddRefs(channelRequest)); + + if (!mPreloadStartNotified) { + mPreloadStartNotified = true; + mRequest->NotifyStart(channelRequest); + } + + if (mRequest->IsCanceled()) { + // If request cancelled, ignore any incoming data. + *aConsumedLength = aDataLength; + return NS_OK; + } + + nsresult rv = NS_OK; + if (mRequest->IsUnknownDataType()) { + rv = EnsureKnownDataType(aLoader); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mRequest->IsTextSource()) { + if (!EnsureDecoder(aLoader, aData, aDataLength, + /* aEndOfStream = */ false)) { + return NS_OK; + } + + // Below we will/shall consume entire data chunk. + *aConsumedLength = aDataLength; + + // Decoder has already been initialized. -- trying to decode all loaded + // bytes. + rv = DecodeRawData(aData, aDataLength, /* aEndOfStream = */ false); + NS_ENSURE_SUCCESS(rv, rv); + + // If SRI is required for this load, appending new bytes to the hash. + if (mSRIDataVerifier && NS_SUCCEEDED(mSRIStatus)) { + mSRIStatus = mSRIDataVerifier->Update(aDataLength, aData); + } + } else if (mRequest->IsBinASTSource()) { + if (!mRequest->ScriptBinASTData().append(aData, aDataLength)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Below we will/shall consume entire data chunk. + *aConsumedLength = aDataLength; + + // If SRI is required for this load, appending new bytes to the hash. + if (mSRIDataVerifier && NS_SUCCEEDED(mSRIStatus)) { + mSRIStatus = mSRIDataVerifier->Update(aDataLength, aData); + } + } else { + MOZ_ASSERT(mRequest->IsBytecode()); + if (!mRequest->mScriptBytecode.append(aData, aDataLength)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + *aConsumedLength = aDataLength; + uint32_t sriLength = 0; + rv = MaybeDecodeSRI(&sriLength); + if (NS_FAILED(rv)) { + return channelRequest->Cancel(mScriptLoader->RestartLoad(mRequest)); + } + if (sriLength) { + mRequest->mBytecodeOffset = JS::AlignTranscodingBytecodeOffset(sriLength); + } + } + + return rv; +} + +bool ScriptLoadHandler::TrySetDecoder(nsIIncrementalStreamLoader* aLoader, + const uint8_t* aData, + uint32_t aDataLength, bool aEndOfStream) { + MOZ_ASSERT(mDecoder == nullptr, + "can't have a decoder already if we're trying to set one"); + + // JavaScript modules are always UTF-8. + if (mRequest->IsModuleRequest()) { + mDecoder = UTF_8_ENCODING->NewDecoderWithBOMRemoval(); + return true; + } + + // Determine if BOM check should be done. This occurs either + // if end-of-stream has been reached, or at least 3 bytes have + // been read from input. + if (!aEndOfStream && (aDataLength < 3)) { + return false; + } + + // Do BOM detection. + const Encoding* encoding; + size_t bomLength; + Tie(encoding, bomLength) = Encoding::ForBOM(Span(aData, aDataLength)); + if (encoding) { + mDecoder = encoding->NewDecoderWithBOMRemoval(); + return true; + } + + // BOM detection failed, check content stream for charset. + nsCOMPtr<nsIRequest> req; + nsresult rv = aLoader->GetRequest(getter_AddRefs(req)); + NS_ASSERTION(req, "StreamLoader's request went away prematurely"); + NS_ENSURE_SUCCESS(rv, false); + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(req); + + if (channel) { + nsAutoCString label; + if (NS_SUCCEEDED(channel->GetContentCharset(label)) && + (encoding = Encoding::ForLabel(label))) { + mDecoder = encoding->NewDecoderWithoutBOMHandling(); + return true; + } + } + + // Check the hint charset from the script element or preload + // request. + nsAutoString hintCharset; + if (!mRequest->IsPreload()) { + mRequest->GetScriptElement()->GetScriptCharset(hintCharset); + } else { + nsTArray<ScriptLoader::PreloadInfo>::index_type i = + mScriptLoader->mPreloads.IndexOf( + mRequest, 0, ScriptLoader::PreloadRequestComparator()); + + NS_ASSERTION(i != mScriptLoader->mPreloads.NoIndex, + "Incorrect preload bookkeeping"); + hintCharset = mScriptLoader->mPreloads[i].mCharset; + } + + if ((encoding = Encoding::ForLabel(hintCharset))) { + mDecoder = encoding->NewDecoderWithoutBOMHandling(); + return true; + } + + // Get the charset from the charset of the document. + if (mScriptLoader->mDocument) { + encoding = mScriptLoader->mDocument->GetDocumentCharacterSet(); + mDecoder = encoding->NewDecoderWithoutBOMHandling(); + return true; + } + + // Curiously, there are various callers that don't pass aDocument. The + // fallback in the old code was ISO-8859-1, which behaved like + // windows-1252. + mDecoder = WINDOWS_1252_ENCODING->NewDecoderWithoutBOMHandling(); + return true; +} + +nsresult ScriptLoadHandler::MaybeDecodeSRI(uint32_t* sriLength) { + *sriLength = 0; + + if (!mSRIDataVerifier || mSRIDataVerifier->IsComplete() || + NS_FAILED(mSRIStatus)) { + return NS_OK; + } + + // Skip until the content is large enough to be decoded. + if (mRequest->mScriptBytecode.length() <= + mSRIDataVerifier->DataSummaryLength()) { + return NS_OK; + } + + mSRIStatus = mSRIDataVerifier->ImportDataSummary( + mRequest->mScriptBytecode.length(), mRequest->mScriptBytecode.begin()); + + if (NS_FAILED(mSRIStatus)) { + // We are unable to decode the hash contained in the alternate data which + // contains the bytecode, or it does not use the same algorithm. + LOG( + ("ScriptLoadHandler::MaybeDecodeSRI, failed to decode SRI, restart " + "request")); + return mSRIStatus; + } + + *sriLength = mSRIDataVerifier->DataSummaryLength(); + MOZ_ASSERT(*sriLength > 0); + return NS_OK; +} + +nsresult ScriptLoadHandler::EnsureKnownDataType( + nsIIncrementalStreamLoader* aLoader) { + MOZ_ASSERT(mRequest->IsUnknownDataType()); + MOZ_ASSERT(mRequest->IsLoading()); + + nsCOMPtr<nsIRequest> req; + nsresult rv = aLoader->GetRequest(getter_AddRefs(req)); + MOZ_ASSERT(req, "StreamLoader's request went away prematurely"); + NS_ENSURE_SUCCESS(rv, rv); + + if (mRequest->IsLoadingSource()) { + mRequest->SetTextSource(); + TRACE_FOR_TEST(mRequest->GetScriptElement(), "scriptloader_load_source"); + return NS_OK; + } + + nsCOMPtr<nsICacheInfoChannel> cic(do_QueryInterface(req)); + if (cic) { + nsAutoCString altDataType; + cic->GetAlternativeDataType(altDataType); + if (altDataType.Equals(nsContentUtils::JSBytecodeMimeType())) { + mRequest->SetBytecode(); + TRACE_FOR_TEST(mRequest->GetScriptElement(), + "scriptloader_load_bytecode"); + return NS_OK; + } + MOZ_ASSERT(altDataType.IsEmpty()); + } + + if (nsJSUtils::BinASTEncodingEnabled()) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(req); + if (httpChannel) { + nsAutoCString mimeType; + httpChannel->GetContentType(mimeType); + if (mimeType.LowerCaseEqualsASCII(APPLICATION_JAVASCRIPT_BINAST)) { + if (mRequest->ShouldAcceptBinASTEncoding()) { + mRequest->SetBinASTSource(); + TRACE_FOR_TEST(mRequest->GetScriptElement(), + "scriptloader_load_source"); + return NS_OK; + } + + // If the request isn't allowed to accept BinAST, fallback to text + // source. The possibly binary source will be passed to normal + // JS parser and will throw error there. + mRequest->SetTextSource(); + return NS_OK; + } + } + } + + mRequest->SetTextSource(); + TRACE_FOR_TEST(mRequest->GetScriptElement(), "scriptloader_load_source"); + + MOZ_ASSERT(!mRequest->IsUnknownDataType()); + MOZ_ASSERT(mRequest->IsLoading()); + return NS_OK; +} + +NS_IMETHODIMP +ScriptLoadHandler::OnStreamComplete(nsIIncrementalStreamLoader* aLoader, + nsISupports* aContext, nsresult aStatus, + uint32_t aDataLength, + const uint8_t* aData) { + nsresult rv = NS_OK; + if (LOG_ENABLED()) { + nsAutoCString url; + mRequest->mURI->GetAsciiSpec(url); + LOG(("ScriptLoadRequest (%p): Stream complete (url = %s)", mRequest.get(), + url.get())); + } + + nsCOMPtr<nsIRequest> channelRequest; + aLoader->GetRequest(getter_AddRefs(channelRequest)); + + if (!mPreloadStartNotified) { + mPreloadStartNotified = true; + mRequest->NotifyStart(channelRequest); + } + + auto notifyStop = + MakeScopeExit([&] { mRequest->NotifyStop(channelRequest, rv); }); + + if (!mRequest->IsCanceled()) { + if (mRequest->IsUnknownDataType()) { + rv = EnsureKnownDataType(aLoader); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mRequest->IsTextSource()) { + DebugOnly<bool> encoderSet = + EnsureDecoder(aLoader, aData, aDataLength, /* aEndOfStream = */ true); + MOZ_ASSERT(encoderSet); + rv = DecodeRawData(aData, aDataLength, /* aEndOfStream = */ true); + NS_ENSURE_SUCCESS(rv, rv); + + LOG(("ScriptLoadRequest (%p): Source length in code units = %u", + mRequest.get(), unsigned(mRequest->ScriptTextLength()))); + + // If SRI is required for this load, appending new bytes to the hash. + if (mSRIDataVerifier && NS_SUCCEEDED(mSRIStatus)) { + mSRIStatus = mSRIDataVerifier->Update(aDataLength, aData); + } + } else if (mRequest->IsBinASTSource()) { + if (!mRequest->ScriptBinASTData().append(aData, aDataLength)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // If SRI is required for this load, appending new bytes to the hash. + if (mSRIDataVerifier && NS_SUCCEEDED(mSRIStatus)) { + mSRIStatus = mSRIDataVerifier->Update(aDataLength, aData); + } + } else { + MOZ_ASSERT(mRequest->IsBytecode()); + if (!mRequest->mScriptBytecode.append(aData, aDataLength)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + LOG(("ScriptLoadRequest (%p): Bytecode length = %u", mRequest.get(), + unsigned(mRequest->mScriptBytecode.length()))); + + // If we abort while decoding the SRI, we fallback on explictly requesting + // the source. Thus, we should not continue in + // ScriptLoader::OnStreamComplete, which removes the request from the + // waiting lists. + // + // We calculate the SRI length below. + uint32_t unused; + rv = MaybeDecodeSRI(&unused); + if (NS_FAILED(rv)) { + return channelRequest->Cancel(mScriptLoader->RestartLoad(mRequest)); + } + + // The bytecode cache always starts with the SRI hash, thus even if there + // is no SRI data verifier instance, we still want to skip the hash. + uint32_t sriLength; + rv = SRICheckDataVerifier::DataSummaryLength( + mRequest->mScriptBytecode.length(), mRequest->mScriptBytecode.begin(), + &sriLength); + if (NS_FAILED(rv)) { + return channelRequest->Cancel(mScriptLoader->RestartLoad(mRequest)); + } + + mRequest->mBytecodeOffset = JS::AlignTranscodingBytecodeOffset(sriLength); + } + } + + // Everything went well, keep the CacheInfoChannel alive such that we can + // later save the bytecode on the cache entry. + if (NS_SUCCEEDED(rv) && mRequest->IsSource() && + StaticPrefs::dom_script_loader_bytecode_cache_enabled()) { + mRequest->mCacheInfo = do_QueryInterface(channelRequest); + LOG(("ScriptLoadRequest (%p): nsICacheInfoChannel = %p", mRequest.get(), + mRequest->mCacheInfo.get())); + } + + // we have to mediate and use mRequest. + rv = mScriptLoader->OnStreamComplete(aLoader, mRequest, aStatus, mSRIStatus, + mSRIDataVerifier.get()); + + // In case of failure, clear the mCacheInfoChannel to avoid keeping it alive. + if (NS_FAILED(rv)) { + mRequest->mCacheInfo = nullptr; + } + + return rv; +} + +#undef LOG_ENABLED +#undef LOG + +} // namespace dom +} // namespace mozilla diff --git a/dom/script/ScriptLoadHandler.h b/dom/script/ScriptLoadHandler.h new file mode 100644 index 0000000000..96352a11ac --- /dev/null +++ b/dom/script/ScriptLoadHandler.h @@ -0,0 +1,117 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +/* + * A class that handles loading and evaluation of <script> elements. + */ + +#ifndef mozilla_dom_ScriptLoadHandler_h +#define mozilla_dom_ScriptLoadHandler_h + +#include "nsIIncrementalStreamLoader.h" +#include "nsISupports.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { + +class Decoder; + +namespace dom { + +class ScriptLoadRequest; +class ScriptLoader; +class SRICheckDataVerifier; + +class ScriptLoadHandler final : public nsIIncrementalStreamLoaderObserver { + public: + explicit ScriptLoadHandler( + ScriptLoader* aScriptLoader, ScriptLoadRequest* aRequest, + UniquePtr<SRICheckDataVerifier>&& aSRIDataVerifier); + + NS_DECL_ISUPPORTS + NS_DECL_NSIINCREMENTALSTREAMLOADEROBSERVER + + private: + virtual ~ScriptLoadHandler(); + + /* + * Decode the given data into the already-allocated internal + * |ScriptTextBuffer<Unit>|. + * + * This function is intended to be called only by |DecodeRawData| after + * determining which sort of |ScriptTextBuffer<Unit>| has been allocated. + */ + template <typename Unit> + nsresult DecodeRawDataHelper(const uint8_t* aData, uint32_t aDataLength, + bool aEndOfStream); + + /* + * Once the charset is found by the EnsureDecoder function, we can + * incrementally convert the charset to the one expected by the JS Parser. + */ + nsresult DecodeRawData(const uint8_t* aData, uint32_t aDataLength, + bool aEndOfStream); + + /* + * Discover the charset by looking at the stream data, the script tag, and + * other indicators. Returns true if charset has been discovered. + */ + bool EnsureDecoder(nsIIncrementalStreamLoader* aLoader, const uint8_t* aData, + uint32_t aDataLength, bool aEndOfStream) { + // Check if the decoder has already been created. + if (mDecoder) { + return true; + } + + return TrySetDecoder(aLoader, aData, aDataLength, aEndOfStream); + } + + /* + * Attempt to determine how script data will be decoded, when such + * determination hasn't already been made. (If you don't know whether it's + * been made yet, use |EnsureDecoder| above instead.) Return false if there + * isn't enough information yet to make the determination, or true if a + * determination was made. + */ + bool TrySetDecoder(nsIIncrementalStreamLoader* aLoader, const uint8_t* aData, + uint32_t aDataLength, bool aEndOfStream); + + /* + * When streaming bytecode, we have the opportunity to fallback early if SRI + * does not match the expectation of the document. + * + * If SRI hash is decoded, `sriLength` is set to the length of the hash. + */ + nsresult MaybeDecodeSRI(uint32_t* sriLength); + + // Query the channel to find the data type associated with the input stream. + nsresult EnsureKnownDataType(nsIIncrementalStreamLoader* aLoader); + + // ScriptLoader which will handle the parsed script. + RefPtr<ScriptLoader> mScriptLoader; + + // The ScriptLoadRequest for this load. Decoded data are accumulated on it. + RefPtr<ScriptLoadRequest> mRequest; + + // SRI data verifier. + UniquePtr<SRICheckDataVerifier> mSRIDataVerifier; + + // Status of SRI data operations. + nsresult mSRIStatus; + + // Unicode decoder for charset. + mozilla::UniquePtr<mozilla::Decoder> mDecoder; + + // Flipped to true after calling NotifyStart the first time + bool mPreloadStartNotified = false; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ScriptLoadHandler_h diff --git a/dom/script/ScriptLoadRequest.cpp b/dom/script/ScriptLoadRequest.cpp new file mode 100644 index 0000000000..08c72a118c --- /dev/null +++ b/dom/script/ScriptLoadRequest.cpp @@ -0,0 +1,319 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "ScriptLoadRequest.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/Unused.h" +#include "mozilla/Utf8.h" // mozilla::Utf8Unit + +#include "ModuleLoadRequest.h" +#include "nsContentUtils.h" +#include "nsICacheInfoChannel.h" +#include "nsIClassOfService.h" +#include "nsISupportsPriority.h" +#include "ScriptLoadRequest.h" +#include "ScriptSettings.h" + +namespace mozilla { +namespace dom { + +////////////////////////////////////////////////////////////// +// ScriptFetchOptions +////////////////////////////////////////////////////////////// + +NS_IMPL_CYCLE_COLLECTION(ScriptFetchOptions, mElement, mTriggeringPrincipal) + +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(ScriptFetchOptions, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(ScriptFetchOptions, Release) + +ScriptFetchOptions::ScriptFetchOptions(mozilla::CORSMode aCORSMode, + ReferrerPolicy aReferrerPolicy, + Element* aElement, + nsIPrincipal* aTriggeringPrincipal) + : mCORSMode(aCORSMode), + mReferrerPolicy(aReferrerPolicy), + mIsPreload(false), + mElement(aElement), + mTriggeringPrincipal(aTriggeringPrincipal) { + MOZ_ASSERT(mTriggeringPrincipal); +} + +ScriptFetchOptions::~ScriptFetchOptions() = default; + +////////////////////////////////////////////////////////////// +// ScriptLoadRequest +////////////////////////////////////////////////////////////// + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ScriptLoadRequest) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ScriptLoadRequest) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ScriptLoadRequest) + +NS_IMPL_CYCLE_COLLECTION_CLASS(ScriptLoadRequest) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFetchOptions, mCacheInfo) + tmp->mScript = nullptr; + if (Runnable* runnable = tmp->mRunnable.exchange(nullptr)) { + runnable->Release(); + } + tmp->DropBytecodeCacheReferences(); + tmp->MaybeUnblockOnload(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(ScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFetchOptions, mCacheInfo, + mLoadBlockedDocument) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(ScriptLoadRequest) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mScript) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +ScriptLoadRequest::ScriptLoadRequest(ScriptKind aKind, nsIURI* aURI, + ScriptFetchOptions* aFetchOptions, + const SRIMetadata& aIntegrity, + nsIURI* aReferrer) + : mKind(aKind), + mScriptMode(ScriptMode::eBlocking), + mProgress(Progress::eLoading), + mDataType(DataType::eUnknown), + mScriptFromHead(false), + mIsInline(true), + mInDeferList(false), + mInAsyncList(false), + mIsNonAsyncScriptInserted(false), + mIsXSLT(false), + mIsCanceled(false), + mWasCompiledOMT(false), + mIsTracking(false), + mFetchOptions(aFetchOptions), + mOffThreadToken(nullptr), + mRunnable(nullptr), + mScriptTextLength(0), + mScriptBytecode(), + mBytecodeOffset(0), + mURI(aURI), + mLineNo(1), + mIntegrity(aIntegrity), + mReferrer(aReferrer), + mUnreportedPreloadError(NS_OK) { + MOZ_ASSERT(mFetchOptions); +} + +ScriptLoadRequest::~ScriptLoadRequest() { + // When speculative parsing is enabled, it is possible to off-main-thread + // compile scripts that are never executed. These should be cleaned up here + // if they exist. + MOZ_ASSERT_IF( + !StaticPrefs:: + dom_script_loader_external_scripts_speculative_omt_parse_enabled(), + !mOffThreadToken); + + MaybeCancelOffThreadScript(); + + if (mScript) { + DropBytecodeCacheReferences(); + } + + MaybeUnblockOnload(); + DropJSObjects(this); +} + +void ScriptLoadRequest::BlockOnload(Document* aDocument) { + MOZ_ASSERT(!mLoadBlockedDocument); + aDocument->BlockOnload(); + mLoadBlockedDocument = aDocument; +} + +void ScriptLoadRequest::MaybeUnblockOnload() { + if (mLoadBlockedDocument) { + mLoadBlockedDocument->UnblockOnload(false); + mLoadBlockedDocument = nullptr; + } +} + +void ScriptLoadRequest::SetReady() { + MOZ_ASSERT(mProgress != Progress::eReady); + mProgress = Progress::eReady; +} + +void ScriptLoadRequest::Cancel() { + MaybeCancelOffThreadScript(); + mIsCanceled = true; +} + +void ScriptLoadRequest::MaybeCancelOffThreadScript() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mOffThreadToken) { + return; + } + + JSContext* cx = danger::GetJSContext(); + // Follow the same conditions as ScriptLoader::AttemptAsyncScriptCompile + if (IsModuleRequest()) { + JS::CancelOffThreadModule(cx, mOffThreadToken); + } else if (IsSource()) { + JS::CancelOffThreadScript(cx, mOffThreadToken); + } else { + MOZ_ASSERT(IsBytecode()); + JS::CancelOffThreadScriptDecoder(cx, mOffThreadToken); + } + + // Cancellation request above should guarantee removal of the parse task, so + // releasing the runnable should be safe to do here. + if (Runnable* runnable = mRunnable.exchange(nullptr)) { + runnable->Release(); + } + + MaybeUnblockOnload(); + mOffThreadToken = nullptr; +} + +void ScriptLoadRequest::DropBytecodeCacheReferences() { + mCacheInfo = nullptr; + mScript = nullptr; + DropJSObjects(this); +} + +inline ModuleLoadRequest* ScriptLoadRequest::AsModuleRequest() { + MOZ_ASSERT(IsModuleRequest()); + return static_cast<ModuleLoadRequest*>(this); +} + +void ScriptLoadRequest::SetScriptMode(bool aDeferAttr, bool aAsyncAttr, + bool aLinkPreload) { + if (aLinkPreload) { + mScriptMode = ScriptMode::eLinkPreload; + } else if (aAsyncAttr) { + mScriptMode = ScriptMode::eAsync; + } else if (aDeferAttr || IsModuleRequest()) { + mScriptMode = ScriptMode::eDeferred; + } else { + mScriptMode = ScriptMode::eBlocking; + } +} + +void ScriptLoadRequest::SetUnknownDataType() { + mDataType = DataType::eUnknown; + mScriptData.reset(); +} + +void ScriptLoadRequest::SetTextSource() { + MOZ_ASSERT(IsUnknownDataType()); + mDataType = DataType::eTextSource; + if (StaticPrefs::dom_script_loader_external_scripts_utf8_parsing_enabled()) { + mScriptData.emplace(VariantType<ScriptTextBuffer<Utf8Unit>>()); + } else { + mScriptData.emplace(VariantType<ScriptTextBuffer<char16_t>>()); + } +} + +void ScriptLoadRequest::SetBinASTSource() { MOZ_CRASH("BinAST not supported"); } + +void ScriptLoadRequest::SetBytecode() { + MOZ_ASSERT(IsUnknownDataType()); + mDataType = DataType::eBytecode; +} + +bool ScriptLoadRequest::ShouldAcceptBinASTEncoding() const { + MOZ_CRASH("BinAST not supported"); +} + +void ScriptLoadRequest::ClearScriptSource() { + if (IsTextSource()) { + ClearScriptText(); + } else if (IsBinASTSource()) { + ScriptBinASTData().clearAndFree(); + } +} + +void ScriptLoadRequest::SetScript(JSScript* aScript) { + MOZ_ASSERT(!mScript); + mScript = aScript; + HoldJSObjects(this); +} + +// static +void ScriptLoadRequest::PrioritizeAsPreload(nsIChannel* aChannel) { + if (nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(aChannel)) { + cos->AddClassFlags(nsIClassOfService::Unblocked); + } + if (nsCOMPtr<nsISupportsPriority> sp = do_QueryInterface(aChannel)) { + sp->AdjustPriority(nsISupportsPriority::PRIORITY_HIGHEST); + } +} + +void ScriptLoadRequest::PrioritizeAsPreload() { + if (!IsLinkPreloadScript()) { + // Do the prioritization only if this request has not already been created + // as a preload. + PrioritizeAsPreload(Channel()); + } +} + +nsIScriptElement* ScriptLoadRequest::GetScriptElement() const { + nsCOMPtr<nsIScriptElement> scriptElement = + do_QueryInterface(mFetchOptions->mElement); + return scriptElement; +} + +void ScriptLoadRequest::SetIsLoadRequest(nsIScriptElement* aElement) { + MOZ_ASSERT(aElement); + MOZ_ASSERT(!GetScriptElement()); + MOZ_ASSERT(IsPreload()); + mFetchOptions->mElement = do_QueryInterface(aElement); + mFetchOptions->mIsPreload = false; +} + +////////////////////////////////////////////////////////////// +// ScriptLoadRequestList +////////////////////////////////////////////////////////////// + +ScriptLoadRequestList::~ScriptLoadRequestList() { Clear(); } + +void ScriptLoadRequestList::Clear() { + while (!isEmpty()) { + RefPtr<ScriptLoadRequest> first = StealFirst(); + first->Cancel(); + // And just let it go out of scope and die. + } +} + +#ifdef DEBUG +bool ScriptLoadRequestList::Contains(ScriptLoadRequest* aElem) const { + for (const ScriptLoadRequest* req = getFirst(); req; req = req->getNext()) { + if (req == aElem) { + return true; + } + } + + return false; +} +#endif // DEBUG + +inline void ImplCycleCollectionUnlink(ScriptLoadRequestList& aField) { + while (!aField.isEmpty()) { + RefPtr<ScriptLoadRequest> first = aField.StealFirst(); + } +} + +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, + ScriptLoadRequestList& aField, const char* aName, uint32_t aFlags) { + for (ScriptLoadRequest* request = aField.getFirst(); request; + request = request->getNext()) { + CycleCollectionNoteChild(aCallback, request, aName, aFlags); + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/script/ScriptLoadRequest.h b/dom/script/ScriptLoadRequest.h new file mode 100644 index 0000000000..cb1c50b7ea --- /dev/null +++ b/dom/script/ScriptLoadRequest.h @@ -0,0 +1,409 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_ScriptLoadRequest_h +#define mozilla_dom_ScriptLoadRequest_h + +#include "js/AllocPolicy.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "mozilla/Atomics.h" +#include "mozilla/Assertions.h" +#include "mozilla/CORSMode.h" +#include "mozilla/dom/SRIMetadata.h" +#include "mozilla/LinkedList.h" +#include "mozilla/Maybe.h" +#include "mozilla/PreloaderBase.h" +#include "mozilla/Utf8.h" // mozilla::Utf8Unit +#include "mozilla/Variant.h" +#include "mozilla/Vector.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIScriptElement.h" +#include "ScriptKind.h" + +class nsICacheInfoChannel; + +namespace JS { +class OffThreadToken; +} + +namespace mozilla { +namespace dom { + +class Element; +class ModuleLoadRequest; +class ScriptLoadRequestList; + +/* + * Some options used when fetching script resources. This only loosely + * corresponds to HTML's "script fetch options". + * + * These are common to all modules in a module graph, and hence a single + * instance is shared by all ModuleLoadRequest objects in a graph. + */ + +class ScriptFetchOptions { + ~ScriptFetchOptions(); + + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(ScriptFetchOptions) + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(ScriptFetchOptions) + + ScriptFetchOptions(mozilla::CORSMode aCORSMode, + enum ReferrerPolicy aReferrerPolicy, Element* aElement, + nsIPrincipal* aTriggeringPrincipal); + + const mozilla::CORSMode mCORSMode; + const enum ReferrerPolicy mReferrerPolicy; + bool mIsPreload; + nsCOMPtr<Element> mElement; + nsCOMPtr<nsIPrincipal> mTriggeringPrincipal; +}; + +/* + * A class that handles loading and evaluation of <script> elements. + */ + +class ScriptLoadRequest + : public PreloaderBase, + private mozilla::LinkedListElement<ScriptLoadRequest> { + typedef LinkedListElement<ScriptLoadRequest> super; + + // Allow LinkedListElement<ScriptLoadRequest> to cast us to itself as needed. + friend class mozilla::LinkedListElement<ScriptLoadRequest>; + friend class ScriptLoadRequestList; + + protected: + virtual ~ScriptLoadRequest(); + + public: + ScriptLoadRequest(ScriptKind aKind, nsIURI* aURI, + ScriptFetchOptions* aFetchOptions, + const SRIMetadata& aIntegrity, nsIURI* aReferrer); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ScriptLoadRequest) + + // PreloaderBase + static void PrioritizeAsPreload(nsIChannel* aChannel); + virtual void PrioritizeAsPreload() override; + + bool IsModuleRequest() const { return mKind == ScriptKind::eModule; } + + ModuleLoadRequest* AsModuleRequest(); + +#ifdef MOZ_GECKO_PROFILER + TimeStamp mOffThreadParseStartTime; + TimeStamp mOffThreadParseStopTime; +#endif + + void FireScriptAvailable(nsresult aResult) { + bool isInlineClassicScript = mIsInline && !IsModuleRequest(); + GetScriptElement()->ScriptAvailable(aResult, GetScriptElement(), + isInlineClassicScript, mURI, mLineNo); + } + void FireScriptEvaluated(nsresult aResult) { + GetScriptElement()->ScriptEvaluated(aResult, GetScriptElement(), mIsInline); + } + + bool IsPreload() const { + MOZ_ASSERT_IF(mFetchOptions->mIsPreload, !GetScriptElement()); + return mFetchOptions->mIsPreload; + } + + virtual void Cancel(); + + bool IsCanceled() const { return mIsCanceled; } + + virtual void SetReady(); + + JS::OffThreadToken** OffThreadTokenPtr() { + return mOffThreadToken ? &mOffThreadToken : nullptr; + } + + bool IsTracking() const { return mIsTracking; } + void SetIsTracking() { + MOZ_ASSERT(!mIsTracking); + mIsTracking = true; + } + + void BlockOnload(Document* aDocument); + + void MaybeUnblockOnload(); + + enum class Progress : uint8_t { + eLoading, // Request either source or bytecode + eLoading_Source, // Explicitly Request source stream + eCompiling, + eFetchingImports, + eReady + }; + + bool IsReadyToRun() const { return mProgress == Progress::eReady; } + bool IsLoading() const { + return mProgress == Progress::eLoading || + mProgress == Progress::eLoading_Source; + } + bool IsLoadingSource() const { + return mProgress == Progress::eLoading_Source; + } + bool InCompilingStage() const { + return mProgress == Progress::eCompiling || + (IsReadyToRun() && mWasCompiledOMT); + } + + // Type of data provided by the nsChannel. + enum class DataType : uint8_t { + eUnknown, + eTextSource, + eBinASTSource, + eBytecode + }; + + bool IsUnknownDataType() const { return mDataType == DataType::eUnknown; } + bool IsTextSource() const { return mDataType == DataType::eTextSource; } + bool IsBinASTSource() const { return false; } + bool IsSource() const { return IsTextSource() || IsBinASTSource(); } + bool IsBytecode() const { return mDataType == DataType::eBytecode; } + + void SetUnknownDataType(); + void SetTextSource(); + void SetBinASTSource(); + void SetBytecode(); + + // Use a vector backed by the JS allocator for script text so that contents + // can be transferred in constant time to the JS engine, not copied in linear + // time. + template <typename Unit> + using ScriptTextBuffer = Vector<Unit, 0, js::MallocAllocPolicy>; + + // BinAST data isn't transferred to the JS engine, so it doesn't need to use + // the JS allocator. + using BinASTSourceBuffer = Vector<uint8_t>; + + bool IsUTF16Text() const { + return mScriptData->is<ScriptTextBuffer<char16_t>>(); + } + bool IsUTF8Text() const { + return mScriptData->is<ScriptTextBuffer<Utf8Unit>>(); + } + + template <typename Unit> + const ScriptTextBuffer<Unit>& ScriptText() const { + MOZ_ASSERT(IsTextSource()); + return mScriptData->as<ScriptTextBuffer<Unit>>(); + } + template <typename Unit> + ScriptTextBuffer<Unit>& ScriptText() { + MOZ_ASSERT(IsTextSource()); + return mScriptData->as<ScriptTextBuffer<Unit>>(); + } + + const BinASTSourceBuffer& ScriptBinASTData() const { + MOZ_ASSERT(IsBinASTSource()); + return mScriptData->as<BinASTSourceBuffer>(); + } + BinASTSourceBuffer& ScriptBinASTData() { + MOZ_ASSERT(IsBinASTSource()); + return mScriptData->as<BinASTSourceBuffer>(); + } + + size_t ScriptTextLength() const { + MOZ_ASSERT(IsTextSource()); + return IsUTF16Text() ? ScriptText<char16_t>().length() + : ScriptText<Utf8Unit>().length(); + } + + void ClearScriptText() { + MOZ_ASSERT(IsTextSource()); + return IsUTF16Text() ? ScriptText<char16_t>().clearAndFree() + : ScriptText<Utf8Unit>().clearAndFree(); + } + + enum class ScriptMode : uint8_t { + eBlocking, + eDeferred, + eAsync, + eLinkPreload // this is a load initiated by <link rel="preload" + // as="script"> tag + }; + + void SetScriptMode(bool aDeferAttr, bool aAsyncAttr, bool aLinkPreload); + + bool IsLinkPreloadScript() const { + return mScriptMode == ScriptMode::eLinkPreload; + } + + bool IsBlockingScript() const { return mScriptMode == ScriptMode::eBlocking; } + + bool IsDeferredScript() const { return mScriptMode == ScriptMode::eDeferred; } + + bool IsAsyncScript() const { return mScriptMode == ScriptMode::eAsync; } + + virtual bool IsTopLevel() const { + // Classic scripts are always top level. + return true; + } + + mozilla::CORSMode CORSMode() const { return mFetchOptions->mCORSMode; } + enum ReferrerPolicy ReferrerPolicy() const { + return mFetchOptions->mReferrerPolicy; + } + nsIScriptElement* GetScriptElement() const; + nsIPrincipal* TriggeringPrincipal() const { + return mFetchOptions->mTriggeringPrincipal; + } + + // Make this request a preload (speculative) request. + void SetIsPreloadRequest() { + MOZ_ASSERT(!GetScriptElement()); + MOZ_ASSERT(!IsPreload()); + mFetchOptions->mIsPreload = true; + } + + // Make a preload request into an actual load request for the given element. + void SetIsLoadRequest(nsIScriptElement* aElement); + + FromParser GetParserCreated() const { + nsIScriptElement* element = GetScriptElement(); + if (!element) { + return NOT_FROM_PARSER; + } + return element->GetParserCreated(); + } + + bool ShouldAcceptBinASTEncoding() const; + + void ClearScriptSource(); + + void SetScript(JSScript* aScript); + + void MaybeCancelOffThreadScript(); + void DropBytecodeCacheReferences(); + + using super::getNext; + using super::isInList; + + const ScriptKind mKind; // Whether this is a classic script or a module + // script. + ScriptMode mScriptMode; // Whether this is a blocking, defer or async script. + Progress mProgress; // Are we still waiting for a load to complete? + DataType mDataType; // Does this contain Source or Bytecode? + bool mScriptFromHead; // Synchronous head script block loading of other non + // js/css content. + bool mIsInline; // Is the script inline or loaded? + bool mInDeferList; // True if we live in mDeferRequests. + bool mInAsyncList; // True if we live in mLoadingAsyncRequests or + // mLoadedAsyncRequests. + bool mIsNonAsyncScriptInserted; // True if we live in + // mNonAsyncExternalScriptInsertedRequests + bool mIsXSLT; // True if we live in mXSLTRequests. + bool mIsCanceled; // True if we have been explicitly canceled. + bool mWasCompiledOMT; // True if the script has been compiled off main + // thread. + bool mIsTracking; // True if the script comes from a source on our + // tracking protection list. + + RefPtr<ScriptFetchOptions> mFetchOptions; + + JS::OffThreadToken* mOffThreadToken; // Off-thread parsing token. + Maybe<nsString> mSourceMapURL; // Holds source map url for loaded scripts + + Atomic<Runnable*> mRunnable; // Runnable created when dispatching off thread + // compile. Tracked here so that it can be + // properly released during cancellation. + + // Holds the top-level JSScript that corresponds to the current source, once + // it is parsed, and planned to be saved in the bytecode cache. + JS::Heap<JSScript*> mScript; + + // Holds script source data for non-inline scripts. + Maybe<Variant<ScriptTextBuffer<char16_t>, ScriptTextBuffer<Utf8Unit>, + BinASTSourceBuffer>> + mScriptData; + + // The length of script source text, set when reading completes. This is used + // since mScriptData is cleared when the source is passed to the JS engine. + size_t mScriptTextLength; + + // Holds the SRI serialized hash and the script bytecode for non-inline + // scripts. + mozilla::Vector<uint8_t> mScriptBytecode; + uint32_t mBytecodeOffset; // Offset of the bytecode in mScriptBytecode + + const nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIPrincipal> mOriginPrincipal; + nsAutoCString + mURL; // Keep the URI's filename alive during off thread parsing. + int32_t mLineNo; + const SRIMetadata mIntegrity; + const nsCOMPtr<nsIURI> mReferrer; + + // Non-null if there is a document that this request is blocking from loading. + RefPtr<Document> mLoadBlockedDocument; + + // Holds the Cache information, which is used to register the bytecode + // on the cache entry, such that we can load it the next time. + nsCOMPtr<nsICacheInfoChannel> mCacheInfo; + + // The base URL used for resolving relative module imports. + nsCOMPtr<nsIURI> mBaseURL; + + // For preload requests, we defer reporting errors to the console until the + // request is used. + nsresult mUnreportedPreloadError; +}; + +class ScriptLoadRequestList : private mozilla::LinkedList<ScriptLoadRequest> { + typedef mozilla::LinkedList<ScriptLoadRequest> super; + + public: + ~ScriptLoadRequestList(); + + void Clear(); + +#ifdef DEBUG + bool Contains(ScriptLoadRequest* aElem) const; +#endif // DEBUG + + using super::getFirst; + using super::isEmpty; + + void AppendElement(ScriptLoadRequest* aElem) { + MOZ_ASSERT(!aElem->isInList()); + NS_ADDREF(aElem); + insertBack(aElem); + } + + MOZ_MUST_USE + already_AddRefed<ScriptLoadRequest> Steal(ScriptLoadRequest* aElem) { + aElem->removeFrom(*this); + return dont_AddRef(aElem); + } + + MOZ_MUST_USE + already_AddRefed<ScriptLoadRequest> StealFirst() { + MOZ_ASSERT(!isEmpty()); + return Steal(getFirst()); + } + + void Remove(ScriptLoadRequest* aElem) { + aElem->removeFrom(*this); + NS_RELEASE(aElem); + } +}; + +void ImplCycleCollectionUnlink(ScriptLoadRequestList& aField); + +void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback, + ScriptLoadRequestList& aField, + const char* aName, uint32_t aFlags); + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ScriptLoadRequest_h diff --git a/dom/script/ScriptLoader.cpp b/dom/script/ScriptLoader.cpp new file mode 100644 index 0000000000..501c953e92 --- /dev/null +++ b/dom/script/ScriptLoader.cpp @@ -0,0 +1,4343 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "ScriptLoader.h" +#include "ScriptLoadHandler.h" +#include "ScriptLoadRequest.h" +#include "ScriptTrace.h" +#include "LoadedScript.h" +#include "ModuleLoadRequest.h" + +#include "prsystem.h" +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/Array.h" // JS::GetArrayLength +#include "js/CompilationAndEvaluation.h" +#include "js/ContextOptions.h" // JS::ContextOptionsRef +#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* +#include "js/MemoryFunctions.h" +#include "js/Modules.h" // JS::FinishDynamicModuleImport, JS::{G,S}etModuleResolveHook, JS::Get{ModulePrivate,ModuleScript,RequestedModule{s,Specifier,SourcePos}}, JS::SetModule{DynamicImport,Metadata}Hook +#include "js/OffThreadScriptCompilation.h" +#include "js/Realm.h" +#include "js/SourceText.h" +#include "js/Utility.h" +#include "xpcpublic.h" +#include "GeckoProfiler.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIContent.h" +#include "nsJSUtils.h" +#include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/JSExecutionContext.h" +#include "mozilla/dom/ScriptDecoding.h" // mozilla::dom::ScriptDecoding +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/SRILogHelper.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/net/UrlClassifierFeatureFactory.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_network.h" +#include "nsAboutProtocolUtils.h" +#include "nsGkAtoms.h" +#include "nsNetUtil.h" +#include "nsGlobalWindowInner.h" +#include "nsIScriptGlobalObject.h" +#include "nsIScriptContext.h" +#include "nsIPrincipal.h" +#include "nsJSPrincipals.h" +#include "nsContentPolicyUtils.h" +#include "nsIClassifiedChannel.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIClassOfService.h" +#include "nsICacheInfoChannel.h" +#include "nsITimedChannel.h" +#include "nsIScriptElement.h" +#include "nsISupportsPriority.h" +#include "nsIDocShell.h" +#include "nsContentUtils.h" +#include "nsUnicharUtils.h" +#include "nsError.h" +#include "nsThreadUtils.h" +#include "nsDocShellCID.h" +#include "nsIContentSecurityPolicy.h" +#include "mozilla/Logging.h" +#include "nsCRT.h" +#include "nsContentCreatorFunctions.h" +#include "nsProxyRelease.h" +#include "nsSandboxFlags.h" +#include "nsContentTypeParser.h" +#include "nsINetworkPredictor.h" +#include "nsMimeTypes.h" +#include "mozilla/ConsoleReportCollector.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/LoadInfo.h" +#include "ReferrerInfo.h" + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/Attributes.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "mozilla/Utf8.h" // mozilla::Utf8Unit +#include "nsIScriptError.h" +#include "nsIAsyncOutputStream.h" + +using JS::SourceText; + +using mozilla::Telemetry::LABELS_DOM_SCRIPT_PRELOAD_RESULT; + +namespace mozilla { +namespace dom { + +LazyLogModule ScriptLoader::gCspPRLog("CSP"); +LazyLogModule ScriptLoader::gScriptLoaderLog("ScriptLoader"); + +#undef LOG +#define LOG(args) \ + MOZ_LOG(ScriptLoader::gScriptLoaderLog, mozilla::LogLevel::Debug, args) + +#define LOG_ENABLED() \ + MOZ_LOG_TEST(ScriptLoader::gScriptLoaderLog, mozilla::LogLevel::Debug) + +// Alternate Data MIME type used by the ScriptLoader to register that we want to +// store bytecode without reading it. +static constexpr auto kNullMimeType = "javascript/null"_ns; + +///////////////////////////////////////////////////////////// +// AsyncCompileShutdownObserver +///////////////////////////////////////////////////////////// + +NS_IMPL_ISUPPORTS(AsyncCompileShutdownObserver, nsIObserver) + +void AsyncCompileShutdownObserver::OnShutdown() { + if (mScriptLoader) { + mScriptLoader->Shutdown(); + Unregister(); + } +} + +void AsyncCompileShutdownObserver::Unregister() { + if (mScriptLoader) { + mScriptLoader = nullptr; + nsContentUtils::UnregisterShutdownObserver(this); + } +} + +NS_IMETHODIMP +AsyncCompileShutdownObserver::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + OnShutdown(); + return NS_OK; +} + +////////////////////////////////////////////////////////////// +// ScriptLoader::PreloadInfo +////////////////////////////////////////////////////////////// + +inline void ImplCycleCollectionUnlink(ScriptLoader::PreloadInfo& aField) { + ImplCycleCollectionUnlink(aField.mRequest); +} + +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, + ScriptLoader::PreloadInfo& aField, const char* aName, uint32_t aFlags = 0) { + ImplCycleCollectionTraverse(aCallback, aField.mRequest, aName, aFlags); +} + +////////////////////////////////////////////////////////////// +// ScriptLoader +////////////////////////////////////////////////////////////// + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ScriptLoader) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(ScriptLoader, mNonAsyncExternalScriptInsertedRequests, + mLoadingAsyncRequests, mLoadedAsyncRequests, + mDeferRequests, mXSLTRequests, mDynamicImportRequests, + mParserBlockingRequest, mBytecodeEncodingQueue, + mPreloads, mPendingChildLoaders, mFetchedModules) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ScriptLoader) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ScriptLoader) + +ScriptLoader::ScriptLoader(Document* aDocument) + : mDocument(aDocument), + mParserBlockingBlockerCount(0), + mBlockerCount(0), + mNumberOfProcessors(0), + mEnabled(true), + mDeferEnabled(false), + mSpeculativeOMTParsingEnabled(false), + mDeferCheckpointReached(false), + mBlockingDOMContentLoaded(false), + mLoadEventFired(false), + mGiveUpEncoding(false), + mReporter(new ConsoleReportCollector()) { + LOG(("ScriptLoader::ScriptLoader %p", this)); + EnsureModuleHooksInitialized(); + + mSpeculativeOMTParsingEnabled = StaticPrefs:: + dom_script_loader_external_scripts_speculative_omt_parse_enabled(); + + mShutdownObserver = new AsyncCompileShutdownObserver(this); + nsContentUtils::RegisterShutdownObserver(mShutdownObserver); +} + +ScriptLoader::~ScriptLoader() { + LOG(("ScriptLoader::~ScriptLoader %p", this)); + + mObservers.Clear(); + + if (mParserBlockingRequest) { + mParserBlockingRequest->FireScriptAvailable(NS_ERROR_ABORT); + } + + for (ScriptLoadRequest* req = mXSLTRequests.getFirst(); req; + req = req->getNext()) { + req->FireScriptAvailable(NS_ERROR_ABORT); + } + + for (ScriptLoadRequest* req = mDeferRequests.getFirst(); req; + req = req->getNext()) { + req->FireScriptAvailable(NS_ERROR_ABORT); + } + + for (ScriptLoadRequest* req = mLoadingAsyncRequests.getFirst(); req; + req = req->getNext()) { + req->FireScriptAvailable(NS_ERROR_ABORT); + } + + for (ScriptLoadRequest* req = mLoadedAsyncRequests.getFirst(); req; + req = req->getNext()) { + req->FireScriptAvailable(NS_ERROR_ABORT); + } + + for (ScriptLoadRequest* req = mDynamicImportRequests.getFirst(); req; + req = req->getNext()) { + FinishDynamicImportAndReject(req->AsModuleRequest(), NS_ERROR_ABORT); + } + + for (ScriptLoadRequest* req = + mNonAsyncExternalScriptInsertedRequests.getFirst(); + req; req = req->getNext()) { + req->FireScriptAvailable(NS_ERROR_ABORT); + } + + // Unblock the kids, in case any of them moved to a different document + // subtree in the meantime and therefore aren't actually going away. + for (uint32_t j = 0; j < mPendingChildLoaders.Length(); ++j) { + mPendingChildLoaders[j]->RemoveParserBlockingScriptExecutionBlocker(); + } + + for (size_t i = 0; i < mPreloads.Length(); i++) { + AccumulateCategorical(LABELS_DOM_SCRIPT_PRELOAD_RESULT::NotUsed); + } + + if (mShutdownObserver) { + mShutdownObserver->Unregister(); + mShutdownObserver = nullptr; + } +} + +// Collect telemtry data about the cache information, and the kind of source +// which are being loaded, and where it is being loaded from. +static void CollectScriptTelemetry(ScriptLoadRequest* aRequest) { + using namespace mozilla::Telemetry; + + // Skip this function if we are not running telemetry. + if (!CanRecordExtended()) { + return; + } + + // Report the script kind. + if (aRequest->IsModuleRequest()) { + AccumulateCategorical(LABELS_DOM_SCRIPT_KIND::ModuleScript); + } else { + AccumulateCategorical(LABELS_DOM_SCRIPT_KIND::ClassicScript); + } + + // Report the type of source. This is used to monitor the status of the + // JavaScript Start-up Bytecode Cache, with the expectation of an almost zero + // source-fallback and alternate-data being roughtly equal to source loads. + if (aRequest->IsLoadingSource()) { + if (aRequest->mIsInline) { + AccumulateCategorical(LABELS_DOM_SCRIPT_LOADING_SOURCE::Inline); + } else if (aRequest->IsTextSource()) { + AccumulateCategorical(LABELS_DOM_SCRIPT_LOADING_SOURCE::SourceFallback); + } + // TODO: Add telemetry for BinAST encoded source. + } else { + MOZ_ASSERT(aRequest->IsLoading()); + if (aRequest->IsTextSource()) { + AccumulateCategorical(LABELS_DOM_SCRIPT_LOADING_SOURCE::Source); + } else if (aRequest->IsBytecode()) { + AccumulateCategorical(LABELS_DOM_SCRIPT_LOADING_SOURCE::AltData); + } + // TODO: Add telemetry for BinAST encoded source. + } +} + +// Helper method for checking if the script element is an event-handler +// This means that it has both a for-attribute and a event-attribute. +// Also, if the for-attribute has a value that matches "\s*window\s*", +// and the event-attribute matches "\s*onload([ \(].*)?" then it isn't an +// eventhandler. (both matches are case insensitive). +// This is how IE seems to filter out a window's onload handler from a +// <script for=... event=...> element. + +static bool IsScriptEventHandler(ScriptKind kind, nsIContent* aScriptElement) { + if (kind != ScriptKind::eClassic) { + return false; + } + + if (!aScriptElement->IsHTMLElement()) { + return false; + } + + nsAutoString forAttr, eventAttr; + if (!aScriptElement->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::_for, + forAttr) || + !aScriptElement->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::event, + eventAttr)) { + return false; + } + + const nsAString& for_str = + nsContentUtils::TrimWhitespace<nsCRT::IsAsciiSpace>(forAttr); + if (!for_str.LowerCaseEqualsLiteral("window")) { + return true; + } + + // We found for="window", now check for event="onload". + const nsAString& event_str = + nsContentUtils::TrimWhitespace<nsCRT::IsAsciiSpace>(eventAttr, false); + if (!StringBeginsWith(event_str, u"onload"_ns, + nsCaseInsensitiveStringComparator)) { + // It ain't "onload.*". + + return true; + } + + nsAutoString::const_iterator start, end; + event_str.BeginReading(start); + event_str.EndReading(end); + + start.advance(6); // advance past "onload" + + if (start != end && *start != '(' && *start != ' ') { + // We got onload followed by something other than space or + // '('. Not good enough. + + return true; + } + + return false; +} + +nsContentPolicyType ScriptLoadRequestToContentPolicyType( + ScriptLoadRequest* aRequest) { + if (aRequest->IsPreload()) { + return aRequest->IsModuleRequest() + ? nsIContentPolicy::TYPE_INTERNAL_MODULE_PRELOAD + : nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD; + } + + return aRequest->IsModuleRequest() ? nsIContentPolicy::TYPE_INTERNAL_MODULE + : nsIContentPolicy::TYPE_INTERNAL_SCRIPT; +} + +nsresult ScriptLoader::CheckContentPolicy(Document* aDocument, + nsISupports* aContext, + const nsAString& aType, + ScriptLoadRequest* aRequest) { + nsContentPolicyType contentPolicyType = + ScriptLoadRequestToContentPolicyType(aRequest); + + nsCOMPtr<nsINode> requestingNode = do_QueryInterface(aContext); + nsCOMPtr<nsILoadInfo> secCheckLoadInfo = new net::LoadInfo( + aDocument->NodePrincipal(), // loading principal + aDocument->NodePrincipal(), // triggering principal + requestingNode, nsILoadInfo::SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK, + contentPolicyType); + + // snapshot the nonce at load start time for performing CSP checks + if (contentPolicyType == nsIContentPolicy::TYPE_INTERNAL_SCRIPT || + contentPolicyType == nsIContentPolicy::TYPE_INTERNAL_MODULE) { + nsCOMPtr<nsINode> node = do_QueryInterface(aContext); + if (node) { + nsString* cspNonce = + static_cast<nsString*>(node->GetProperty(nsGkAtoms::nonce)); + if (cspNonce) { + secCheckLoadInfo->SetCspNonce(*cspNonce); + } + } + } + + int16_t shouldLoad = nsIContentPolicy::ACCEPT; + nsresult rv = NS_CheckContentLoadPolicy( + aRequest->mURI, secCheckLoadInfo, NS_LossyConvertUTF16toASCII(aType), + &shouldLoad, nsContentUtils::GetContentPolicy()); + if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) { + if (NS_FAILED(rv) || shouldLoad != nsIContentPolicy::REJECT_TYPE) { + return NS_ERROR_CONTENT_BLOCKED; + } + return NS_ERROR_CONTENT_BLOCKED_SHOW_ALT; + } + + return NS_OK; +} + +/* static */ +bool ScriptLoader::IsAboutPageLoadingChromeURI(ScriptLoadRequest* aRequest, + Document* aDocument) { + // if the uri to be loaded is not of scheme chrome:, there is nothing to do. + if (!aRequest->mURI->SchemeIs("chrome")) { + return false; + } + + // we can either get here with a regular contentPrincipal or with a + // NullPrincipal in case we are showing an error page in a sandboxed iframe. + // In either case if the about: page is linkable from content, there is + // nothing to do. + uint32_t aboutModuleFlags = 0; + nsresult rv = NS_OK; + + nsCOMPtr<nsIPrincipal> triggeringPrincipal = aRequest->TriggeringPrincipal(); + if (triggeringPrincipal->GetIsContentPrincipal()) { + if (!triggeringPrincipal->SchemeIs("about")) { + return false; + } + rv = triggeringPrincipal->GetAboutModuleFlags(&aboutModuleFlags); + NS_ENSURE_SUCCESS(rv, false); + } else if (triggeringPrincipal->GetIsNullPrincipal()) { + nsCOMPtr<nsIURI> docURI = aDocument->GetDocumentURI(); + if (!docURI->SchemeIs("about")) { + return false; + } + + nsCOMPtr<nsIAboutModule> aboutModule; + rv = NS_GetAboutModule(docURI, getter_AddRefs(aboutModule)); + if (NS_FAILED(rv) || !aboutModule) { + return false; + } + rv = aboutModule->GetURIFlags(docURI, &aboutModuleFlags); + NS_ENSURE_SUCCESS(rv, false); + } else { + return false; + } + + if (aboutModuleFlags & nsIAboutModule::MAKE_LINKABLE) { + return false; + } + + // seems like an about page wants to load a chrome URI. + return true; +} + +bool ScriptLoader::ModuleMapContainsURL(nsIURI* aURL) const { + // Returns whether we have fetched, or are currently fetching, a module script + // for a URL. + return mFetchingModules.Contains(aURL) || mFetchedModules.Contains(aURL); +} + +bool ScriptLoader::IsFetchingModule(ModuleLoadRequest* aRequest) const { + bool fetching = mFetchingModules.Contains(aRequest->mURI); + MOZ_ASSERT_IF(fetching, !mFetchedModules.Contains(aRequest->mURI)); + return fetching; +} + +void ScriptLoader::SetModuleFetchStarted(ModuleLoadRequest* aRequest) { + // Update the module map to indicate that a module is currently being fetched. + + MOZ_ASSERT(aRequest->IsLoading()); + MOZ_ASSERT(!ModuleMapContainsURL(aRequest->mURI)); + mFetchingModules.Put(aRequest->mURI, + RefPtr<GenericNonExclusivePromise::Private>{}); +} + +void ScriptLoader::SetModuleFetchFinishedAndResumeWaitingRequests( + ModuleLoadRequest* aRequest, nsresult aResult) { + // Update module map with the result of fetching a single module script. + // + // If any requests for the same URL are waiting on this one to complete, they + // will have ModuleLoaded or LoadFailed on them when the promise is + // resolved/rejected. This is set up in StartLoad. + + LOG( + ("ScriptLoadRequest (%p): Module fetch finished (script == %p, result == " + "%u)", + aRequest, aRequest->mModuleScript.get(), unsigned(aResult))); + + RefPtr<GenericNonExclusivePromise::Private> promise; + MOZ_ALWAYS_TRUE( + mFetchingModules.Remove(aRequest->mURI, getter_AddRefs(promise))); + + RefPtr<ModuleScript> moduleScript(aRequest->mModuleScript); + MOZ_ASSERT(NS_FAILED(aResult) == !moduleScript); + + mFetchedModules.Put(aRequest->mURI, RefPtr{moduleScript}); + + if (promise) { + if (moduleScript) { + LOG(("ScriptLoadRequest (%p): resolving %p", aRequest, promise.get())); + promise->Resolve(true, __func__); + } else { + LOG(("ScriptLoadRequest (%p): rejecting %p", aRequest, promise.get())); + promise->Reject(aResult, __func__); + } + } +} + +RefPtr<GenericNonExclusivePromise> ScriptLoader::WaitForModuleFetch( + nsIURI* aURL) { + MOZ_ASSERT(ModuleMapContainsURL(aURL)); + + if (auto entry = mFetchingModules.Lookup(aURL)) { + if (!entry.Data()) { + entry.Data() = new GenericNonExclusivePromise::Private(__func__); + } + return entry.Data(); + } + + RefPtr<ModuleScript> ms; + MOZ_ALWAYS_TRUE(mFetchedModules.Get(aURL, getter_AddRefs(ms))); + if (!ms) { + return GenericNonExclusivePromise::CreateAndReject(NS_ERROR_FAILURE, + __func__); + } + + return GenericNonExclusivePromise::CreateAndResolve(true, __func__); +} + +ModuleScript* ScriptLoader::GetFetchedModule(nsIURI* aURL) const { + if (LOG_ENABLED()) { + nsAutoCString url; + aURL->GetAsciiSpec(url); + LOG(("GetFetchedModule %s", url.get())); + } + + bool found; + ModuleScript* ms = mFetchedModules.GetWeak(aURL, &found); + MOZ_ASSERT(found); + return ms; +} + +nsresult ScriptLoader::ProcessFetchedModuleSource(ModuleLoadRequest* aRequest) { + MOZ_ASSERT(!aRequest->mModuleScript); + + nsresult rv = CreateModuleScript(aRequest); + MOZ_ASSERT(NS_FAILED(rv) == !aRequest->mModuleScript); + + aRequest->ClearScriptSource(); + + if (NS_FAILED(rv)) { + aRequest->LoadFailed(); + return rv; + } + + if (!aRequest->mIsInline) { + SetModuleFetchFinishedAndResumeWaitingRequests(aRequest, rv); + } + + if (!aRequest->mModuleScript->HasParseError()) { + StartFetchingModuleDependencies(aRequest); + } + + return NS_OK; +} + +static nsresult ResolveRequestedModules(ModuleLoadRequest* aRequest, + nsCOMArray<nsIURI>* aUrlsOut); + +nsresult ScriptLoader::CreateModuleScript(ModuleLoadRequest* aRequest) { + MOZ_ASSERT(!aRequest->mModuleScript); + MOZ_ASSERT(aRequest->mBaseURL); + + LOG(("ScriptLoadRequest (%p): Create module script", aRequest)); + + nsCOMPtr<nsIScriptGlobalObject> globalObject = GetScriptGlobalObject(); + if (!globalObject) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIScriptContext> context = globalObject->GetScriptContext(); + if (!context) { + return NS_ERROR_FAILURE; + } + + nsAutoMicroTask mt; + + AutoAllowLegacyScriptExecution exemption; + + AutoEntryScript aes(globalObject, "CompileModule", true); + + bool oldProcessingScriptTag = context->GetProcessingScriptTag(); + context->SetProcessingScriptTag(true); + + nsresult rv; + { + JSContext* cx = aes.cx(); + JS::Rooted<JSObject*> module(cx); + + if (aRequest->mWasCompiledOMT) { + module = JS::FinishOffThreadModule(cx, aRequest->mOffThreadToken); + aRequest->mOffThreadToken = nullptr; + rv = module ? NS_OK : NS_ERROR_FAILURE; + } else { + JS::Rooted<JSObject*> global(cx, globalObject->GetGlobalJSObject()); + + JS::CompileOptions options(cx); + rv = FillCompileOptionsForRequest(aes, aRequest, global, &options); + + if (NS_SUCCEEDED(rv)) { + MaybeSourceText maybeSource; + rv = GetScriptSource(cx, aRequest, &maybeSource); + if (NS_SUCCEEDED(rv)) { + rv = maybeSource.constructed<SourceText<char16_t>>() + ? nsJSUtils::CompileModule( + cx, maybeSource.ref<SourceText<char16_t>>(), global, + options, &module) + : nsJSUtils::CompileModule( + cx, maybeSource.ref<SourceText<Utf8Unit>>(), global, + options, &module); + } + } + } + + MOZ_ASSERT(NS_SUCCEEDED(rv) == (module != nullptr)); + + RefPtr<ModuleScript> moduleScript = + new ModuleScript(aRequest->mFetchOptions, aRequest->mBaseURL); + aRequest->mModuleScript = moduleScript; + + if (!module) { + LOG(("ScriptLoadRequest (%p): compilation failed (%d)", aRequest, + unsigned(rv))); + + MOZ_ASSERT(aes.HasException()); + JS::Rooted<JS::Value> error(cx); + if (!aes.StealException(&error)) { + aRequest->mModuleScript = nullptr; + return NS_ERROR_FAILURE; + } + + moduleScript->SetParseError(error); + aRequest->ModuleErrored(); + return NS_OK; + } + + moduleScript->SetModuleRecord(module); + + // Validate requested modules and treat failure to resolve module specifiers + // the same as a parse error. + rv = ResolveRequestedModules(aRequest, nullptr); + if (NS_FAILED(rv)) { + aRequest->ModuleErrored(); + return NS_OK; + } + } + + context->SetProcessingScriptTag(oldProcessingScriptTag); + + LOG(("ScriptLoadRequest (%p): module script == %p", aRequest, + aRequest->mModuleScript.get())); + + return rv; +} + +static nsresult HandleResolveFailure(JSContext* aCx, LoadedScript* aScript, + const nsAString& aSpecifier, + uint32_t aLineNumber, + uint32_t aColumnNumber, + JS::MutableHandle<JS::Value> errorOut) { + JS::Rooted<JSString*> filename(aCx); + if (aScript) { + nsAutoCString url; + aScript->BaseURL()->GetAsciiSpec(url); + filename = JS_NewStringCopyZ(aCx, url.get()); + } else { + filename = JS_NewStringCopyZ(aCx, "(unknown)"); + } + + if (!filename) { + return NS_ERROR_OUT_OF_MEMORY; + } + + AutoTArray<nsString, 1> errorParams; + errorParams.AppendElement(aSpecifier); + + nsAutoString errorText; + nsresult rv = nsContentUtils::FormatLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "ModuleResolveFailure", errorParams, + errorText); + NS_ENSURE_SUCCESS(rv, rv); + + JS::Rooted<JSString*> string(aCx, JS_NewUCStringCopyZ(aCx, errorText.get())); + if (!string) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (!JS::CreateError(aCx, JSEXN_TYPEERR, nullptr, filename, aLineNumber, + aColumnNumber, nullptr, string, errorOut)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +static already_AddRefed<nsIURI> ResolveModuleSpecifier( + ScriptLoader* aLoader, LoadedScript* aScript, const nsAString& aSpecifier) { + // The following module specifiers are allowed by the spec: + // - a valid absolute URL + // - a valid relative URL that starts with "/", "./" or "../" + // + // Bareword module specifiers are currently disallowed as these may be given + // special meanings in the future. + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aSpecifier); + if (NS_SUCCEEDED(rv)) { + return uri.forget(); + } + + if (rv != NS_ERROR_MALFORMED_URI) { + return nullptr; + } + + if (!StringBeginsWith(aSpecifier, u"/"_ns) && + !StringBeginsWith(aSpecifier, u"./"_ns) && + !StringBeginsWith(aSpecifier, u"../"_ns)) { + return nullptr; + } + + // Get the document's base URL if we don't have a referencing script here. + nsCOMPtr<nsIURI> baseURL; + if (aScript) { + baseURL = aScript->BaseURL(); + } else { + baseURL = aLoader->GetDocument()->GetDocBaseURI(); + } + + rv = NS_NewURI(getter_AddRefs(uri), aSpecifier, nullptr, baseURL); + if (NS_SUCCEEDED(rv)) { + return uri.forget(); + } + + return nullptr; +} + +static nsresult ResolveRequestedModules(ModuleLoadRequest* aRequest, + nsCOMArray<nsIURI>* aUrlsOut) { + ModuleScript* ms = aRequest->mModuleScript; + + AutoJSAPI jsapi; + if (!jsapi.Init(ms->ModuleRecord())) { + return NS_ERROR_FAILURE; + } + + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> moduleRecord(cx, ms->ModuleRecord()); + JS::Rooted<JSObject*> requestedModules(cx); + requestedModules = JS::GetRequestedModules(cx, moduleRecord); + MOZ_ASSERT(requestedModules); + + uint32_t length; + if (!JS::GetArrayLength(cx, requestedModules, &length)) { + return NS_ERROR_FAILURE; + } + + JS::Rooted<JS::Value> element(cx); + for (uint32_t i = 0; i < length; i++) { + if (!JS_GetElement(cx, requestedModules, i, &element)) { + return NS_ERROR_FAILURE; + } + + JS::Rooted<JSString*> str(cx, JS::GetRequestedModuleSpecifier(cx, element)); + MOZ_ASSERT(str); + + nsAutoJSString specifier; + if (!specifier.init(cx, str)) { + return NS_ERROR_FAILURE; + } + + // Let url be the result of resolving a module specifier given module script + // and requested. + nsCOMPtr<nsIURI> uri = + ResolveModuleSpecifier(aRequest->mLoader, ms, specifier); + if (!uri) { + uint32_t lineNumber = 0; + uint32_t columnNumber = 0; + JS::GetRequestedModuleSourcePos(cx, element, &lineNumber, &columnNumber); + + JS::Rooted<JS::Value> error(cx); + nsresult rv = HandleResolveFailure(cx, ms, specifier, lineNumber, + columnNumber, &error); + NS_ENSURE_SUCCESS(rv, rv); + + ms->SetParseError(error); + return NS_ERROR_FAILURE; + } + + if (aUrlsOut) { + aUrlsOut->AppendElement(uri.forget()); + } + } + + return NS_OK; +} + +void ScriptLoader::StartFetchingModuleDependencies( + ModuleLoadRequest* aRequest) { + LOG(("ScriptLoadRequest (%p): Start fetching module dependencies", aRequest)); + + MOZ_ASSERT(aRequest->mModuleScript); + MOZ_ASSERT(!aRequest->mModuleScript->HasParseError()); + MOZ_ASSERT(!aRequest->IsReadyToRun()); + + auto visitedSet = aRequest->mVisitedSet; + MOZ_ASSERT(visitedSet->Contains(aRequest->mURI)); + + aRequest->mProgress = ModuleLoadRequest::Progress::eFetchingImports; + + nsCOMArray<nsIURI> urls; + nsresult rv = ResolveRequestedModules(aRequest, &urls); + if (NS_FAILED(rv)) { + aRequest->ModuleErrored(); + return; + } + + // Remove already visited URLs from the list. Put unvisited URLs into the + // visited set. + int32_t i = 0; + while (i < urls.Count()) { + nsIURI* url = urls[i]; + if (visitedSet->Contains(url)) { + urls.RemoveObjectAt(i); + } else { + visitedSet->PutEntry(url); + i++; + } + } + + if (urls.Count() == 0) { + // There are no descendants to load so this request is ready. + aRequest->DependenciesLoaded(); + return; + } + + // For each url in urls, fetch a module script tree given url, module script's + // CORS setting, and module script's settings object. + nsTArray<RefPtr<GenericPromise>> importsReady; + for (auto url : urls) { + RefPtr<GenericPromise> childReady = + StartFetchingModuleAndDependencies(aRequest, url); + importsReady.AppendElement(childReady); + } + + // Wait for all imports to become ready. + RefPtr<GenericPromise::AllPromiseType> allReady = + GenericPromise::All(GetMainThreadSerialEventTarget(), importsReady); + allReady->Then(GetMainThreadSerialEventTarget(), __func__, aRequest, + &ModuleLoadRequest::DependenciesLoaded, + &ModuleLoadRequest::ModuleErrored); +} + +RefPtr<GenericPromise> ScriptLoader::StartFetchingModuleAndDependencies( + ModuleLoadRequest* aParent, nsIURI* aURI) { + MOZ_ASSERT(aURI); + + RefPtr<ModuleLoadRequest> childRequest = + ModuleLoadRequest::CreateStaticImport(aURI, aParent); + + aParent->mImports.AppendElement(childRequest); + + if (LOG_ENABLED()) { + nsAutoCString url1; + aParent->mURI->GetAsciiSpec(url1); + + nsAutoCString url2; + aURI->GetAsciiSpec(url2); + + LOG(("ScriptLoadRequest (%p): Start fetching dependency %p", aParent, + childRequest.get())); + LOG(("StartFetchingModuleAndDependencies \"%s\" -> \"%s\"", url1.get(), + url2.get())); + } + + RefPtr<GenericPromise> ready = childRequest->mReady.Ensure(__func__); + + nsresult rv = StartLoad(childRequest); + if (NS_FAILED(rv)) { + MOZ_ASSERT(!childRequest->mModuleScript); + LOG(("ScriptLoadRequest (%p): rejecting %p", aParent, + &childRequest->mReady)); + childRequest->mReady.Reject(rv, __func__); + return ready; + } + + return ready; +} + +static ScriptLoader* GetCurrentScriptLoader(JSContext* aCx) { + auto reportError = mozilla::MakeScopeExit([aCx]() { + JS_ReportErrorASCII(aCx, "No ScriptLoader found for the current context"); + }); + + JSObject* object = JS::CurrentGlobalOrNull(aCx); + if (!object) { + return nullptr; + } + + nsIGlobalObject* global = xpc::NativeGlobal(object); + if (!global) { + return nullptr; + } + + nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(global); + nsGlobalWindowInner* innerWindow = nsGlobalWindowInner::Cast(win); + if (!innerWindow) { + return nullptr; + } + + Document* document = innerWindow->GetDocument(); + if (!document) { + return nullptr; + } + + ScriptLoader* loader = document->ScriptLoader(); + if (!loader) { + return nullptr; + } + + reportError.release(); + return loader; +} + +static LoadedScript* GetLoadedScriptOrNull( + JSContext* aCx, JS::Handle<JS::Value> aReferencingPrivate) { + if (aReferencingPrivate.isUndefined()) { + return nullptr; + } + + auto script = static_cast<LoadedScript*>(aReferencingPrivate.toPrivate()); + if (script->IsEventScript()) { + return nullptr; + } + + MOZ_ASSERT_IF( + script->IsModuleScript(), + JS::GetModulePrivate(script->AsModuleScript()->ModuleRecord()) == + aReferencingPrivate); + + return script; +} + +// 8.1.3.8.1 HostResolveImportedModule(referencingModule, specifier) +JSObject* HostResolveImportedModule(JSContext* aCx, + JS::Handle<JS::Value> aReferencingPrivate, + JS::Handle<JSString*> aSpecifier) { + JS::Rooted<JSObject*> module(aCx); + ScriptLoader::ResolveImportedModule(aCx, aReferencingPrivate, aSpecifier, + &module); + return module; +} + +/* static */ +void ScriptLoader::ResolveImportedModule( + JSContext* aCx, JS::Handle<JS::Value> aReferencingPrivate, + JS::Handle<JSString*> aSpecifier, JS::MutableHandle<JSObject*> aModuleOut) { + MOZ_ASSERT(!aModuleOut); + + RefPtr<LoadedScript> script(GetLoadedScriptOrNull(aCx, aReferencingPrivate)); + + // Let url be the result of resolving a module specifier given referencing + // module script and specifier. + nsAutoJSString string; + if (!string.init(aCx, aSpecifier)) { + return; + } + + RefPtr<ScriptLoader> loader = GetCurrentScriptLoader(aCx); + if (!loader) { + return; + } + + nsCOMPtr<nsIURI> uri = ResolveModuleSpecifier(loader, script, string); + + // This cannot fail because resolving a module specifier must have been + // previously successful with these same two arguments. + MOZ_ASSERT(uri, "Failed to resolve previously-resolved module specifier"); + + // Let resolved module script be moduleMap[url]. (This entry must exist for us + // to have gotten to this point.) + ModuleScript* ms = loader->GetFetchedModule(uri); + MOZ_ASSERT(ms, "Resolved module not found in module map"); + MOZ_ASSERT(!ms->HasParseError()); + MOZ_ASSERT(ms->ModuleRecord()); + + aModuleOut.set(ms->ModuleRecord()); +} + +bool HostPopulateImportMeta(JSContext* aCx, + JS::Handle<JS::Value> aReferencingPrivate, + JS::Handle<JSObject*> aMetaObject) { + RefPtr<ModuleScript> script = + static_cast<ModuleScript*>(aReferencingPrivate.toPrivate()); + MOZ_ASSERT(script->IsModuleScript()); + MOZ_ASSERT(JS::GetModulePrivate(script->ModuleRecord()) == + aReferencingPrivate); + + nsAutoCString url; + MOZ_DIAGNOSTIC_ASSERT(script->BaseURL()); + MOZ_ALWAYS_SUCCEEDS(script->BaseURL()->GetAsciiSpec(url)); + + JS::Rooted<JSString*> urlString(aCx, JS_NewStringCopyZ(aCx, url.get())); + if (!urlString) { + JS_ReportOutOfMemory(aCx); + return false; + } + + return JS_DefineProperty(aCx, aMetaObject, "url", urlString, + JSPROP_ENUMERATE); +} + +bool HostImportModuleDynamically(JSContext* aCx, + JS::Handle<JS::Value> aReferencingPrivate, + JS::Handle<JSString*> aSpecifier, + JS::Handle<JSObject*> aPromise) { + RefPtr<LoadedScript> script(GetLoadedScriptOrNull(aCx, aReferencingPrivate)); + + // Attempt to resolve the module specifier. + nsAutoJSString specifier; + if (!specifier.init(aCx, aSpecifier)) { + return false; + } + + RefPtr<ScriptLoader> loader = GetCurrentScriptLoader(aCx); + if (!loader) { + return false; + } + + nsCOMPtr<nsIURI> uri = ResolveModuleSpecifier(loader, script, specifier); + if (!uri) { + JS::Rooted<JS::Value> error(aCx); + nsresult rv = HandleResolveFailure(aCx, script, specifier, 0, 0, &error); + if (NS_FAILED(rv)) { + JS_ReportOutOfMemory(aCx); + return false; + } + + JS_SetPendingException(aCx, error); + return false; + } + + // Create a new top-level load request. + ScriptFetchOptions* options; + nsIURI* baseURL; + if (script) { + options = script->GetFetchOptions(); + baseURL = script->BaseURL(); + } else { + // We don't have a referencing script so fall back on using + // options from the document. This can happen when the user + // triggers an inline event handler, as there is no active script + // there. + Document* document = loader->GetDocument(); + options = new ScriptFetchOptions(mozilla::CORS_NONE, + document->GetReferrerPolicy(), nullptr, + document->NodePrincipal()); + baseURL = document->GetDocBaseURI(); + } + + RefPtr<ModuleLoadRequest> request = ModuleLoadRequest::CreateDynamicImport( + uri, options, baseURL, loader, aReferencingPrivate, aSpecifier, aPromise); + + loader->StartDynamicImport(request); + return true; +} + +void ScriptLoader::StartDynamicImport(ModuleLoadRequest* aRequest) { + LOG(("ScriptLoadRequest (%p): Start dynamic import", aRequest)); + + mDynamicImportRequests.AppendElement(aRequest); + + nsresult rv = StartLoad(aRequest); + if (NS_FAILED(rv)) { + FinishDynamicImportAndReject(aRequest, rv); + } +} + +void ScriptLoader::FinishDynamicImportAndReject(ModuleLoadRequest* aRequest, + nsresult aResult) { + AutoJSAPI jsapi; + MOZ_ASSERT(NS_FAILED(aResult)); + MOZ_ALWAYS_TRUE(jsapi.Init(aRequest->mDynamicPromise)); + if (!JS::ContextOptionsRef(jsapi.cx()).topLevelAwait()) { + // This is used so that Top Level Await functionality can be turned off + // entirely. It will be removed in bug#1676612. + FinishDynamicImport_NoTLA(jsapi.cx(), aRequest, aResult); + } else { + // Path for when Top Level Await is enabled. + FinishDynamicImport(jsapi.cx(), aRequest, aResult, nullptr); + } +} + +// This is used so that Top Level Await functionality can be turned off +// entirely. It will be removed in bug#1676612. +void ScriptLoader::FinishDynamicImport_NoTLA(JSContext* aCx, + ModuleLoadRequest* aRequest, + nsresult aResult) { + LOG(("ScriptLoadRequest (%p): Finish dynamic import %x %d", aRequest, + unsigned(aResult), JS_IsExceptionPending(aCx))); + + // Complete the dynamic import, report failures indicated by aResult or as a + // pending exception on the context. + + JS::DynamicImportStatus status = + (NS_FAILED(aResult) || JS_IsExceptionPending(aCx)) + ? JS::DynamicImportStatus::Failed + : JS::DynamicImportStatus::Ok; + + if (NS_FAILED(aResult) && + aResult != NS_SUCCESS_DOM_SCRIPT_EVALUATION_THREW_UNCATCHABLE) { + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + JS_ReportErrorNumberUC(aCx, js::GetErrorMessage, nullptr, + JSMSG_DYNAMIC_IMPORT_FAILED); + } + + JS::Rooted<JS::Value> referencingScript(aCx, + aRequest->mDynamicReferencingPrivate); + JS::Rooted<JSString*> specifier(aCx, aRequest->mDynamicSpecifier); + JS::Rooted<JSObject*> promise(aCx, aRequest->mDynamicPromise); + + JS::FinishDynamicModuleImport_NoTLA(aCx, status, referencingScript, specifier, + promise); + + // FinishDynamicModuleImport clears any pending exception. + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + + aRequest->ClearDynamicImport(); +} + +void ScriptLoader::FinishDynamicImport( + JSContext* aCx, ModuleLoadRequest* aRequest, nsresult aResult, + JS::Handle<JSObject*> aEvaluationPromise) { + // If aResult is a failed result, we don't have an EvaluationPromise. If it + // succeeded, evaluationPromise may still be null, but in this case it will + // be handled by rejecting the dynamic module import promise in the JSAPI. + MOZ_ASSERT_IF(NS_FAILED(aResult), !aEvaluationPromise); + LOG(("ScriptLoadRequest (%p): Finish dynamic import %x %d", aRequest, + unsigned(aResult), JS_IsExceptionPending(aCx))); + + // Complete the dynamic import, report failures indicated by aResult or as a + // pending exception on the context. + + if (NS_FAILED(aResult) && + aResult != NS_SUCCESS_DOM_SCRIPT_EVALUATION_THREW_UNCATCHABLE) { + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + JS_ReportErrorNumberUC(aCx, js::GetErrorMessage, nullptr, + JSMSG_DYNAMIC_IMPORT_FAILED); + } + + JS::Rooted<JS::Value> referencingScript(aCx, + aRequest->mDynamicReferencingPrivate); + JS::Rooted<JSString*> specifier(aCx, aRequest->mDynamicSpecifier); + JS::Rooted<JSObject*> promise(aCx, aRequest->mDynamicPromise); + + JS::FinishDynamicModuleImport(aCx, aEvaluationPromise, referencingScript, + specifier, promise); + + // FinishDynamicModuleImport clears any pending exception. + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + + aRequest->ClearDynamicImport(); +} + +static void DynamicImportPrefChangedCallback(const char* aPrefName, + void* aClosure) { + bool enabled = Preferences::GetBool(aPrefName); + JS::ModuleDynamicImportHook hook = + enabled ? HostImportModuleDynamically : nullptr; + + AutoJSAPI jsapi; + jsapi.Init(); + JSRuntime* rt = JS_GetRuntime(jsapi.cx()); + JS::SetModuleDynamicImportHook(rt, hook); +} + +void ScriptLoader::EnsureModuleHooksInitialized() { + AutoJSAPI jsapi; + jsapi.Init(); + JSRuntime* rt = JS_GetRuntime(jsapi.cx()); + if (JS::GetModuleResolveHook(rt)) { + return; + } + + JS::SetModuleResolveHook(rt, HostResolveImportedModule); + JS::SetModuleMetadataHook(rt, HostPopulateImportMeta); + JS::SetScriptPrivateReferenceHooks(rt, HostAddRefTopLevelScript, + HostReleaseTopLevelScript); + + Preferences::RegisterCallbackAndCall(DynamicImportPrefChangedCallback, + "javascript.options.dynamicImport", + (void*)nullptr); +} + +void ScriptLoader::CheckModuleDependenciesLoaded(ModuleLoadRequest* aRequest) { + LOG(("ScriptLoadRequest (%p): Check dependencies loaded", aRequest)); + + RefPtr<ModuleScript> moduleScript = aRequest->mModuleScript; + if (!moduleScript || moduleScript->HasParseError()) { + return; + } + + for (auto childRequest : aRequest->mImports) { + ModuleScript* childScript = childRequest->mModuleScript; + if (!childScript) { + aRequest->mModuleScript = nullptr; + LOG(("ScriptLoadRequest (%p): %p failed (load error)", aRequest, + childRequest.get())); + return; + } + } + + LOG(("ScriptLoadRequest (%p): all ok", aRequest)); +} + +class ScriptRequestProcessor : public Runnable { + private: + RefPtr<ScriptLoader> mLoader; + RefPtr<ScriptLoadRequest> mRequest; + + public: + ScriptRequestProcessor(ScriptLoader* aLoader, ScriptLoadRequest* aRequest) + : Runnable("dom::ScriptRequestProcessor"), + mLoader(aLoader), + mRequest(aRequest) {} + NS_IMETHOD Run() override { + if (mRequest->IsModuleRequest() && + mRequest->AsModuleRequest()->IsDynamicImport()) { + mLoader->ProcessDynamicImport(mRequest->AsModuleRequest()); + return NS_OK; + } + + return mLoader->ProcessRequest(mRequest); + } +}; + +void ScriptLoader::RunScriptWhenSafe(ScriptLoadRequest* aRequest) { + auto runnable = new ScriptRequestProcessor(this, aRequest); + nsContentUtils::AddScriptRunner(runnable); +} + +void ScriptLoader::ProcessLoadedModuleTree(ModuleLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->IsReadyToRun()); + + if (aRequest->IsTopLevel()) { + if (aRequest->IsDynamicImport()) { + MOZ_ASSERT(aRequest->isInList()); + RefPtr<ScriptLoadRequest> req = mDynamicImportRequests.Steal(aRequest); + RunScriptWhenSafe(req); + } else if (aRequest->mIsInline && + aRequest->GetParserCreated() == NOT_FROM_PARSER) { + MOZ_ASSERT(!aRequest->isInList()); + RunScriptWhenSafe(aRequest); + } else { + MaybeMoveToLoadedList(aRequest); + ProcessPendingRequests(); + } + } + + aRequest->MaybeUnblockOnload(); +} + +JS::Value ScriptLoader::FindFirstParseError(ModuleLoadRequest* aRequest) { + MOZ_ASSERT(aRequest); + + ModuleScript* moduleScript = aRequest->mModuleScript; + MOZ_ASSERT(moduleScript); + + if (moduleScript->HasParseError()) { + return moduleScript->ParseError(); + } + + for (ModuleLoadRequest* childRequest : aRequest->mImports) { + JS::Value error = FindFirstParseError(childRequest); + if (!error.isUndefined()) { + return error; + } + } + + return JS::UndefinedValue(); +} + +bool ScriptLoader::InstantiateModuleTree(ModuleLoadRequest* aRequest) { + // Instantiate a top-level module and record any error. + + MOZ_ASSERT(aRequest); + MOZ_ASSERT(aRequest->IsTopLevel()); + + LOG(("ScriptLoadRequest (%p): Instantiate module tree", aRequest)); + + ModuleScript* moduleScript = aRequest->mModuleScript; + MOZ_ASSERT(moduleScript); + + JS::Value parseError = FindFirstParseError(aRequest); + if (!parseError.isUndefined()) { + moduleScript->SetErrorToRethrow(parseError); + LOG(("ScriptLoadRequest (%p): found parse error", aRequest)); + return true; + } + + MOZ_ASSERT(moduleScript->ModuleRecord()); + + nsAutoMicroTask mt; + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(moduleScript->ModuleRecord()))) { + return false; + } + + JS::Rooted<JSObject*> module(jsapi.cx(), moduleScript->ModuleRecord()); + bool ok = NS_SUCCEEDED(nsJSUtils::ModuleInstantiate(jsapi.cx(), module)); + + if (!ok) { + LOG(("ScriptLoadRequest (%p): Instantiate failed", aRequest)); + MOZ_ASSERT(jsapi.HasException()); + JS::RootedValue exception(jsapi.cx()); + if (!jsapi.StealException(&exception)) { + return false; + } + MOZ_ASSERT(!exception.isUndefined()); + moduleScript->SetErrorToRethrow(exception); + } + + return true; +} + +nsresult ScriptLoader::InitDebuggerDataForModuleTree( + JSContext* aCx, ModuleLoadRequest* aRequest) { + // JS scripts can be associated with a DOM element for use by the debugger, + // but preloading can cause scripts to be compiled before DOM script element + // nodes have been created. This method ensures that this association takes + // place before the first time a module script is run. + + MOZ_ASSERT(aRequest); + + ModuleScript* moduleScript = aRequest->mModuleScript; + if (moduleScript->DebuggerDataInitialized()) { + return NS_OK; + } + + for (ModuleLoadRequest* childRequest : aRequest->mImports) { + nsresult rv = InitDebuggerDataForModuleTree(aCx, childRequest); + NS_ENSURE_SUCCESS(rv, rv); + } + + JS::Rooted<JSObject*> module(aCx, moduleScript->ModuleRecord()); + MOZ_ASSERT(module); + + // The script is now ready to be exposed to the debugger. + JS::Rooted<JSScript*> script(aCx, JS::GetModuleScript(module)); + JS::ExposeScriptToDebugger(aCx, script); + + moduleScript->SetDebuggerDataInitialized(); + return NS_OK; +} + +nsresult ScriptLoader::RestartLoad(ScriptLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->IsBytecode()); + aRequest->mScriptBytecode.clearAndFree(); + TRACE_FOR_TEST(aRequest->GetScriptElement(), "scriptloader_fallback"); + + // Notify preload restart so that we can register this preload request again. + aRequest->NotifyRestart(mDocument); + + // Start a new channel from which we explicitly request to stream the source + // instead of the bytecode. + aRequest->mProgress = ScriptLoadRequest::Progress::eLoading_Source; + nsresult rv = StartLoad(aRequest); + if (NS_FAILED(rv)) { + return rv; + } + + // Close the current channel and this ScriptLoadHandler as we created a new + // one for the same request. + return NS_BINDING_RETARGETED; +} + +nsresult ScriptLoader::StartLoad(ScriptLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->IsLoading()); + NS_ENSURE_TRUE(mDocument, NS_ERROR_NULL_POINTER); + aRequest->SetUnknownDataType(); + + // If this document is sandboxed without 'allow-scripts', abort. + if (mDocument->HasScriptsBlockedBySandbox()) { + return NS_OK; + } + + if (LOG_ENABLED()) { + nsAutoCString url; + aRequest->mURI->GetAsciiSpec(url); + LOG(("ScriptLoadRequest (%p): Start Load (url = %s)", aRequest, url.get())); + } + + if (aRequest->IsModuleRequest()) { + // Check whether the module has been fetched or is currently being fetched, + // and if so wait for it rather than starting a new fetch. + ModuleLoadRequest* request = aRequest->AsModuleRequest(); + if (ModuleMapContainsURL(request->mURI)) { + LOG(("ScriptLoadRequest (%p): Waiting for module fetch", aRequest)); + WaitForModuleFetch(request->mURI) + ->Then(GetMainThreadSerialEventTarget(), __func__, request, + &ModuleLoadRequest::ModuleLoaded, + &ModuleLoadRequest::LoadFailed); + return NS_OK; + } + } + + nsContentPolicyType contentPolicyType = + ScriptLoadRequestToContentPolicyType(aRequest); + nsCOMPtr<nsINode> context; + if (aRequest->GetScriptElement()) { + context = do_QueryInterface(aRequest->GetScriptElement()); + } else { + context = mDocument; + } + + nsCOMPtr<nsILoadGroup> loadGroup = mDocument->GetDocumentLoadGroup(); + nsCOMPtr<nsPIDOMWindowOuter> window = mDocument->GetWindow(); + NS_ENSURE_TRUE(window, NS_ERROR_NULL_POINTER); + nsIDocShell* docshell = window->GetDocShell(); + nsCOMPtr<nsIInterfaceRequestor> prompter(do_QueryInterface(docshell)); + + nsSecurityFlags securityFlags; + if (aRequest->IsModuleRequest()) { + // According to the spec, module scripts have different behaviour to classic + // scripts and always use CORS. Only exception: Non linkable about: pages + // which load local module scripts. + if (IsAboutPageLoadingChromeURI(aRequest, mDocument)) { + securityFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL; + } else { + securityFlags = nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT; + if (aRequest->CORSMode() == CORS_NONE || + aRequest->CORSMode() == CORS_ANONYMOUS) { + securityFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN; + } else { + MOZ_ASSERT(aRequest->CORSMode() == CORS_USE_CREDENTIALS); + securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; + } + } + } else { + securityFlags = + aRequest->CORSMode() == CORS_NONE + ? nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL + : nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT; + if (aRequest->CORSMode() == CORS_ANONYMOUS) { + securityFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN; + } else if (aRequest->CORSMode() == CORS_USE_CREDENTIALS) { + securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; + } + } + securityFlags |= nsILoadInfo::SEC_ALLOW_CHROME; + + nsCOMPtr<nsIChannel> channel; + nsresult rv = NS_NewChannelWithTriggeringPrincipal( + getter_AddRefs(channel), aRequest->mURI, context, + aRequest->TriggeringPrincipal(), securityFlags, contentPolicyType, + nullptr, // aPerformanceStorage + loadGroup, prompter); + + NS_ENSURE_SUCCESS(rv, rv); + + // snapshot the nonce at load start time for performing CSP checks + if (contentPolicyType == nsIContentPolicy::TYPE_INTERNAL_SCRIPT || + contentPolicyType == nsIContentPolicy::TYPE_INTERNAL_MODULE) { + if (context) { + nsString* cspNonce = + static_cast<nsString*>(context->GetProperty(nsGkAtoms::nonce)); + if (cspNonce) { + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + loadInfo->SetCspNonce(*cspNonce); + } + } + } + + nsCOMPtr<nsIScriptGlobalObject> globalObject = GetScriptGlobalObject(); + if (!globalObject) { + return NS_ERROR_FAILURE; + } + + // To avoid decoding issues, the build-id is part of the JSBytecodeMimeType + // constant. + aRequest->mCacheInfo = nullptr; + nsCOMPtr<nsICacheInfoChannel> cic(do_QueryInterface(channel)); + if (cic && StaticPrefs::dom_script_loader_bytecode_cache_enabled() && + // Globals with instrumentation have modified script bytecode and can't + // use cached bytecode. + !js::GlobalHasInstrumentation(globalObject->GetGlobalJSObject()) && + // Bug 1436400: no bytecode cache support for modules yet. + !aRequest->IsModuleRequest()) { + if (!aRequest->IsLoadingSource()) { + // Inform the HTTP cache that we prefer to have information coming from + // the bytecode cache instead of the sources, if such entry is already + // registered. + LOG(("ScriptLoadRequest (%p): Maybe request bytecode", aRequest)); + cic->PreferAlternativeDataType(nsContentUtils::JSBytecodeMimeType(), + ""_ns, true); + } else { + // If we are explicitly loading from the sources, such as after a + // restarted request, we might still want to save the bytecode after. + // + // The following tell the cache to look for an alternative data type which + // does not exist, such that we can later save the bytecode with a + // different alternative data type. + LOG(("ScriptLoadRequest (%p): Request saving bytecode later", aRequest)); + cic->PreferAlternativeDataType(kNullMimeType, ""_ns, true); + } + } + + LOG(("ScriptLoadRequest (%p): mode=%u tracking=%d", aRequest, + unsigned(aRequest->mScriptMode), aRequest->IsTracking())); + + if (aRequest->IsLinkPreloadScript()) { + // This is <link rel="preload" as="script"> initiated speculative load, + // put it to the group that is not blocked by leaders and doesn't block + // follower at the same time. Giving it a much higher priority will make + // this request be processed ahead of other Unblocked requests, but with + // the same weight as Leaders. This will make us behave similar way for + // both http2 and http1. + ScriptLoadRequest::PrioritizeAsPreload(channel); + ScriptLoadRequest::AddLoadBackgroundFlag(channel); + } else if (nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(channel)) { + if (aRequest->mScriptFromHead && aRequest->IsBlockingScript()) { + // synchronous head scripts block loading of most other non js/css + // content such as images, Leader implicitely disallows tailing + cos->AddClassFlags(nsIClassOfService::Leader); + } else if (aRequest->IsDeferredScript() && + !StaticPrefs::network_http_tailing_enabled()) { + // Bug 1395525 and the !StaticPrefs::network_http_tailing_enabled() bit: + // We want to make sure that turing tailing off by the pref makes the + // browser behave exactly the same way as before landing the tailing + // patch. + + // head/body deferred scripts are blocked by leaders but are not + // allowed tailing because they block DOMContentLoaded + cos->AddClassFlags(nsIClassOfService::TailForbidden); + } else { + // other scripts (=body sync or head/body async) are neither blocked + // nor prioritized + cos->AddClassFlags(nsIClassOfService::Unblocked); + + if (aRequest->IsAsyncScript()) { + // async scripts are allowed tailing, since those and only those + // don't block DOMContentLoaded; this flag doesn't enforce tailing, + // just overweights the Unblocked flag when the channel is found + // to be a thrird-party tracker and thus set the Tail flag to engage + // tailing. + cos->AddClassFlags(nsIClassOfService::TailAllowed); + } + } + } + + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel)); + if (httpChannel) { + // HTTP content negotation has little value in this context. + nsAutoCString acceptTypes("*/*"); + if (nsJSUtils::BinASTEncodingEnabled() && + aRequest->ShouldAcceptBinASTEncoding()) { + acceptTypes = APPLICATION_JAVASCRIPT_BINAST ", */*"; + } + rv = httpChannel->SetRequestHeader("Accept"_ns, acceptTypes, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsCOMPtr<nsIReferrerInfo> referrerInfo = + new ReferrerInfo(aRequest->mReferrer, aRequest->ReferrerPolicy()); + rv = httpChannel->SetReferrerInfoWithoutClone(referrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + nsCOMPtr<nsIHttpChannelInternal> internalChannel( + do_QueryInterface(httpChannel)); + if (internalChannel) { + rv = internalChannel->SetIntegrityMetadata( + aRequest->mIntegrity.GetIntegrityString()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + + mozilla::net::PredictorLearn( + aRequest->mURI, mDocument->GetDocumentURI(), + nsINetworkPredictor::LEARN_LOAD_SUBRESOURCE, + mDocument->NodePrincipal()->OriginAttributesRef()); + + // Set the initiator type + nsCOMPtr<nsITimedChannel> timedChannel(do_QueryInterface(httpChannel)); + if (timedChannel) { + if (aRequest->IsLinkPreloadScript()) { + timedChannel->SetInitiatorType(u"link"_ns); + } else { + timedChannel->SetInitiatorType(u"script"_ns); + } + } + + UniquePtr<mozilla::dom::SRICheckDataVerifier> sriDataVerifier; + if (!aRequest->mIntegrity.IsEmpty()) { + nsAutoCString sourceUri; + if (mDocument->GetDocumentURI()) { + mDocument->GetDocumentURI()->GetAsciiSpec(sourceUri); + } + sriDataVerifier = MakeUnique<SRICheckDataVerifier>(aRequest->mIntegrity, + sourceUri, mReporter); + } + + RefPtr<ScriptLoadHandler> handler = + new ScriptLoadHandler(this, aRequest, std::move(sriDataVerifier)); + + nsCOMPtr<nsIIncrementalStreamLoader> loader; + rv = NS_NewIncrementalStreamLoader(getter_AddRefs(loader), handler); + NS_ENSURE_SUCCESS(rv, rv); + + auto key = PreloadHashKey::CreateAsScript( + aRequest->mURI, aRequest->CORSMode(), aRequest->mKind); + aRequest->NotifyOpen(key, channel, mDocument, + aRequest->IsLinkPreloadScript()); + + rv = channel->AsyncOpen(loader); + + if (NS_FAILED(rv)) { + // Make sure to inform any <link preload> tags about failure to load the + // resource. + aRequest->NotifyStart(channel); + aRequest->NotifyStop(rv); + } + + NS_ENSURE_SUCCESS(rv, rv); + + if (aRequest->IsModuleRequest()) { + // We successfully started fetching a module so put its URL in the module + // map and mark it as fetching. + SetModuleFetchStarted(aRequest->AsModuleRequest()); + LOG(("ScriptLoadRequest (%p): Start fetching module", aRequest)); + } + + return NS_OK; +} + +bool ScriptLoader::PreloadURIComparator::Equals(const PreloadInfo& aPi, + nsIURI* const& aURI) const { + bool same; + return NS_SUCCEEDED(aPi.mRequest->mURI->Equals(aURI, &same)) && same; +} + +static bool CSPAllowsInlineScript(nsIScriptElement* aElement, + Document* aDocument) { + nsCOMPtr<nsIContentSecurityPolicy> csp = aDocument->GetCsp(); + nsresult rv = NS_OK; + + if (!csp) { + // no CSP --> allow + return true; + } + + // query the nonce + nsCOMPtr<Element> scriptContent = do_QueryInterface(aElement); + nsAutoString nonce; + if (scriptContent) { + nsString* cspNonce = + static_cast<nsString*>(scriptContent->GetProperty(nsGkAtoms::nonce)); + if (cspNonce) { + nonce = *cspNonce; + } + } + + bool parserCreated = + aElement->GetParserCreated() != mozilla::dom::NOT_FROM_PARSER; + + bool allowInlineScript = false; + rv = csp->GetAllowsInline( + nsIContentSecurityPolicy::SCRIPT_SRC_DIRECTIVE, nonce, parserCreated, + scriptContent, nullptr /* nsICSPEventListener */, u""_ns, + aElement->GetScriptLineNumber(), aElement->GetScriptColumnNumber(), + &allowInlineScript); + return NS_SUCCEEDED(rv) && allowInlineScript; +} + +ScriptLoadRequest* ScriptLoader::CreateLoadRequest( + ScriptKind aKind, nsIURI* aURI, nsIScriptElement* aElement, + nsIPrincipal* aTriggeringPrincipal, CORSMode aCORSMode, + const SRIMetadata& aIntegrity, ReferrerPolicy aReferrerPolicy) { + nsIURI* referrer = mDocument->GetDocumentURIAsReferrer(); + nsCOMPtr<Element> domElement = do_QueryInterface(aElement); + ScriptFetchOptions* fetchOptions = new ScriptFetchOptions( + aCORSMode, aReferrerPolicy, domElement, aTriggeringPrincipal); + + if (aKind == ScriptKind::eClassic) { + return new ScriptLoadRequest(aKind, aURI, fetchOptions, aIntegrity, + referrer); + } + + MOZ_ASSERT(aKind == ScriptKind::eModule); + return ModuleLoadRequest::CreateTopLevel(aURI, fetchOptions, aIntegrity, + referrer, this); +} + +bool ScriptLoader::ProcessScriptElement(nsIScriptElement* aElement) { + // We need a document to evaluate scripts. + NS_ENSURE_TRUE(mDocument, false); + + // Check to see if scripts has been turned off. + if (!mEnabled || !mDocument->IsScriptEnabled()) { + return false; + } + + NS_ASSERTION(!aElement->IsMalformed(), "Executing malformed script"); + + nsCOMPtr<nsIContent> scriptContent = do_QueryInterface(aElement); + + nsAutoString type; + bool hasType = aElement->GetScriptType(type); + + ScriptKind scriptKind = aElement->GetScriptIsModule() ? ScriptKind::eModule + : ScriptKind::eClassic; + + // Step 13. Check that the script is not an eventhandler + if (IsScriptEventHandler(scriptKind, scriptContent)) { + return false; + } + + // For classic scripts, check the type attribute to determine language and + // version. If type exists, it trumps the deprecated 'language=' + if (scriptKind == ScriptKind::eClassic) { + if (!type.IsEmpty()) { + NS_ENSURE_TRUE(nsContentUtils::IsJavascriptMIMEType(type), false); + } else if (!hasType) { + // no 'type=' element + // "language" is a deprecated attribute of HTML, so we check it only for + // HTML script elements. + if (scriptContent->IsHTMLElement()) { + nsAutoString language; + scriptContent->AsElement()->GetAttr(kNameSpaceID_None, + nsGkAtoms::language, language); + if (!language.IsEmpty()) { + if (!nsContentUtils::IsJavaScriptLanguage(language)) { + return false; + } + } + } + } + } + + // "In modern user agents that support module scripts, the script element with + // the nomodule attribute will be ignored". + // "The nomodule attribute must not be specified on module scripts (and will + // be ignored if it is)." + if (mDocument->ModuleScriptsEnabled() && scriptKind == ScriptKind::eClassic && + scriptContent->IsHTMLElement() && + scriptContent->AsElement()->HasAttr(kNameSpaceID_None, + nsGkAtoms::nomodule)) { + return false; + } + + // Step 15. and later in the HTML5 spec + if (aElement->GetScriptExternal()) { + return ProcessExternalScript(aElement, scriptKind, type, scriptContent); + } + + return ProcessInlineScript(aElement, scriptKind); +} + +bool ScriptLoader::ProcessExternalScript(nsIScriptElement* aElement, + ScriptKind aScriptKind, + nsAutoString aTypeAttr, + nsIContent* aScriptContent) { + LOG(("ScriptLoader (%p): Process external script for element %p", this, + aElement)); + + nsCOMPtr<nsIURI> scriptURI = aElement->GetScriptURI(); + if (!scriptURI) { + // Asynchronously report the failure to create a URI object + NS_DispatchToCurrentThread( + NewRunnableMethod("nsIScriptElement::FireErrorEvent", aElement, + &nsIScriptElement::FireErrorEvent)); + return false; + } + + SRIMetadata sriMetadata; + { + nsAutoString integrity; + aScriptContent->AsElement()->GetAttr(kNameSpaceID_None, + nsGkAtoms::integrity, integrity); + GetSRIMetadata(integrity, &sriMetadata); + } + + RefPtr<ScriptLoadRequest> request = + LookupPreloadRequest(aElement, aScriptKind, sriMetadata); + + if (request && + NS_FAILED(CheckContentPolicy(mDocument, aElement, aTypeAttr, request))) { + LOG(("ScriptLoader (%p): content policy check failed for preload", this)); + + // Probably plans have changed; even though the preload was allowed seems + // like the actual load is not; let's cancel the preload request. + request->Cancel(); + AccumulateCategorical(LABELS_DOM_SCRIPT_PRELOAD_RESULT::RejectedByPolicy); + return false; + } + + if (request) { + // Use the preload request. + + LOG(("ScriptLoadRequest (%p): Using preload request", request.get())); + + // It's possible these attributes changed since we started the preload so + // update them here. + request->SetScriptMode(aElement->GetScriptDeferred(), + aElement->GetScriptAsync(), false); + + AccumulateCategorical(LABELS_DOM_SCRIPT_PRELOAD_RESULT::Used); + } else { + // No usable preload found. + + nsCOMPtr<nsIPrincipal> principal = + aElement->GetScriptURITriggeringPrincipal(); + if (!principal) { + principal = aScriptContent->NodePrincipal(); + } + + CORSMode ourCORSMode = aElement->GetCORSMode(); + ReferrerPolicy referrerPolicy = GetReferrerPolicy(aElement); + + request = CreateLoadRequest(aScriptKind, scriptURI, aElement, principal, + ourCORSMode, sriMetadata, referrerPolicy); + request->mIsInline = false; + request->SetScriptMode(aElement->GetScriptDeferred(), + aElement->GetScriptAsync(), false); + // keep request->mScriptFromHead to false so we don't treat non preloaded + // scripts as blockers for full page load. See bug 792438. + + LOG(("ScriptLoadRequest (%p): Created request for external script", + request.get())); + + nsresult rv = StartLoad(request); + if (NS_FAILED(rv)) { + ReportErrorToConsole(request, rv); + + // Asynchronously report the load failure + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod("nsIScriptElement::FireErrorEvent", aElement, + &nsIScriptElement::FireErrorEvent); + if (mDocument) { + mDocument->Dispatch(TaskCategory::Other, runnable.forget()); + } else { + NS_DispatchToCurrentThread(runnable); + } + return false; + } + } + + // We should still be in loading stage of script unless we're loading a + // module or speculatively off-main-thread parsing a script. + NS_ASSERTION(SpeculativeOMTParsingEnabled() || !request->InCompilingStage() || + request->IsModuleRequest(), + "Request should not yet be in compiling stage."); + + if (request->IsAsyncScript()) { + AddAsyncRequest(request); + if (request->IsReadyToRun()) { + // The script is available already. Run it ASAP when the event + // loop gets a chance to spin. + + // KVKV TODO: Instead of processing immediately, try off-thread-parsing + // it and only schedule a pending ProcessRequest if that fails. + ProcessPendingRequestsAsync(); + } + return false; + } + if (!aElement->GetParserCreated()) { + // Violate the HTML5 spec in order to make LABjs and the "order" plug-in + // for RequireJS work with their Gecko-sniffed code path. See + // http://lists.w3.org/Archives/Public/public-html/2010Oct/0088.html + request->mIsNonAsyncScriptInserted = true; + mNonAsyncExternalScriptInsertedRequests.AppendElement(request); + if (request->IsReadyToRun()) { + // The script is available already. Run it ASAP when the event + // loop gets a chance to spin. + ProcessPendingRequestsAsync(); + } + return false; + } + // we now have a parser-inserted request that may or may not be still + // loading + if (request->IsDeferredScript()) { + // We don't want to run this yet. + // If we come here, the script is a parser-created script and it has + // the defer attribute but not the async attribute. Since a + // a parser-inserted script is being run, we came here by the parser + // running the script, which means the parser is still alive and the + // parse is ongoing. + NS_ASSERTION(mDocument->GetCurrentContentSink() || + aElement->GetParserCreated() == FROM_PARSER_XSLT, + "Non-XSLT Defer script on a document without an active " + "parser; bug 592366."); + AddDeferRequest(request); + return false; + } + + if (aElement->GetParserCreated() == FROM_PARSER_XSLT) { + // Need to maintain order for XSLT-inserted scripts + NS_ASSERTION(!mParserBlockingRequest, + "Parser-blocking scripts and XSLT scripts in the same doc!"); + request->mIsXSLT = true; + mXSLTRequests.AppendElement(request); + if (request->IsReadyToRun()) { + // The script is available already. Run it ASAP when the event + // loop gets a chance to spin. + ProcessPendingRequestsAsync(); + } + return true; + } + + if (request->IsReadyToRun() && ReadyToExecuteParserBlockingScripts()) { + // The request has already been loaded and there are no pending style + // sheets. If the script comes from the network stream, cheat for + // performance reasons and avoid a trip through the event loop. + if (aElement->GetParserCreated() == FROM_PARSER_NETWORK) { + return ProcessRequest(request) == NS_ERROR_HTMLPARSER_BLOCK; + } + // Otherwise, we've got a document.written script, make a trip through + // the event loop to hide the preload effects from the scripts on the + // Web page. + NS_ASSERTION(!mParserBlockingRequest, + "There can be only one parser-blocking script at a time"); + NS_ASSERTION(mXSLTRequests.isEmpty(), + "Parser-blocking scripts and XSLT scripts in the same doc!"); + mParserBlockingRequest = request; + ProcessPendingRequestsAsync(); + return true; + } + + // The script hasn't loaded yet or there's a style sheet blocking it. + // The script will be run when it loads or the style sheet loads. + NS_ASSERTION(!mParserBlockingRequest, + "There can be only one parser-blocking script at a time"); + NS_ASSERTION(mXSLTRequests.isEmpty(), + "Parser-blocking scripts and XSLT scripts in the same doc!"); + mParserBlockingRequest = request; + return true; +} + +bool ScriptLoader::ProcessInlineScript(nsIScriptElement* aElement, + ScriptKind aScriptKind) { + // Is this document sandboxed without 'allow-scripts'? + if (mDocument->HasScriptsBlockedBySandbox()) { + return false; + } + + // Does CSP allow this inline script to run? + if (!CSPAllowsInlineScript(aElement, mDocument)) { + return false; + } + + // Inline classic scripts ignore their CORS mode and are always CORS_NONE. + CORSMode corsMode = CORS_NONE; + if (aScriptKind == ScriptKind::eModule) { + corsMode = aElement->GetCORSMode(); + } + + ReferrerPolicy referrerPolicy = GetReferrerPolicy(aElement); + RefPtr<ScriptLoadRequest> request = + CreateLoadRequest(aScriptKind, mDocument->GetDocumentURI(), aElement, + mDocument->NodePrincipal(), corsMode, + SRIMetadata(), // SRI doesn't apply + referrerPolicy); + request->mIsInline = true; + request->mLineNo = aElement->GetScriptLineNumber(); + request->mProgress = ScriptLoadRequest::Progress::eLoading_Source; + request->SetTextSource(); + TRACE_FOR_TEST_BOOL(request->GetScriptElement(), "scriptloader_load_source"); + CollectScriptTelemetry(request); + + // Only the 'async' attribute is heeded on an inline module script and + // inline classic scripts ignore both these attributes. + MOZ_ASSERT(!aElement->GetScriptDeferred()); + MOZ_ASSERT_IF(!request->IsModuleRequest(), !aElement->GetScriptAsync()); + request->SetScriptMode(false, aElement->GetScriptAsync(), false); + + LOG(("ScriptLoadRequest (%p): Created request for inline script", + request.get())); + + request->mBaseURL = mDocument->GetDocBaseURI(); + + if (request->IsModuleRequest()) { + ModuleLoadRequest* modReq = request->AsModuleRequest(); + if (aElement->GetParserCreated() != NOT_FROM_PARSER) { + if (aElement->GetScriptAsync()) { + AddAsyncRequest(modReq); + } else { + AddDeferRequest(modReq); + } + } + + nsresult rv = ProcessFetchedModuleSource(modReq); + if (NS_FAILED(rv)) { + ReportErrorToConsole(modReq, rv); + HandleLoadError(modReq, rv); + } + + return false; + } + request->mProgress = ScriptLoadRequest::Progress::eReady; + if (aElement->GetParserCreated() == FROM_PARSER_XSLT && + (!ReadyToExecuteParserBlockingScripts() || !mXSLTRequests.isEmpty())) { + // Need to maintain order for XSLT-inserted scripts + NS_ASSERTION(!mParserBlockingRequest, + "Parser-blocking scripts and XSLT scripts in the same doc!"); + mXSLTRequests.AppendElement(request); + return true; + } + if (aElement->GetParserCreated() == NOT_FROM_PARSER) { + NS_ASSERTION( + !nsContentUtils::IsSafeToRunScript(), + "A script-inserted script is inserted without an update batch?"); + RunScriptWhenSafe(request); + return false; + } + if (aElement->GetParserCreated() == FROM_PARSER_NETWORK && + !ReadyToExecuteParserBlockingScripts()) { + NS_ASSERTION(!mParserBlockingRequest, + "There can be only one parser-blocking script at a time"); + mParserBlockingRequest = request; + NS_ASSERTION(mXSLTRequests.isEmpty(), + "Parser-blocking scripts and XSLT scripts in the same doc!"); + return true; + } + // We now have a document.written inline script or we have an inline script + // from the network but there is no style sheet that is blocking scripts. + // Don't check for style sheets blocking scripts in the document.write + // case to avoid style sheet network activity affecting when + // document.write returns. It's not really necessary to do this if + // there's no document.write currently on the call stack. However, + // this way matches IE more closely than checking if document.write + // is on the call stack. + NS_ASSERTION(nsContentUtils::IsSafeToRunScript(), + "Not safe to run a parser-inserted script?"); + return ProcessRequest(request) == NS_ERROR_HTMLPARSER_BLOCK; +} + +ScriptLoadRequest* ScriptLoader::LookupPreloadRequest( + nsIScriptElement* aElement, ScriptKind aScriptKind, + const SRIMetadata& aSRIMetadata) { + MOZ_ASSERT(aElement); + + nsTArray<PreloadInfo>::index_type i = + mPreloads.IndexOf(aElement->GetScriptURI(), 0, PreloadURIComparator()); + if (i == nsTArray<PreloadInfo>::NoIndex) { + return nullptr; + } + + // Found preloaded request. Note that a script-inserted script can steal a + // preload! + RefPtr<ScriptLoadRequest> request = mPreloads[i].mRequest; + request->SetIsLoadRequest(aElement); + + if (request->mWasCompiledOMT && !request->IsModuleRequest()) { + request->SetReady(); + } + + nsString preloadCharset(mPreloads[i].mCharset); + mPreloads.RemoveElementAt(i); + + // Double-check that the charset the preload used is the same as the charset + // we have now. + nsAutoString elementCharset; + aElement->GetScriptCharset(elementCharset); + + if (!elementCharset.Equals(preloadCharset) || + aElement->GetCORSMode() != request->CORSMode() || + aScriptKind != request->mKind) { + // Drop the preload. + request->Cancel(); + AccumulateCategorical(LABELS_DOM_SCRIPT_PRELOAD_RESULT::RequestMismatch); + return nullptr; + } + + if (!aSRIMetadata.CanTrustBeDelegatedTo(request->mIntegrity)) { + // Don't cancel link preload requests, we want to deliver onload according + // the result of the load, cancellation would unexpectedly lead to error + // notification. + if (!request->IsLinkPreloadScript()) { + request->Cancel(); + } + return nullptr; + } + + // Report any errors that we skipped while preloading. + ReportPreloadErrorsToConsole(request); + + // This makes sure the pending preload (if exists) for this resource is + // properly marked as used and thus not notified in the console as unused. + request->NotifyUsage(); + // A used preload must no longer be found in the Document's hash table. Any + // <link preload> tag after the <script> tag will start a new request, that + // can be satisfied from a different cache, but not from the preload cache. + request->RemoveSelf(mDocument); + + return request; +} + +void ScriptLoader::GetSRIMetadata(const nsAString& aIntegrityAttr, + SRIMetadata* aMetadataOut) { + MOZ_ASSERT(aMetadataOut->IsEmpty()); + + if (aIntegrityAttr.IsEmpty()) { + return; + } + + MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug, + ("ScriptLoader::GetSRIMetadata, integrity=%s", + NS_ConvertUTF16toUTF8(aIntegrityAttr).get())); + + nsAutoCString sourceUri; + if (mDocument->GetDocumentURI()) { + mDocument->GetDocumentURI()->GetAsciiSpec(sourceUri); + } + SRICheck::IntegrityMetadata(aIntegrityAttr, sourceUri, mReporter, + aMetadataOut); +} + +ReferrerPolicy ScriptLoader::GetReferrerPolicy(nsIScriptElement* aElement) { + ReferrerPolicy scriptReferrerPolicy = aElement->GetReferrerPolicy(); + if (scriptReferrerPolicy != ReferrerPolicy::_empty) { + return scriptReferrerPolicy; + } + return mDocument->GetReferrerPolicy(); +} + +namespace { + +class NotifyOffThreadScriptLoadCompletedRunnable : public Runnable { + RefPtr<ScriptLoadRequest> mRequest; + RefPtr<ScriptLoader> mLoader; + RefPtr<DocGroup> mDocGroup; + JS::OffThreadToken* mToken; + + public: + ScriptLoadRequest* GetScriptLoadRequest() { return mRequest; } + + NotifyOffThreadScriptLoadCompletedRunnable(ScriptLoadRequest* aRequest, + ScriptLoader* aLoader) + : Runnable("dom::NotifyOffThreadScriptLoadCompletedRunnable"), + mRequest(aRequest), + mLoader(aLoader), + mDocGroup(aLoader->GetDocGroup()), + mToken(nullptr) { + MOZ_ASSERT(NS_IsMainThread()); + } + + virtual ~NotifyOffThreadScriptLoadCompletedRunnable(); + + void SetToken(JS::OffThreadToken* aToken) { + MOZ_ASSERT(aToken && !mToken); + mToken = aToken; + } + + static void Dispatch( + already_AddRefed<NotifyOffThreadScriptLoadCompletedRunnable>&& aSelf) { + RefPtr<NotifyOffThreadScriptLoadCompletedRunnable> self = aSelf; + RefPtr<DocGroup> docGroup = self->mDocGroup; + docGroup->Dispatch(TaskCategory::Other, self.forget()); + } + + NS_DECL_NSIRUNNABLE +}; + +} /* anonymous namespace */ + +void ScriptLoader::Shutdown() { + CancelScriptLoadRequests(); + GiveUpBytecodeEncoding(); +} + +void ScriptLoader::CancelScriptLoadRequests() { + // Cancel all requests that have not been executed. + if (mParserBlockingRequest) { + mParserBlockingRequest->Cancel(); + } + + for (ScriptLoadRequest* req = mXSLTRequests.getFirst(); req; + req = req->getNext()) { + req->Cancel(); + } + + for (ScriptLoadRequest* req = mDeferRequests.getFirst(); req; + req = req->getNext()) { + req->Cancel(); + } + + for (ScriptLoadRequest* req = mLoadingAsyncRequests.getFirst(); req; + req = req->getNext()) { + req->Cancel(); + } + + for (ScriptLoadRequest* req = mLoadedAsyncRequests.getFirst(); req; + req = req->getNext()) { + req->Cancel(); + } + + for (ScriptLoadRequest* req = mDynamicImportRequests.getFirst(); req; + req = req->getNext()) { + req->Cancel(); + } + + for (ScriptLoadRequest* req = + mNonAsyncExternalScriptInsertedRequests.getFirst(); + req; req = req->getNext()) { + req->Cancel(); + } + + for (size_t i = 0; i < mPreloads.Length(); i++) { + mPreloads[i].mRequest->Cancel(); + } +} + +nsresult ScriptLoader::ProcessOffThreadRequest(ScriptLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->mProgress == ScriptLoadRequest::Progress::eCompiling); + MOZ_ASSERT(!aRequest->mWasCompiledOMT); + + aRequest->mWasCompiledOMT = true; + + if (aRequest->IsModuleRequest()) { + MOZ_ASSERT(aRequest->mOffThreadToken); + ModuleLoadRequest* request = aRequest->AsModuleRequest(); + return ProcessFetchedModuleSource(request); + } + + // Element may not be ready yet if speculatively compiling, so process the + // request in ProcessPendingRequests when it is available. + MOZ_ASSERT_IF(!SpeculativeOMTParsingEnabled(), aRequest->GetScriptElement()); + if (!aRequest->GetScriptElement()) { + // Unblock onload here in case this request never gets executed. + aRequest->MaybeUnblockOnload(); + return NS_OK; + } + + aRequest->SetReady(); + + if (aRequest == mParserBlockingRequest) { + if (!ReadyToExecuteParserBlockingScripts()) { + // If not ready to execute scripts, schedule an async call to + // ProcessPendingRequests to handle it. + ProcessPendingRequestsAsync(); + return NS_OK; + } + + // Same logic as in top of ProcessPendingRequests. + mParserBlockingRequest = nullptr; + UnblockParser(aRequest); + ProcessRequest(aRequest); + ContinueParserAsync(aRequest); + return NS_OK; + } + + // Async scripts and blocking scripts can be executed right away. + if ((aRequest->IsAsyncScript() || aRequest->IsBlockingScript()) && + !aRequest->isInList()) { + return ProcessRequest(aRequest); + } + + // Process other scripts in the proper order. + ProcessPendingRequests(); + return NS_OK; +} + +NotifyOffThreadScriptLoadCompletedRunnable:: + ~NotifyOffThreadScriptLoadCompletedRunnable() { + if (MOZ_UNLIKELY(mRequest || mLoader) && !NS_IsMainThread()) { + NS_ReleaseOnMainThread( + "NotifyOffThreadScriptLoadCompletedRunnable::mRequest", + mRequest.forget()); + NS_ReleaseOnMainThread( + "NotifyOffThreadScriptLoadCompletedRunnable::mLoader", + mLoader.forget()); + } +} + +static void GetProfilerLabelForRequest(ScriptLoadRequest* aRequest, + nsACString& aOutString) { +#ifdef MOZ_GECKO_PROFILER + if (!profiler_is_active()) { + aOutString.Append("<script> element"); + return; + } + aOutString.Append("<script"); + if (aRequest->IsAsyncScript()) { + aOutString.Append(" async"); + } else if (aRequest->IsDeferredScript()) { + aOutString.Append(" defer"); + } + if (aRequest->IsModuleRequest()) { + aOutString.Append(" type=\"module\""); + } + + nsAutoCString url; + if (aRequest->mURI) { + aRequest->mURI->GetAsciiSpec(url); + } else { + url = "<unknown>"; + } + + if (aRequest->mIsInline) { + if (aRequest->GetParserCreated() != NOT_FROM_PARSER) { + aOutString.Append("> inline at line "); + aOutString.AppendInt(aRequest->mLineNo); + aOutString.Append(" of "); + } else { + aOutString.Append("> inline (dynamically created) in "); + } + aOutString.Append(url); + } else { + aOutString.Append(" src=\""); + aOutString.Append(url); + aOutString.Append("\">"); + } +#else + aOutString.Append("<script> element"); +#endif +} + +NS_IMETHODIMP +NotifyOffThreadScriptLoadCompletedRunnable::Run() { + MOZ_ASSERT(NS_IsMainThread()); + + // We want these to be dropped on the main thread, once we return from this + // function. + RefPtr<ScriptLoadRequest> request = std::move(mRequest); + + // Runnable pointer should have been cleared in the offthread callback. + MOZ_ASSERT(!request->mRunnable); + +#ifdef MOZ_GECKO_PROFILER + if (profiler_is_active()) { + ProfilerString8View scriptSourceString; + if (request->IsTextSource()) { + scriptSourceString = "ScriptCompileOffThread"; + } else if (request->IsBinASTSource()) { + scriptSourceString = "BinASTDecodeOffThread"; + } else { + MOZ_ASSERT(request->IsBytecode()); + scriptSourceString = "BytecodeDecodeOffThread"; + } + + nsAutoCString profilerLabelString; + GetProfilerLabelForRequest(request, profilerLabelString); + PROFILER_MARKER_TEXT( + scriptSourceString, JS, + MarkerTiming::Interval(request->mOffThreadParseStartTime, + request->mOffThreadParseStopTime), + profilerLabelString); + } +#endif + + RefPtr<ScriptLoader> loader = std::move(mLoader); + + // Request was already cancelled at some earlier point. + if (!request->mOffThreadToken) { + return NS_OK; + } + + return loader->ProcessOffThreadRequest(request); +} + +static void OffThreadScriptLoaderCallback(JS::OffThreadToken* aToken, + void* aCallbackData) { + RefPtr<NotifyOffThreadScriptLoadCompletedRunnable> aRunnable = dont_AddRef( + static_cast<NotifyOffThreadScriptLoadCompletedRunnable*>(aCallbackData)); + MOZ_ASSERT(aRunnable.get() == aRunnable->GetScriptLoadRequest()->mRunnable); + +#ifdef MOZ_GECKO_PROFILER + aRunnable->GetScriptLoadRequest()->mOffThreadParseStopTime = + TimeStamp::NowUnfuzzed(); +#endif + + LogRunnable::Run run(aRunnable); + + aRunnable->SetToken(aToken); + + // If mRunnable was cleared then request was canceled so do nothing. + if (!aRunnable->GetScriptLoadRequest()->mRunnable.exchange(nullptr)) { + return; + } + + NotifyOffThreadScriptLoadCompletedRunnable::Dispatch(aRunnable.forget()); +} + +nsresult ScriptLoader::AttemptAsyncScriptCompile(ScriptLoadRequest* aRequest, + bool* aCouldCompileOut) { + // If speculative parsing is enabled, the request may not be ready to run if + // the element is not yet available. + MOZ_ASSERT_IF(!SpeculativeOMTParsingEnabled() && !aRequest->IsModuleRequest(), + aRequest->IsReadyToRun()); + MOZ_ASSERT(!aRequest->mWasCompiledOMT); + MOZ_ASSERT(aCouldCompileOut && !*aCouldCompileOut); + + // Don't off-thread compile inline scripts. + if (aRequest->mIsInline) { + return NS_OK; + } + + nsCOMPtr<nsIScriptGlobalObject> globalObject = GetScriptGlobalObject(); + if (!globalObject) { + return NS_ERROR_FAILURE; + } + + AutoJSAPI jsapi; + if (!jsapi.Init(globalObject)) { + return NS_ERROR_FAILURE; + } + + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> global(cx, globalObject->GetGlobalJSObject()); + JS::CompileOptions options(cx); + + nsresult rv = FillCompileOptionsForRequest(jsapi, aRequest, global, &options); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aRequest->IsTextSource()) { + if (!JS::CanCompileOffThread(cx, options, aRequest->ScriptTextLength())) { + return NS_OK; + } + } else { + MOZ_ASSERT(aRequest->IsBytecode()); + + // NOTE: Regardless of using stencil XDR or not, we use off-thread parse + // global and instantiate off-thread, to avoid regressing performance. + options.useOffThreadParseGlobal = true; + + size_t length = + aRequest->mScriptBytecode.length() - aRequest->mBytecodeOffset; + if (!JS::CanDecodeOffThread(cx, options, length)) { + return NS_OK; + } + } + + RefPtr<NotifyOffThreadScriptLoadCompletedRunnable> runnable = + new NotifyOffThreadScriptLoadCompletedRunnable(aRequest, this); + + // Emulate dispatch. CompileOffThreadModule will call + // OffThreadScriptLoaderCallback were we will emulate run. + LogRunnable::LogDispatch(runnable); + +#ifdef MOZ_GECKO_PROFILER + aRequest->mOffThreadParseStartTime = TimeStamp::NowUnfuzzed(); +#endif + + // Save the runnable so it can be properly cleared during cancellation. + aRequest->mRunnable = runnable.get(); + auto signalOOM = + mozilla::MakeScopeExit([&aRequest]() { aRequest->mRunnable = nullptr; }); + + if (aRequest->IsModuleRequest()) { + MOZ_ASSERT(aRequest->IsTextSource()); + MaybeSourceText maybeSource; + nsresult rv = GetScriptSource(cx, aRequest, &maybeSource); + NS_ENSURE_SUCCESS(rv, rv); + + aRequest->mOffThreadToken = + maybeSource.constructed<SourceText<char16_t>>() + ? JS::CompileOffThreadModule( + cx, options, maybeSource.ref<SourceText<char16_t>>(), + OffThreadScriptLoaderCallback, static_cast<void*>(runnable)) + : JS::CompileOffThreadModule( + cx, options, maybeSource.ref<SourceText<Utf8Unit>>(), + OffThreadScriptLoaderCallback, static_cast<void*>(runnable)); + if (!aRequest->mOffThreadToken) { + return NS_ERROR_OUT_OF_MEMORY; + } + } else if (aRequest->IsBytecode()) { + aRequest->mOffThreadToken = JS::DecodeOffThreadScript( + cx, options, aRequest->mScriptBytecode, aRequest->mBytecodeOffset, + OffThreadScriptLoaderCallback, static_cast<void*>(runnable)); + if (!aRequest->mOffThreadToken) { + return NS_ERROR_OUT_OF_MEMORY; + } + } else { + MOZ_ASSERT(aRequest->IsTextSource()); + MaybeSourceText maybeSource; + nsresult rv = GetScriptSource(cx, aRequest, &maybeSource); + NS_ENSURE_SUCCESS(rv, rv); + + aRequest->mOffThreadToken = + maybeSource.constructed<SourceText<char16_t>>() + ? JS::CompileOffThread( + cx, options, maybeSource.ref<SourceText<char16_t>>(), + OffThreadScriptLoaderCallback, static_cast<void*>(runnable)) + : JS::CompileOffThread( + cx, options, maybeSource.ref<SourceText<Utf8Unit>>(), + OffThreadScriptLoaderCallback, static_cast<void*>(runnable)); + if (!aRequest->mOffThreadToken) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + signalOOM.release(); + + aRequest->BlockOnload(mDocument); + + // Once the compilation is finished, an event would be added to the event loop + // to call ScriptLoader::ProcessOffThreadRequest with the same request. + aRequest->mProgress = ScriptLoadRequest::Progress::eCompiling; + + *aCouldCompileOut = true; + Unused << runnable.forget(); + return NS_OK; +} + +nsresult ScriptLoader::CompileOffThreadOrProcessRequest( + ScriptLoadRequest* aRequest) { + NS_ASSERTION(nsContentUtils::IsSafeToRunScript(), + "Processing requests when running scripts is unsafe."); + + if (!aRequest->mOffThreadToken && !aRequest->InCompilingStage()) { + bool couldCompile = false; + nsresult rv = AttemptAsyncScriptCompile(aRequest, &couldCompile); + if (NS_FAILED(rv)) { + HandleLoadError(aRequest, rv); + return rv; + } + + if (couldCompile) { + return NS_OK; + } + } + + return ProcessRequest(aRequest); +} + +nsresult ScriptLoader::GetScriptSource(JSContext* aCx, + ScriptLoadRequest* aRequest, + MaybeSourceText* aMaybeSource) { + // If there's no script text, we try to get it from the element + if (aRequest->mIsInline) { + nsAutoString inlineData; + aRequest->GetScriptElement()->GetScriptText(inlineData); + + size_t nbytes = inlineData.Length() * sizeof(char16_t); + JS::UniqueTwoByteChars chars( + static_cast<char16_t*>(JS_malloc(aCx, nbytes))); + if (!chars) { + return NS_ERROR_OUT_OF_MEMORY; + } + + memcpy(chars.get(), inlineData.get(), nbytes); + + SourceText<char16_t> srcBuf; + if (!srcBuf.init(aCx, std::move(chars), inlineData.Length())) { + return NS_ERROR_OUT_OF_MEMORY; + } + + aMaybeSource->construct<SourceText<char16_t>>(std::move(srcBuf)); + return NS_OK; + } + + size_t length = aRequest->ScriptTextLength(); + if (aRequest->IsUTF16Text()) { + JS::UniqueTwoByteChars chars; + chars.reset(aRequest->ScriptText<char16_t>().extractOrCopyRawBuffer()); + if (!chars) { + JS_ReportOutOfMemory(aCx); + return NS_ERROR_OUT_OF_MEMORY; + } + + SourceText<char16_t> srcBuf; + if (!srcBuf.init(aCx, std::move(chars), length)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + aMaybeSource->construct<SourceText<char16_t>>(std::move(srcBuf)); + return NS_OK; + } + + MOZ_ASSERT(aRequest->IsUTF8Text()); + UniquePtr<Utf8Unit[], JS::FreePolicy> chars; + chars.reset(aRequest->ScriptText<Utf8Unit>().extractOrCopyRawBuffer()); + if (!chars) { + JS_ReportOutOfMemory(aCx); + return NS_ERROR_OUT_OF_MEMORY; + } + + SourceText<Utf8Unit> srcBuf; + if (!srcBuf.init(aCx, std::move(chars), length)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + aMaybeSource->construct<SourceText<Utf8Unit>>(std::move(srcBuf)); + return NS_OK; +} + +nsresult ScriptLoader::ProcessRequest(ScriptLoadRequest* aRequest) { + LOG(("ScriptLoadRequest (%p): Process request", aRequest)); + + NS_ASSERTION(nsContentUtils::IsSafeToRunScript(), + "Processing requests when running scripts is unsafe."); + NS_ASSERTION(aRequest->IsReadyToRun(), + "Processing a request that is not ready to run."); + + NS_ENSURE_ARG(aRequest); + + auto unblockOnload = MakeScopeExit([&] { aRequest->MaybeUnblockOnload(); }); + + if (aRequest->IsModuleRequest()) { + ModuleLoadRequest* request = aRequest->AsModuleRequest(); + if (request->mModuleScript) { + if (!InstantiateModuleTree(request)) { + request->mModuleScript = nullptr; + } + } + + if (!request->mModuleScript) { + // There was an error fetching a module script. Nothing to do here. + LOG(("ScriptLoadRequest (%p): Error loading request, firing error", + aRequest)); + FireScriptAvailable(NS_ERROR_FAILURE, aRequest); + return NS_OK; + } + } + + nsCOMPtr<nsINode> scriptElem = + do_QueryInterface(aRequest->GetScriptElement()); + + nsCOMPtr<Document> doc; + if (!aRequest->mIsInline) { + doc = scriptElem->OwnerDoc(); + } + + nsCOMPtr<nsIScriptElement> oldParserInsertedScript; + uint32_t parserCreated = aRequest->GetParserCreated(); + if (parserCreated) { + oldParserInsertedScript = mCurrentParserInsertedScript; + mCurrentParserInsertedScript = aRequest->GetScriptElement(); + } + + aRequest->GetScriptElement()->BeginEvaluating(); + + FireScriptAvailable(NS_OK, aRequest); + + // The window may have gone away by this point, in which case there's no point + // in trying to run the script. + + { + // Try to perform a microtask checkpoint + nsAutoMicroTask mt; + } + + nsPIDOMWindowInner* pwin = mDocument->GetInnerWindow(); + bool runScript = !!pwin; + if (runScript) { + nsContentUtils::DispatchTrustedEvent( + scriptElem->OwnerDoc(), scriptElem, u"beforescriptexecute"_ns, + CanBubble::eYes, Cancelable::eYes, &runScript); + } + + // Inner window could have gone away after firing beforescriptexecute + pwin = mDocument->GetInnerWindow(); + if (!pwin) { + runScript = false; + } + + nsresult rv = NS_OK; + if (runScript) { + if (doc) { + doc->IncrementIgnoreDestructiveWritesCounter(); + } + rv = EvaluateScript(aRequest); + if (doc) { + doc->DecrementIgnoreDestructiveWritesCounter(); + } + + nsContentUtils::DispatchTrustedEvent(scriptElem->OwnerDoc(), scriptElem, + u"afterscriptexecute"_ns, + CanBubble::eYes, Cancelable::eNo); + } + + FireScriptEvaluated(rv, aRequest); + + aRequest->GetScriptElement()->EndEvaluating(); + + if (parserCreated) { + mCurrentParserInsertedScript = oldParserInsertedScript; + } + + if (aRequest->mOffThreadToken) { + // The request was parsed off-main-thread, but the result of the off + // thread parse was not actually needed to process the request + // (disappearing window, some other error, ...). Finish the + // request to avoid leaks in the JS engine. + MOZ_ASSERT(!aRequest->IsModuleRequest()); + aRequest->MaybeCancelOffThreadScript(); + } + + // Free any source data, but keep the bytecode content as we might have to + // save it later. + aRequest->ClearScriptSource(); + if (aRequest->IsBytecode()) { + // We received bytecode as input, thus we were decoding, and we will not be + // encoding the bytecode once more. We can safely clear the content of this + // buffer. + aRequest->mScriptBytecode.clearAndFree(); + } + + return rv; +} + +void ScriptLoader::ProcessDynamicImport(ModuleLoadRequest* aRequest) { + if (aRequest->mModuleScript) { + if (!InstantiateModuleTree(aRequest)) { + aRequest->mModuleScript = nullptr; + } + } + + nsresult rv = NS_ERROR_FAILURE; + if (aRequest->mModuleScript) { + rv = EvaluateScript(aRequest); + } + + if (NS_FAILED(rv)) { + FinishDynamicImportAndReject(aRequest, rv); + } +} + +void ScriptLoader::FireScriptAvailable(nsresult aResult, + ScriptLoadRequest* aRequest) { + for (int32_t i = 0; i < mObservers.Count(); i++) { + nsCOMPtr<nsIScriptLoaderObserver> obs = mObservers[i]; + obs->ScriptAvailable(aResult, aRequest->GetScriptElement(), + aRequest->mIsInline, aRequest->mURI, + aRequest->mLineNo); + } + + aRequest->FireScriptAvailable(aResult); +} + +void ScriptLoader::FireScriptEvaluated(nsresult aResult, + ScriptLoadRequest* aRequest) { + for (int32_t i = 0; i < mObservers.Count(); i++) { + nsCOMPtr<nsIScriptLoaderObserver> obs = mObservers[i]; + obs->ScriptEvaluated(aResult, aRequest->GetScriptElement(), + aRequest->mIsInline); + } + + aRequest->FireScriptEvaluated(aResult); +} + +already_AddRefed<nsIScriptGlobalObject> ScriptLoader::GetScriptGlobalObject() { + if (!mDocument) { + return nullptr; + } + + nsPIDOMWindowInner* pwin = mDocument->GetInnerWindow(); + if (!pwin) { + return nullptr; + } + + nsCOMPtr<nsIScriptGlobalObject> globalObject = do_QueryInterface(pwin); + NS_ASSERTION(globalObject, "windows must be global objects"); + + // and make sure we are setup for this type of script. + nsresult rv = globalObject->EnsureScriptEnvironment(); + if (NS_FAILED(rv)) { + return nullptr; + } + + return globalObject.forget(); +} + +nsresult ScriptLoader::FillCompileOptionsForRequest( + const mozilla::dom::AutoJSAPI& jsapi, ScriptLoadRequest* aRequest, + JS::Handle<JSObject*> aScopeChain, JS::CompileOptions* aOptions) { + // It's very important to use aRequest->mURI, not the final URI of the channel + // aRequest ended up getting script data from, as the script filename. + nsresult rv = aRequest->mURI->GetSpec(aRequest->mURL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mDocument) { + mDocument->NoteScriptTrackingStatus(aRequest->mURL, aRequest->IsTracking()); + } + + const char* introductionType; + if (aRequest->IsModuleRequest() && + !aRequest->AsModuleRequest()->IsTopLevel()) { + introductionType = "importedModule"; + } else if (!aRequest->mIsInline) { + introductionType = "srcScript"; + } else if (aRequest->GetParserCreated() == FROM_PARSER_NETWORK) { + introductionType = "inlineScript"; + } else { + introductionType = "injectedScript"; + } + aOptions->setIntroductionInfoToCaller(jsapi.cx(), introductionType); + aOptions->setFileAndLine(aRequest->mURL.get(), aRequest->mLineNo); + aOptions->setIsRunOnce(true); + aOptions->setNoScriptRval(true); + if (aRequest->mSourceMapURL) { + aOptions->setSourceMapURL(aRequest->mSourceMapURL->get()); + } + if (aRequest->mOriginPrincipal) { + nsIPrincipal* scriptPrin = nsContentUtils::ObjectPrincipal(aScopeChain); + bool subsumes = scriptPrin->Subsumes(aRequest->mOriginPrincipal); + aOptions->setMutedErrors(!subsumes); + } + + if (aRequest->IsModuleRequest()) { + aOptions->hideScriptFromDebugger = true; + } + + return NS_OK; +} + +/* static */ +bool ScriptLoader::ShouldCacheBytecode(ScriptLoadRequest* aRequest) { + using mozilla::TimeDuration; + using mozilla::TimeStamp; + + // We need the nsICacheInfoChannel to exist to be able to open the alternate + // data output stream. This pointer would only be non-null if the bytecode was + // activated at the time the channel got created in StartLoad. + if (!aRequest->mCacheInfo) { + LOG(("ScriptLoadRequest (%p): Cannot cache anything (cacheInfo = %p)", + aRequest, aRequest->mCacheInfo.get())); + return false; + } + + // Look at the preference to know which strategy (parameters) should be used + // when the bytecode cache is enabled. + int32_t strategy = StaticPrefs::dom_script_loader_bytecode_cache_strategy(); + + // List of parameters used by the strategies. + bool hasSourceLengthMin = false; + bool hasFetchCountMin = false; + size_t sourceLengthMin = 100; + size_t binASTLengthMin = 70; + int32_t fetchCountMin = 4; + + LOG(("ScriptLoadRequest (%p): Bytecode-cache: strategy = %d.", aRequest, + strategy)); + switch (strategy) { + case -2: { + // Reader mode, keep requesting alternate data but no longer save it. + LOG(("ScriptLoadRequest (%p): Bytecode-cache: Encoding disabled.", + aRequest)); + return false; + } + case -1: { + // Eager mode, skip heuristics! + hasSourceLengthMin = false; + hasFetchCountMin = false; + break; + } + default: + case 0: { + hasSourceLengthMin = true; + hasFetchCountMin = true; + sourceLengthMin = 1024; + binASTLengthMin = 700; + // If we were to optimize only for speed, without considering the impact + // on memory, we should set this threshold to 2. (Bug 900784 comment 120) + fetchCountMin = 4; + break; + } + } + + // If the script is too small/large, do not attempt at creating a bytecode + // cache for this script, as the overhead of parsing it might not be worth the + // effort. + if (hasSourceLengthMin) { + size_t sourceLength; + size_t minLength; + if (aRequest->IsTextSource()) { + sourceLength = aRequest->mScriptTextLength; + minLength = sourceLengthMin; + } else { + MOZ_ASSERT(aRequest->IsBinASTSource()); + sourceLength = aRequest->ScriptBinASTData().length(); + minLength = binASTLengthMin; + } + if (sourceLength < minLength) { + LOG(("ScriptLoadRequest (%p): Bytecode-cache: Script is too small.", + aRequest)); + return false; + } + } + + // Check that we loaded the cache entry a few times before attempting any + // bytecode-cache optimization, such that we do not waste time on entry which + // are going to be dropped soon. + if (hasFetchCountMin) { + int32_t fetchCount = 0; + if (NS_FAILED(aRequest->mCacheInfo->GetCacheTokenFetchCount(&fetchCount))) { + LOG(("ScriptLoadRequest (%p): Bytecode-cache: Cannot get fetchCount.", + aRequest)); + return false; + } + LOG(("ScriptLoadRequest (%p): Bytecode-cache: fetchCount = %d.", aRequest, + fetchCount)); + if (fetchCount < fetchCountMin) { + return false; + } + } + + LOG(("ScriptLoadRequest (%p): Bytecode-cache: Trigger encoding.", aRequest)); + return true; +} + +class MOZ_RAII AutoSetProcessingScriptTag { + nsCOMPtr<nsIScriptContext> mContext; + bool mOldTag; + + public: + explicit AutoSetProcessingScriptTag(nsIScriptContext* aContext) + : mContext(aContext), mOldTag(mContext->GetProcessingScriptTag()) { + mContext->SetProcessingScriptTag(true); + } + + ~AutoSetProcessingScriptTag() { mContext->SetProcessingScriptTag(mOldTag); } +}; + +static nsresult ExecuteCompiledScript(JSContext* aCx, + ScriptLoadRequest* aRequest, + JSExecutionContext& aExec, + ClassicScript* aLoaderScript) { + JS::Rooted<JSScript*> script(aCx, aExec.GetScript()); + if (!script) { + // Compilation succeeds without producing a script if scripting is + // disabled for the global. + return NS_OK; + } + + if (JS::GetScriptPrivate(script).isUndefined()) { + aLoaderScript->AssociateWithScript(script); + } + + return aExec.ExecScript(); +} + +nsresult ScriptLoader::EvaluateScript(ScriptLoadRequest* aRequest) { + using namespace mozilla::Telemetry; + MOZ_ASSERT(aRequest->IsReadyToRun()); + + // We need a document to evaluate scripts. + if (!mDocument) { + return NS_ERROR_FAILURE; + } + + bool isDynamicImport = aRequest->IsModuleRequest() && + aRequest->AsModuleRequest()->IsDynamicImport(); + if (!isDynamicImport) { + nsCOMPtr<nsIContent> scriptContent( + do_QueryInterface(aRequest->GetScriptElement())); + MOZ_ASSERT(scriptContent); + Document* ownerDoc = scriptContent->OwnerDoc(); + if (ownerDoc != mDocument) { + // Willful violation of HTML5 as of 2010-12-01 + return NS_ERROR_FAILURE; + } + } + + nsCOMPtr<nsIScriptGlobalObject> globalObject = GetScriptGlobalObject(); + if (!globalObject) { + return NS_ERROR_FAILURE; + } + + // Make sure context is a strong reference since we access it after + // we've executed a script, which may cause all other references to + // the context to go away. + nsCOMPtr<nsIScriptContext> context = globalObject->GetScriptContext(); + if (!context) { + return NS_ERROR_FAILURE; + } + +#ifdef MOZ_GECKO_PROFILER + nsCOMPtr<nsPIDOMWindowOuter> window = mDocument->GetWindow(); + nsIDocShell* docShell = window ? window->GetDocShell() : nullptr; +#endif + nsAutoCString profilerLabelString; + GetProfilerLabelForRequest(aRequest, profilerLabelString); + + // New script entry point required, due to the "Create a script" sub-step of + // http://www.whatwg.org/specs/web-apps/current-work/#execute-the-script-block + nsAutoMicroTask mt; + AutoEntryScript aes(globalObject, profilerLabelString.get(), true); + JSContext* cx = aes.cx(); + JS::Rooted<JSObject*> global(cx, globalObject->GetGlobalJSObject()); + + AutoSetProcessingScriptTag setProcessingScriptTag(context); + + nsresult rv; + { + if (aRequest->IsModuleRequest()) { + // When a module is already loaded, it is not feched a second time and the + // mDataType of the request might remain set to DataType::Unknown. + MOZ_ASSERT(aRequest->IsTextSource() || aRequest->IsUnknownDataType()); + LOG(("ScriptLoadRequest (%p): Evaluate Module", aRequest)); + AUTO_PROFILER_MARKER_TEXT("ModuleEvaluation", JS, + MarkerInnerWindowIdFromDocShell(docShell), + profilerLabelString); + + // currentScript is set to null for modules. + AutoCurrentScriptUpdater scriptUpdater(this, nullptr); + + ModuleLoadRequest* request = aRequest->AsModuleRequest(); + MOZ_ASSERT(request->mModuleScript); + MOZ_ASSERT(!request->mOffThreadToken); + + ModuleScript* moduleScript = request->mModuleScript; + if (moduleScript->HasErrorToRethrow()) { + LOG(("ScriptLoadRequest (%p): module has error to rethrow", + aRequest)); + JS::Rooted<JS::Value> error(cx, moduleScript->ErrorToRethrow()); + if (!JS::ContextOptionsRef(cx).topLevelAwait()) { + JS_SetPendingException(cx, error); + // For a dynamic import, the promise is rejected. Otherwise an error + // is either reported by AutoEntryScript. + if (request->IsDynamicImport()) { + FinishDynamicImport_NoTLA(cx, request, NS_OK); + } + } else { + ErrorResult err; + RefPtr<Promise> aPromise = Promise::Create(globalObject, err); + if (NS_WARN_IF(err.Failed())) { + return err.StealNSResult(); + } + aPromise->MaybeReject(error); + JS::Rooted<JSObject*> aEvaluationPromise(cx, aPromise->PromiseObj()); + if (request->IsDynamicImport()) { + FinishDynamicImport(cx, request, NS_OK, aEvaluationPromise); + } else { + if (!JS::ThrowOnModuleEvaluationFailure(cx, aEvaluationPromise)) { + LOG(("ScriptLoadRequest (%p): evaluation failed", aRequest)); + // For a dynamic import, the promise is rejected. Otherwise an + // error is either reported by AutoEntryScript. + } + } + } + return NS_OK; + } + + JS::Rooted<JSObject*> module(cx, moduleScript->ModuleRecord()); + MOZ_ASSERT(module); + + rv = InitDebuggerDataForModuleTree(cx, request); + NS_ENSURE_SUCCESS(rv, rv); + + TRACE_FOR_TEST(aRequest->GetScriptElement(), + "scriptloader_evaluate_module"); + + JS::Rooted<JS::Value> rval(cx); + + rv = nsJSUtils::ModuleEvaluate(cx, module, &rval); + + if (NS_SUCCEEDED(rv)) { + // If we have an infinite loop in a module, which is stopped by the + // user, the module evaluation will fail, but we will not have an + // AutoEntryScript exception. + MOZ_ASSERT(!aes.HasException()); + } + + if (NS_FAILED(rv)) { + LOG(("ScriptLoadRequest (%p): evaluation failed", aRequest)); + // For a dynamic import, the promise is rejected. Otherwise an error is + // either reported by AutoEntryScript. + rv = NS_OK; + } + + if (!JS::ContextOptionsRef(cx).topLevelAwait()) { + if (request->IsDynamicImport()) { + FinishDynamicImport_NoTLA(cx, request, rv); + } + } else { + // Path for when Top Level Await is enabled + JS::Rooted<JSObject*> aEvaluationPromise(cx); + if (NS_SUCCEEDED(rv)) { + // If the user cancels the evaluation on an infinite loop, we need + // to skip this step. In that case, ModuleEvaluate will not return a + // promise, rval will be undefined. We should treat it as a failed + // evaluation, and reject appropriately. + aEvaluationPromise.set(&rval.toObject()); + } + if (request->IsDynamicImport()) { + FinishDynamicImport(cx, request, rv, aEvaluationPromise); + } else { + // If this is not a dynamic import, and if the promise is rejected, + // the value is unwrapped from the promise value. + if (!JS::ThrowOnModuleEvaluationFailure(cx, aEvaluationPromise)) { + LOG(("ScriptLoadRequest (%p): evaluation failed on throw", + aRequest)); + // For a dynamic import, the promise is rejected. Otherwise an + // error is either reported by AutoEntryScript. + rv = NS_OK; + } + } + } + + TRACE_FOR_TEST_NONE(aRequest->GetScriptElement(), + "scriptloader_no_encode"); + aRequest->mCacheInfo = nullptr; + } else { + // Update our current script. + AutoCurrentScriptUpdater scriptUpdater(this, + aRequest->GetScriptElement()); + + // Create a ClassicScript object and associate it with the JSScript. + RefPtr<ClassicScript> classicScript = + new ClassicScript(aRequest->mFetchOptions, aRequest->mBaseURL); + + JS::CompileOptions options(cx); + rv = FillCompileOptionsForRequest(aes, aRequest, global, &options); + options.setPrivateValue(JS::PrivateValue(classicScript)); + + if (NS_SUCCEEDED(rv)) { + if (aRequest->IsBytecode()) { + TRACE_FOR_TEST(aRequest->GetScriptElement(), "scriptloader_execute"); + JSExecutionContext exec(cx, global); + if (aRequest->mOffThreadToken) { + LOG(("ScriptLoadRequest (%p): Decode Bytecode & Join and Execute", + aRequest)); + rv = exec.JoinDecode(&aRequest->mOffThreadToken); + } else { + LOG(("ScriptLoadRequest (%p): Decode Bytecode and Execute", + aRequest)); + AUTO_PROFILER_MARKER_TEXT("BytecodeDecodeMainThread", JS, + MarkerInnerWindowIdFromDocShell(docShell), + profilerLabelString); + + rv = exec.Decode(options, aRequest->mScriptBytecode, + aRequest->mBytecodeOffset); + } + + if (rv == NS_OK) { + AUTO_PROFILER_MARKER_TEXT("ScriptExecution", JS, + MarkerInnerWindowIdFromDocShell(docShell), + profilerLabelString); + rv = ExecuteCompiledScript(cx, aRequest, exec, classicScript); + } + + // We do not expect to be saving anything when we already have some + // bytecode. + MOZ_ASSERT(!aRequest->mCacheInfo); + } else { + MOZ_ASSERT(aRequest->IsSource()); + JS::Rooted<JSScript*> script(cx); + bool encodeBytecode = ShouldCacheBytecode(aRequest); + + { + JSExecutionContext exec(cx, global); + exec.SetEncodeBytecode(encodeBytecode); + TRACE_FOR_TEST(aRequest->GetScriptElement(), + "scriptloader_execute"); + if (aRequest->mOffThreadToken) { + // Off-main-thread parsing. + LOG( + ("ScriptLoadRequest (%p): Join (off-thread parsing) and " + "Execute", + aRequest)); + if (aRequest->IsBinASTSource()) { + rv = exec.JoinDecodeBinAST(&aRequest->mOffThreadToken); + } else { + MOZ_ASSERT(aRequest->IsTextSource()); + rv = exec.JoinCompile(&aRequest->mOffThreadToken); + } + } else { + // Main thread parsing (inline and small scripts) + LOG(("ScriptLoadRequest (%p): Compile And Exec", aRequest)); + if (aRequest->IsBinASTSource()) { + AUTO_PROFILER_MARKER_TEXT( + "BinASTDecodeMainThread", JS, + MarkerInnerWindowIdFromDocShell(docShell), + profilerLabelString); + + rv = exec.DecodeBinAST(options, + aRequest->ScriptBinASTData().begin(), + aRequest->ScriptBinASTData().length()); + } else { + MOZ_ASSERT(aRequest->IsTextSource()); + MaybeSourceText maybeSource; + rv = GetScriptSource(cx, aRequest, &maybeSource); + if (NS_SUCCEEDED(rv)) { + AUTO_PROFILER_MARKER_TEXT( + "ScriptCompileMainThread", JS, + MarkerInnerWindowIdFromDocShell(docShell), + profilerLabelString); + + rv = maybeSource.constructed<SourceText<char16_t>>() + ? exec.Compile( + options, + maybeSource.ref<SourceText<char16_t>>()) + : exec.Compile( + options, + maybeSource.ref<SourceText<Utf8Unit>>()); + } + } + } + + if (rv == NS_OK) { + script = exec.GetScript(); + AUTO_PROFILER_MARKER_TEXT( + "ScriptExecution", JS, + MarkerInnerWindowIdFromDocShell(docShell), + profilerLabelString); + rv = ExecuteCompiledScript(cx, aRequest, exec, classicScript); + } + } + + // Queue the current script load request to later save the bytecode. + if (script && encodeBytecode) { + aRequest->SetScript(script); + TRACE_FOR_TEST(aRequest->GetScriptElement(), "scriptloader_encode"); + MOZ_ASSERT(aRequest->mBytecodeOffset == + aRequest->mScriptBytecode.length()); + RegisterForBytecodeEncoding(aRequest); + } else { + LOG( + ("ScriptLoadRequest (%p): Bytecode-cache: disabled (rv = %X, " + "script = %p)", + aRequest, unsigned(rv), script.get())); + TRACE_FOR_TEST_NONE(aRequest->GetScriptElement(), + "scriptloader_no_encode"); + aRequest->mCacheInfo = nullptr; + } + } + } + } + + // Even if we are not saving the bytecode of the current script, we have + // to trigger the encoding of the bytecode, as the current script can + // call functions of a script for which we are recording the bytecode. + LOG(("ScriptLoadRequest (%p): ScriptLoader = %p", aRequest, this)); + MaybeTriggerBytecodeEncoding(); + } + + return rv; +} + +/* static */ +LoadedScript* ScriptLoader::GetActiveScript(JSContext* aCx) { + JS::Value value = JS::GetScriptedCallerPrivate(aCx); + if (value.isUndefined()) { + return nullptr; + } + + return static_cast<LoadedScript*>(value.toPrivate()); +} + +void ScriptLoader::RegisterForBytecodeEncoding(ScriptLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->mCacheInfo); + MOZ_ASSERT(aRequest->mScript); + MOZ_DIAGNOSTIC_ASSERT(!aRequest->isInList()); + mBytecodeEncodingQueue.AppendElement(aRequest); +} + +void ScriptLoader::LoadEventFired() { + mLoadEventFired = true; + MaybeTriggerBytecodeEncoding(); +} + +void ScriptLoader::Destroy() { + // Off thread compilations will be canceled in ProcessRequest after the inner + // window is removed in Document::Destroy() + if (mShutdownObserver) { + mShutdownObserver->Unregister(); + mShutdownObserver = nullptr; + } + GiveUpBytecodeEncoding(); +} + +void ScriptLoader::MaybeTriggerBytecodeEncoding() { + // If we already gave up, ensure that we are not going to enqueue any script, + // and that we finalize them properly. + if (mGiveUpEncoding) { + LOG(("ScriptLoader (%p): Keep giving-up bytecode encoding.", this)); + GiveUpBytecodeEncoding(); + return; + } + + // We wait for the load event to be fired before saving the bytecode of + // any script to the cache. It is quite common to have load event + // listeners trigger more JavaScript execution, that we want to save as + // part of this start-up bytecode cache. + if (!mLoadEventFired) { + LOG(("ScriptLoader (%p): Wait for the load-end event to fire.", this)); + return; + } + + // No need to fire any event if there is no bytecode to be saved. + if (mBytecodeEncodingQueue.isEmpty()) { + LOG(("ScriptLoader (%p): No script in queue to be encoded.", this)); + return; + } + + // Wait until all scripts are loaded before saving the bytecode, such that + // we capture most of the intialization of the page. + if (HasPendingRequests()) { + LOG(("ScriptLoader (%p): Wait for other pending request to finish.", this)); + return; + } + + // Create a new runnable dedicated to encoding the content of the bytecode of + // all enqueued scripts when the document is idle. In case of failure, we + // give-up on encoding the bytecode. + nsCOMPtr<nsIRunnable> encoder = NewRunnableMethod( + "ScriptLoader::EncodeBytecode", this, &ScriptLoader::EncodeBytecode); + if (NS_FAILED(NS_DispatchToCurrentThreadQueue(encoder.forget(), + EventQueuePriority::Idle))) { + GiveUpBytecodeEncoding(); + return; + } + + LOG(("ScriptLoader (%p): Schedule bytecode encoding.", this)); +} + +void ScriptLoader::EncodeBytecode() { + LOG(("ScriptLoader (%p): Start bytecode encoding.", this)); + + // If any script got added in the previous loop cycle, wait until all + // remaining script executions are completed, such that we capture most of + // the initialization. + if (HasPendingRequests()) { + return; + } + + nsCOMPtr<nsIScriptGlobalObject> globalObject = GetScriptGlobalObject(); + if (!globalObject) { + GiveUpBytecodeEncoding(); + return; + } + + nsCOMPtr<nsIScriptContext> context = globalObject->GetScriptContext(); + if (!context) { + GiveUpBytecodeEncoding(); + return; + } + + TimeStamp startTime = TimeStamp::Now(); + + AutoEntryScript aes(globalObject, "encode bytecode", true); + RefPtr<ScriptLoadRequest> request; + while (!mBytecodeEncodingQueue.isEmpty()) { + request = mBytecodeEncodingQueue.StealFirst(); + EncodeRequestBytecode(aes.cx(), request); + request->mScriptBytecode.clearAndFree(); + request->DropBytecodeCacheReferences(); + } + + TimeDuration delta = TimeStamp::Now() - startTime; + Telemetry::Accumulate(Telemetry::JS_BYTECODE_CACHING_TIME, + static_cast<uint32_t>(delta.ToMilliseconds())); +} + +void ScriptLoader::EncodeRequestBytecode(JSContext* aCx, + ScriptLoadRequest* aRequest) { + using namespace mozilla::Telemetry; + nsresult rv = NS_OK; + MOZ_ASSERT(aRequest->mCacheInfo); + auto bytecodeFailed = mozilla::MakeScopeExit([&]() { + TRACE_FOR_TEST_NONE(aRequest->GetScriptElement(), + "scriptloader_bytecode_failed"); + }); + + JS::RootedScript script(aCx, aRequest->mScript); + if (!JS::FinishIncrementalEncoding(aCx, script, aRequest->mScriptBytecode)) { + // Encoding can be aborted for non-supported syntax (e.g. asm.js), or + // any other internal error. + // We don't care the error and just give up encoding. + JS_ClearPendingException(aCx); + + LOG(("ScriptLoadRequest (%p): Cannot serialize bytecode", aRequest)); + return; + } + + if (aRequest->mScriptBytecode.length() >= UINT32_MAX) { + LOG( + ("ScriptLoadRequest (%p): Bytecode cache is too large to be decoded " + "correctly.", + aRequest)); + return; + } + + // Open the output stream to the cache entry alternate data storage. This + // might fail if the stream is already open by another request, in which + // case, we just ignore the current one. + nsCOMPtr<nsIAsyncOutputStream> output; + rv = aRequest->mCacheInfo->OpenAlternativeOutputStream( + nsContentUtils::JSBytecodeMimeType(), aRequest->mScriptBytecode.length(), + getter_AddRefs(output)); + if (NS_FAILED(rv)) { + LOG( + ("ScriptLoadRequest (%p): Cannot open bytecode cache (rv = %X, output " + "= %p)", + aRequest, unsigned(rv), output.get())); + return; + } + MOZ_ASSERT(output); + + auto closeOutStream = mozilla::MakeScopeExit([&]() { + rv = output->CloseWithStatus(rv); + LOG(("ScriptLoadRequest (%p): Closing (rv = %X)", aRequest, unsigned(rv))); + }); + + uint32_t n; + rv = output->Write(reinterpret_cast<char*>(aRequest->mScriptBytecode.begin()), + aRequest->mScriptBytecode.length(), &n); + LOG(( + "ScriptLoadRequest (%p): Write bytecode cache (rv = %X, length = %u, " + "written = %u)", + aRequest, unsigned(rv), unsigned(aRequest->mScriptBytecode.length()), n)); + if (NS_FAILED(rv)) { + return; + } + + MOZ_RELEASE_ASSERT(aRequest->mScriptBytecode.length() == n); + + bytecodeFailed.release(); + TRACE_FOR_TEST_NONE(aRequest->GetScriptElement(), + "scriptloader_bytecode_saved"); +} + +void ScriptLoader::GiveUpBytecodeEncoding() { + // If the document went away prematurely, we still want to set this, in order + // to avoid queuing more scripts. + mGiveUpEncoding = true; + + // Ideally we prefer to properly end the incremental encoder, such that we + // would not keep a large buffer around. If we cannot, we fallback on the + // removal of all request from the current list and these large buffers would + // be removed at the same time as the source object. + nsCOMPtr<nsIScriptGlobalObject> globalObject = GetScriptGlobalObject(); + AutoAllowLegacyScriptExecution exemption; + Maybe<AutoEntryScript> aes; + + if (globalObject) { + nsCOMPtr<nsIScriptContext> context = globalObject->GetScriptContext(); + if (context) { + aes.emplace(globalObject, "give-up bytecode encoding", true); + } + } + + while (!mBytecodeEncodingQueue.isEmpty()) { + RefPtr<ScriptLoadRequest> request = mBytecodeEncodingQueue.StealFirst(); + LOG(("ScriptLoadRequest (%p): Cannot serialize bytecode", request.get())); + TRACE_FOR_TEST_NONE(request->GetScriptElement(), + "scriptloader_bytecode_failed"); + + if (aes.isSome()) { + JS::RootedScript script(aes->cx(), request->mScript); + if (!JS::FinishIncrementalEncoding(aes->cx(), script, + request->mScriptBytecode)) { + JS_ClearPendingException(aes->cx()); + } + } + + request->mScriptBytecode.clearAndFree(); + request->DropBytecodeCacheReferences(); + } +} + +bool ScriptLoader::HasPendingRequests() { + return mParserBlockingRequest || !mXSLTRequests.isEmpty() || + !mLoadedAsyncRequests.isEmpty() || + !mNonAsyncExternalScriptInsertedRequests.isEmpty() || + !mDeferRequests.isEmpty() || !mDynamicImportRequests.isEmpty() || + !mPendingChildLoaders.IsEmpty(); +} + +void ScriptLoader::ProcessPendingRequestsAsync() { + if (HasPendingRequests()) { + nsCOMPtr<nsIRunnable> task = + NewRunnableMethod("dom::ScriptLoader::ProcessPendingRequests", this, + &ScriptLoader::ProcessPendingRequests); + if (mDocument) { + mDocument->Dispatch(TaskCategory::Other, task.forget()); + } else { + NS_DispatchToCurrentThread(task.forget()); + } + } +} + +void ScriptLoader::ProcessPendingRequests() { + RefPtr<ScriptLoadRequest> request; + + if (mParserBlockingRequest && mParserBlockingRequest->IsReadyToRun() && + ReadyToExecuteParserBlockingScripts()) { + request.swap(mParserBlockingRequest); + UnblockParser(request); + ProcessRequest(request); + ContinueParserAsync(request); + } + + while (ReadyToExecuteParserBlockingScripts() && !mXSLTRequests.isEmpty() && + mXSLTRequests.getFirst()->IsReadyToRun()) { + request = mXSLTRequests.StealFirst(); + ProcessRequest(request); + } + + while (ReadyToExecuteScripts() && !mLoadedAsyncRequests.isEmpty()) { + request = mLoadedAsyncRequests.StealFirst(); + if (request->IsModuleRequest()) { + ProcessRequest(request); + } else { + CompileOffThreadOrProcessRequest(request); + } + } + + while (ReadyToExecuteScripts() && + !mNonAsyncExternalScriptInsertedRequests.isEmpty() && + mNonAsyncExternalScriptInsertedRequests.getFirst()->IsReadyToRun()) { + // Violate the HTML5 spec and execute these in the insertion order in + // order to make LABjs and the "order" plug-in for RequireJS work with + // their Gecko-sniffed code path. See + // http://lists.w3.org/Archives/Public/public-html/2010Oct/0088.html + request = mNonAsyncExternalScriptInsertedRequests.StealFirst(); + ProcessRequest(request); + } + + if (mDeferCheckpointReached && mXSLTRequests.isEmpty()) { + while (ReadyToExecuteScripts() && !mDeferRequests.isEmpty() && + mDeferRequests.getFirst()->IsReadyToRun()) { + request = mDeferRequests.StealFirst(); + ProcessRequest(request); + } + } + + while (!mPendingChildLoaders.IsEmpty() && + ReadyToExecuteParserBlockingScripts()) { + RefPtr<ScriptLoader> child = mPendingChildLoaders[0]; + mPendingChildLoaders.RemoveElementAt(0); + child->RemoveParserBlockingScriptExecutionBlocker(); + } + + if (mDeferCheckpointReached && mDocument && !mParserBlockingRequest && + mNonAsyncExternalScriptInsertedRequests.isEmpty() && + mXSLTRequests.isEmpty() && mDeferRequests.isEmpty() && + MaybeRemovedDeferRequests()) { + return ProcessPendingRequests(); + } + + if (mDeferCheckpointReached && mDocument && !mParserBlockingRequest && + mLoadingAsyncRequests.isEmpty() && mLoadedAsyncRequests.isEmpty() && + mNonAsyncExternalScriptInsertedRequests.isEmpty() && + mXSLTRequests.isEmpty() && mDeferRequests.isEmpty()) { + // No more pending scripts; time to unblock onload. + // OK to unblock onload synchronously here, since callers must be + // prepared for the world changing anyway. + mDeferCheckpointReached = false; + mDocument->UnblockOnload(true); + } +} + +bool ScriptLoader::ReadyToExecuteParserBlockingScripts() { + // Make sure the SelfReadyToExecuteParserBlockingScripts check is first, so + // that we don't block twice on an ancestor. + if (!SelfReadyToExecuteParserBlockingScripts()) { + return false; + } + + if (mDocument && mDocument->GetWindowContext()) { + for (WindowContext* wc = + mDocument->GetWindowContext()->GetParentWindowContext(); + wc; wc = wc->GetParentWindowContext()) { + if (Document* doc = wc->GetDocument()) { + ScriptLoader* ancestor = doc->ScriptLoader(); + if (!ancestor->SelfReadyToExecuteParserBlockingScripts() && + ancestor->AddPendingChildLoader(this)) { + AddParserBlockingScriptExecutionBlocker(); + return false; + } + } + } + } + + return true; +} + +template <typename Unit> +static nsresult ConvertToUnicode(nsIChannel* aChannel, const uint8_t* aData, + uint32_t aLength, + const nsAString& aHintCharset, + Document* aDocument, Unit*& aBufOut, + size_t& aLengthOut) { + if (!aLength) { + aBufOut = nullptr; + aLengthOut = 0; + return NS_OK; + } + + auto data = Span(aData, aLength); + + // The encoding info precedence is as follows from high to low: + // The BOM + // HTTP Content-Type (if name recognized) + // charset attribute (if name recognized) + // The encoding of the document + + UniquePtr<Decoder> unicodeDecoder; + + const Encoding* encoding; + size_t bomLength; + Tie(encoding, bomLength) = Encoding::ForBOM(data); + if (encoding) { + unicodeDecoder = encoding->NewDecoderWithBOMRemoval(); + } + + if (!unicodeDecoder && aChannel) { + nsAutoCString label; + if (NS_SUCCEEDED(aChannel->GetContentCharset(label)) && + (encoding = Encoding::ForLabel(label))) { + unicodeDecoder = encoding->NewDecoderWithoutBOMHandling(); + } + } + + if (!unicodeDecoder && (encoding = Encoding::ForLabel(aHintCharset))) { + unicodeDecoder = encoding->NewDecoderWithoutBOMHandling(); + } + + if (!unicodeDecoder && aDocument) { + unicodeDecoder = + aDocument->GetDocumentCharacterSet()->NewDecoderWithoutBOMHandling(); + } + + if (!unicodeDecoder) { + // Curiously, there are various callers that don't pass aDocument. The + // fallback in the old code was ISO-8859-1, which behaved like + // windows-1252. + unicodeDecoder = WINDOWS_1252_ENCODING->NewDecoderWithoutBOMHandling(); + } + + auto signalOOM = mozilla::MakeScopeExit([&aBufOut, &aLengthOut]() { + aBufOut = nullptr; + aLengthOut = 0; + }); + + CheckedInt<size_t> bufferLength = + ScriptDecoding<Unit>::MaxBufferLength(unicodeDecoder, aLength); + if (!bufferLength.isValid()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + CheckedInt<size_t> bufferByteSize = bufferLength * sizeof(Unit); + if (!bufferByteSize.isValid()) { + return NS_ERROR_OUT_OF_MEMORY; + } + + aBufOut = static_cast<Unit*>(js_malloc(bufferByteSize.value())); + if (!aBufOut) { + return NS_ERROR_OUT_OF_MEMORY; + } + + signalOOM.release(); + aLengthOut = ScriptDecoding<Unit>::DecodeInto( + unicodeDecoder, data, Span(aBufOut, bufferLength.value()), + /* aEndOfSource = */ true); + return NS_OK; +} + +/* static */ +nsresult ScriptLoader::ConvertToUTF16(nsIChannel* aChannel, + const uint8_t* aData, uint32_t aLength, + const nsAString& aHintCharset, + Document* aDocument, char16_t*& aBufOut, + size_t& aLengthOut) { + return ConvertToUnicode(aChannel, aData, aLength, aHintCharset, aDocument, + aBufOut, aLengthOut); +} + +/* static */ +nsresult ScriptLoader::ConvertToUTF8(nsIChannel* aChannel, const uint8_t* aData, + uint32_t aLength, + const nsAString& aHintCharset, + Document* aDocument, Utf8Unit*& aBufOut, + size_t& aLengthOut) { + return ConvertToUnicode(aChannel, aData, aLength, aHintCharset, aDocument, + aBufOut, aLengthOut); +} + +nsresult ScriptLoader::OnStreamComplete( + nsIIncrementalStreamLoader* aLoader, ScriptLoadRequest* aRequest, + nsresult aChannelStatus, nsresult aSRIStatus, + SRICheckDataVerifier* aSRIDataVerifier) { + NS_ASSERTION(aRequest, "null request in stream complete handler"); + NS_ENSURE_TRUE(aRequest, NS_ERROR_FAILURE); + + nsresult rv = VerifySRI(aRequest, aLoader, aSRIStatus, aSRIDataVerifier); + + if (NS_SUCCEEDED(rv)) { + // If we are loading from source, save the computed SRI hash or a dummy SRI + // hash in case we are going to save the bytecode of this script in the + // cache. + if (aRequest->IsSource()) { + uint32_t sriLength = 0; + rv = SaveSRIHash(aRequest, aSRIDataVerifier, &sriLength); + MOZ_ASSERT_IF(NS_SUCCEEDED(rv), + aRequest->mScriptBytecode.length() == sriLength); + + aRequest->mBytecodeOffset = JS::AlignTranscodingBytecodeOffset(sriLength); + if (aRequest->mBytecodeOffset != sriLength) { + // We need extra padding after SRI hash. + if (!aRequest->mScriptBytecode.resize(aRequest->mBytecodeOffset)) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + } + + if (NS_SUCCEEDED(rv)) { + rv = PrepareLoadedRequest(aRequest, aLoader, aChannelStatus); + } + + if (NS_FAILED(rv)) { + ReportErrorToConsole(aRequest, rv); + } + } + + if (NS_FAILED(rv)) { + // When loading bytecode, we verify the SRI hash. If it does not match the + // one from the document we restart the load, forcing us to load the source + // instead. If this happens do not remove the current request from script + // loader's data structures or fire any events. + if (aChannelStatus != NS_BINDING_RETARGETED) { + HandleLoadError(aRequest, rv); + } + } + + // Process our request and/or any pending ones + ProcessPendingRequests(); + + return rv; +} + +nsresult ScriptLoader::VerifySRI(ScriptLoadRequest* aRequest, + nsIIncrementalStreamLoader* aLoader, + nsresult aSRIStatus, + SRICheckDataVerifier* aSRIDataVerifier) const { + nsCOMPtr<nsIRequest> channelRequest; + aLoader->GetRequest(getter_AddRefs(channelRequest)); + nsCOMPtr<nsIChannel> channel; + channel = do_QueryInterface(channelRequest); + + nsresult rv = NS_OK; + if (!aRequest->mIntegrity.IsEmpty() && NS_SUCCEEDED((rv = aSRIStatus))) { + MOZ_ASSERT(aSRIDataVerifier); + MOZ_ASSERT(mReporter); + + nsAutoCString sourceUri; + if (mDocument && mDocument->GetDocumentURI()) { + mDocument->GetDocumentURI()->GetAsciiSpec(sourceUri); + } + rv = aSRIDataVerifier->Verify(aRequest->mIntegrity, channel, sourceUri, + mReporter); + if (channelRequest) { + mReporter->FlushReportsToConsole( + nsContentUtils::GetInnerWindowID(channelRequest)); + } else { + mReporter->FlushConsoleReports(mDocument); + } + if (NS_FAILED(rv)) { + rv = NS_ERROR_SRI_CORRUPT; + } + } + + return rv; +} + +nsresult ScriptLoader::SaveSRIHash(ScriptLoadRequest* aRequest, + SRICheckDataVerifier* aSRIDataVerifier, + uint32_t* sriLength) const { + MOZ_ASSERT(aRequest->IsSource()); + MOZ_ASSERT(aRequest->mScriptBytecode.empty()); + + uint32_t len; + + // If the integrity metadata does not correspond to a valid hash function, + // IsComplete would be false. + if (!aRequest->mIntegrity.IsEmpty() && aSRIDataVerifier->IsComplete()) { + MOZ_ASSERT(aRequest->mScriptBytecode.length() == 0); + + // Encode the SRI computed hash. + len = aSRIDataVerifier->DataSummaryLength(); + + if (!aRequest->mScriptBytecode.resize(len)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + DebugOnly<nsresult> res = aSRIDataVerifier->ExportDataSummary( + len, aRequest->mScriptBytecode.begin()); + MOZ_ASSERT(NS_SUCCEEDED(res)); + } else { + MOZ_ASSERT(aRequest->mScriptBytecode.length() == 0); + + // Encode a dummy SRI hash. + len = SRICheckDataVerifier::EmptyDataSummaryLength(); + + if (!aRequest->mScriptBytecode.resize(len)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + DebugOnly<nsresult> res = SRICheckDataVerifier::ExportEmptyDataSummary( + len, aRequest->mScriptBytecode.begin()); + MOZ_ASSERT(NS_SUCCEEDED(res)); + } + + // Verify that the exported and predicted length correspond. + mozilla::DebugOnly<uint32_t> srilen; + MOZ_ASSERT(NS_SUCCEEDED(SRICheckDataVerifier::DataSummaryLength( + len, aRequest->mScriptBytecode.begin(), &srilen))); + MOZ_ASSERT(srilen == len); + + *sriLength = len; + + return NS_OK; +} + +void ScriptLoader::ReportErrorToConsole(ScriptLoadRequest* aRequest, + nsresult aResult) const { + MOZ_ASSERT(aRequest); + + if (aRequest->IsPreload()) { + // Skip reporting errors in preload requests. If the request is actually + // used then we will report the error in ReportPreloadErrorsToConsole below. + aRequest->mUnreportedPreloadError = aResult; + return; + } + + bool isScript = !aRequest->IsModuleRequest(); + const char* message; + if (aResult == NS_ERROR_MALFORMED_URI) { + message = isScript ? "ScriptSourceMalformed" : "ModuleSourceMalformed"; + } else if (aResult == NS_ERROR_DOM_BAD_URI) { + message = isScript ? "ScriptSourceNotAllowed" : "ModuleSourceNotAllowed"; + } else if (net::UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode( + aResult)) { + // Blocking classifier error codes already show their own console messages. + return; + } else { + message = isScript ? "ScriptSourceLoadFailed" : "ModuleSourceLoadFailed"; + } + + AutoTArray<nsString, 1> params; + CopyUTF8toUTF16(aRequest->mURI->GetSpecOrDefault(), *params.AppendElement()); + + nsIScriptElement* element = aRequest->GetScriptElement(); + uint32_t lineNo = element ? element->GetScriptLineNumber() : 0; + uint32_t columnNo = element ? element->GetScriptColumnNumber() : 0; + + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + "Script Loader"_ns, mDocument, + nsContentUtils::eDOM_PROPERTIES, message, + params, nullptr, u""_ns, lineNo, columnNo); +} + +void ScriptLoader::ReportPreloadErrorsToConsole(ScriptLoadRequest* aRequest) { + if (NS_FAILED(aRequest->mUnreportedPreloadError)) { + ReportErrorToConsole(aRequest, aRequest->mUnreportedPreloadError); + aRequest->mUnreportedPreloadError = NS_OK; + } + + if (aRequest->IsModuleRequest()) { + for (auto childRequest : aRequest->AsModuleRequest()->mImports) { + ReportPreloadErrorsToConsole(childRequest); + } + } +} + +void ScriptLoader::HandleLoadError(ScriptLoadRequest* aRequest, + nsresult aResult) { + /* + * Handle script not loading error because source was an tracking URL (or + * fingerprinting, cryptomining, etc). + * We make a note of this script node by including it in a dedicated + * array of blocked tracking nodes under its parent document. + */ + if (net::UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode( + aResult)) { + nsCOMPtr<nsIContent> cont = do_QueryInterface(aRequest->GetScriptElement()); + mDocument->AddBlockedNodeByClassifier(cont); + } + + if (aRequest->IsModuleRequest() && !aRequest->mIsInline) { + auto request = aRequest->AsModuleRequest(); + SetModuleFetchFinishedAndResumeWaitingRequests(request, aResult); + } + + if (aRequest->mInDeferList) { + MOZ_ASSERT_IF(aRequest->IsModuleRequest(), + aRequest->AsModuleRequest()->IsTopLevel()); + if (aRequest->isInList()) { + RefPtr<ScriptLoadRequest> req = mDeferRequests.Steal(aRequest); + FireScriptAvailable(aResult, req); + } + } else if (aRequest->mInAsyncList) { + MOZ_ASSERT_IF(aRequest->IsModuleRequest(), + aRequest->AsModuleRequest()->IsTopLevel()); + if (aRequest->isInList()) { + RefPtr<ScriptLoadRequest> req = mLoadingAsyncRequests.Steal(aRequest); + FireScriptAvailable(aResult, req); + } + } else if (aRequest->mIsNonAsyncScriptInserted) { + if (aRequest->isInList()) { + RefPtr<ScriptLoadRequest> req = + mNonAsyncExternalScriptInsertedRequests.Steal(aRequest); + FireScriptAvailable(aResult, req); + } + } else if (aRequest->mIsXSLT) { + if (aRequest->isInList()) { + RefPtr<ScriptLoadRequest> req = mXSLTRequests.Steal(aRequest); + FireScriptAvailable(aResult, req); + } + } else if (aRequest->IsPreload()) { + if (aRequest->IsModuleRequest()) { + aRequest->Cancel(); + } + if (aRequest->IsTopLevel()) { + MOZ_ALWAYS_TRUE( + mPreloads.RemoveElement(aRequest, PreloadRequestComparator())); + } + MOZ_ASSERT(!aRequest->isInList()); + AccumulateCategorical(LABELS_DOM_SCRIPT_PRELOAD_RESULT::LoadError); + } else if (aRequest->IsModuleRequest()) { + ModuleLoadRequest* modReq = aRequest->AsModuleRequest(); + if (modReq->IsDynamicImport()) { + MOZ_ASSERT(modReq->IsTopLevel()); + if (aRequest->isInList()) { + RefPtr<ScriptLoadRequest> req = mDynamicImportRequests.Steal(aRequest); + modReq->Cancel(); + // FinishDynamicImport must happen exactly once for each dynamic import + // request. If the load is aborted we do it when we remove the request + // from mDynamicImportRequests. + FinishDynamicImportAndReject(modReq, aResult); + } + } else { + MOZ_ASSERT(!modReq->IsTopLevel()); + MOZ_ASSERT(!modReq->isInList()); + modReq->Cancel(); + // The error is handled for the top level module. + } + } else if (mParserBlockingRequest == aRequest) { + MOZ_ASSERT(!aRequest->isInList()); + mParserBlockingRequest = nullptr; + UnblockParser(aRequest); + + // Ensure that we treat aRequest->GetScriptElement() as our current + // parser-inserted script while firing onerror on it. + MOZ_ASSERT(aRequest->GetScriptElement()->GetParserCreated()); + nsCOMPtr<nsIScriptElement> oldParserInsertedScript = + mCurrentParserInsertedScript; + mCurrentParserInsertedScript = aRequest->GetScriptElement(); + FireScriptAvailable(aResult, aRequest); + ContinueParserAsync(aRequest); + mCurrentParserInsertedScript = oldParserInsertedScript; + } else { + // This happens for blocking requests cancelled by ParsingComplete(). + // Ignore cancellation status for link-preload requests, as cancellation can + // be omitted for them when SRI is stronger on consumer tags. + MOZ_ASSERT(aRequest->IsCanceled() || aRequest->IsLinkPreloadScript()); + MOZ_ASSERT(!aRequest->isInList()); + } +} + +void ScriptLoader::UnblockParser(ScriptLoadRequest* aParserBlockingRequest) { + aParserBlockingRequest->GetScriptElement()->UnblockParser(); +} + +void ScriptLoader::ContinueParserAsync( + ScriptLoadRequest* aParserBlockingRequest) { + aParserBlockingRequest->GetScriptElement()->ContinueParserAsync(); +} + +uint32_t ScriptLoader::NumberOfProcessors() { + if (mNumberOfProcessors > 0) return mNumberOfProcessors; + + int32_t numProcs = PR_GetNumberOfProcessors(); + if (numProcs > 0) mNumberOfProcessors = numProcs; + return mNumberOfProcessors; +} + +static bool IsInternalURIScheme(nsIURI* uri) { + return uri->SchemeIs("moz-extension") || uri->SchemeIs("resource") || + uri->SchemeIs("chrome"); +} + +bool ScriptLoader::ShouldCompileOffThread(ScriptLoadRequest* aRequest) { + if (NumberOfProcessors() <= 1) { + return false; + } + if (aRequest == mParserBlockingRequest) { + return true; + } + if (SpeculativeOMTParsingEnabled()) { + // Processing non async inserted scripts too early can potentially delay the + // load event from firing so focus on other scripts instead. + if (aRequest->mIsNonAsyncScriptInserted && + !StaticPrefs:: + dom_script_loader_external_scripts_speculate_non_parser_inserted_enabled()) { + return false; + } + + // Async and link preload scripts do not need to be parsed right away. + if (aRequest->IsAsyncScript() && + !StaticPrefs:: + dom_script_loader_external_scripts_speculate_async_enabled()) { + return false; + } + + if (aRequest->IsLinkPreloadScript() && + !StaticPrefs:: + dom_script_loader_external_scripts_speculate_link_preload_enabled()) { + return false; + } + + return true; + } + return false; +} + +nsresult ScriptLoader::PrepareLoadedRequest(ScriptLoadRequest* aRequest, + nsIIncrementalStreamLoader* aLoader, + nsresult aStatus) { + if (NS_FAILED(aStatus)) { + return aStatus; + } + + if (aRequest->IsCanceled()) { + return NS_BINDING_ABORTED; + } + MOZ_ASSERT(aRequest->IsLoading()); + CollectScriptTelemetry(aRequest); + + // If we don't have a document, then we need to abort further + // evaluation. + if (!mDocument) { + return NS_ERROR_NOT_AVAILABLE; + } + + // If the load returned an error page, then we need to abort + nsCOMPtr<nsIRequest> req; + nsresult rv = aLoader->GetRequest(getter_AddRefs(req)); + NS_ASSERTION(req, "StreamLoader's request went away prematurely"); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(req); + if (httpChannel) { + bool requestSucceeded; + rv = httpChannel->GetRequestSucceeded(&requestSucceeded); + if (NS_SUCCEEDED(rv) && !requestSucceeded) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsAutoCString sourceMapURL; + if (nsContentUtils::GetSourceMapURL(httpChannel, sourceMapURL)) { + aRequest->mSourceMapURL = Some(NS_ConvertUTF8toUTF16(sourceMapURL)); + } + + nsCOMPtr<nsIClassifiedChannel> classifiedChannel = do_QueryInterface(req); + MOZ_ASSERT(classifiedChannel); + if (classifiedChannel && + classifiedChannel->IsThirdPartyTrackingResource()) { + aRequest->SetIsTracking(); + } + } + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(req); + // If this load was subject to a CORS check, don't flag it with a separate + // origin principal, so that it will treat our document's principal as the + // origin principal. Module loads always use CORS. + if (!aRequest->IsModuleRequest() && aRequest->CORSMode() == CORS_NONE) { + rv = nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( + channel, getter_AddRefs(aRequest->mOriginPrincipal)); + NS_ENSURE_SUCCESS(rv, rv); + } + + // This assertion could fire errorously if we ran out of memory when + // inserting the request in the array. However it's an unlikely case + // so if you see this assertion it is likely something else that is + // wrong, especially if you see it more than once. + NS_ASSERTION(mDeferRequests.Contains(aRequest) || + mLoadingAsyncRequests.Contains(aRequest) || + mNonAsyncExternalScriptInsertedRequests.Contains(aRequest) || + mXSLTRequests.Contains(aRequest) || + mDynamicImportRequests.Contains(aRequest) || + (aRequest->IsModuleRequest() && + !aRequest->AsModuleRequest()->IsTopLevel() && + !aRequest->isInList()) || + mPreloads.Contains(aRequest, PreloadRequestComparator()) || + mParserBlockingRequest == aRequest, + "aRequest should be pending!"); + + nsCOMPtr<nsIURI> uri; + rv = channel->GetOriginalURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + // Fixup moz-extension: and resource: URIs, because the channel URI will + // point to file:, which won't be allowed to load. + if (uri && IsInternalURIScheme(uri)) { + aRequest->mBaseURL = uri; + } else { + channel->GetURI(getter_AddRefs(aRequest->mBaseURL)); + } + + if (aRequest->IsModuleRequest()) { + MOZ_ASSERT(aRequest->IsSource()); + ModuleLoadRequest* request = aRequest->AsModuleRequest(); + + // When loading a module, only responses with a JavaScript MIME type are + // acceptable. + nsAutoCString mimeType; + channel->GetContentType(mimeType); + NS_ConvertUTF8toUTF16 typeString(mimeType); + if (!nsContentUtils::IsJavascriptMIMEType(typeString)) { + return NS_ERROR_FAILURE; + } + + // Attempt to compile off main thread. + bool couldCompile = false; + rv = AttemptAsyncScriptCompile(request, &couldCompile); + NS_ENSURE_SUCCESS(rv, rv); + if (couldCompile) { + return NS_OK; + } + + // Otherwise compile it right away and start fetching descendents. + return ProcessFetchedModuleSource(request); + } + + // The script is now loaded and ready to run. + aRequest->SetReady(); + + // If speculative parsing is enabled attempt to compile all + // external scripts off-main-thread. Otherwise, only omt compile scripts + // blocking the parser. + if (ShouldCompileOffThread(aRequest)) { + MOZ_ASSERT(!aRequest->IsModuleRequest()); + bool couldCompile = false; + nsresult rv = AttemptAsyncScriptCompile(aRequest, &couldCompile); + NS_ENSURE_SUCCESS(rv, rv); + if (couldCompile) { + MOZ_ASSERT(aRequest->mProgress == ScriptLoadRequest::Progress::eCompiling, + "Request should be off-thread compiling now."); + return NS_OK; + } + + // If off-thread compile was rejected, continue with regular processing. + } + + MaybeMoveToLoadedList(aRequest); + + return NS_OK; +} + +void ScriptLoader::DeferCheckpointReached() { + if (mDeferEnabled) { + // Have to check because we apparently get ParsingComplete + // without BeginDeferringScripts in some cases + mDeferCheckpointReached = true; + } + + mDeferEnabled = false; + ProcessPendingRequests(); +} + +void ScriptLoader::ParsingComplete(bool aTerminated) { + if (!aTerminated) { + return; + } + mDeferRequests.Clear(); + mLoadingAsyncRequests.Clear(); + mLoadedAsyncRequests.Clear(); + mNonAsyncExternalScriptInsertedRequests.Clear(); + mXSLTRequests.Clear(); + + for (ScriptLoadRequest* req = mDynamicImportRequests.getFirst(); req; + req = req->getNext()) { + req->Cancel(); + // FinishDynamicImport must happen exactly once for each dynamic import + // request. If the load is aborted we do it when we remove the request + // from mDynamicImportRequests. + FinishDynamicImportAndReject(req->AsModuleRequest(), NS_ERROR_ABORT); + } + mDynamicImportRequests.Clear(); + + if (mParserBlockingRequest) { + mParserBlockingRequest->Cancel(); + mParserBlockingRequest = nullptr; + } + + // Cancel any unused scripts that were compiled speculatively + for (size_t i = 0; i < mPreloads.Length(); i++) { + mPreloads[i].mRequest->MaybeCancelOffThreadScript(); + } + + // Have to call this even if aTerminated so we'll correctly unblock + // onload and all. + DeferCheckpointReached(); +} + +void ScriptLoader::PreloadURI(nsIURI* aURI, const nsAString& aCharset, + const nsAString& aType, + const nsAString& aCrossOrigin, + const nsAString& aIntegrity, bool aScriptFromHead, + bool aAsync, bool aDefer, bool aNoModule, + bool aLinkPreload, + const ReferrerPolicy aReferrerPolicy) { + NS_ENSURE_TRUE_VOID(mDocument); + // Check to see if scripts has been turned off. + if (!mEnabled || !mDocument->IsScriptEnabled()) { + return; + } + + ScriptKind scriptKind = ScriptKind::eClassic; + + if (mDocument->ModuleScriptsEnabled()) { + // Don't load nomodule scripts. + if (aNoModule) { + return; + } + + static const char kASCIIWhitespace[] = "\t\n\f\r "; + + nsAutoString type(aType); + type.Trim(kASCIIWhitespace); + if (type.LowerCaseEqualsASCII("module")) { + scriptKind = ScriptKind::eModule; + } + } + + if (scriptKind == ScriptKind::eClassic && !aType.IsEmpty() && + !nsContentUtils::IsJavascriptMIMEType(aType)) { + // Unknown type. Don't load it. + return; + } + + SRIMetadata sriMetadata; + GetSRIMetadata(aIntegrity, &sriMetadata); + + RefPtr<ScriptLoadRequest> request = CreateLoadRequest( + scriptKind, aURI, nullptr, mDocument->NodePrincipal(), + Element::StringToCORSMode(aCrossOrigin), sriMetadata, aReferrerPolicy); + request->mIsInline = false; + request->mScriptFromHead = aScriptFromHead; + request->SetScriptMode(aDefer, aAsync, aLinkPreload); + request->SetIsPreloadRequest(); + + if (LOG_ENABLED()) { + nsAutoCString url; + aURI->GetAsciiSpec(url); + LOG(("ScriptLoadRequest (%p): Created preload request for %s", + request.get(), url.get())); + } + + nsresult rv = StartLoad(request); + if (NS_FAILED(rv)) { + return; + } + + PreloadInfo* pi = mPreloads.AppendElement(); + pi->mRequest = request; + pi->mCharset = aCharset; +} + +void ScriptLoader::AddDeferRequest(ScriptLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->IsDeferredScript()); + MOZ_ASSERT(!aRequest->mInDeferList && !aRequest->mInAsyncList); + + aRequest->mInDeferList = true; + mDeferRequests.AppendElement(aRequest); + if (mDeferEnabled && aRequest == mDeferRequests.getFirst() && mDocument && + !mBlockingDOMContentLoaded) { + MOZ_ASSERT(mDocument->GetReadyStateEnum() == Document::READYSTATE_LOADING); + mBlockingDOMContentLoaded = true; + mDocument->BlockDOMContentLoaded(); + } +} + +void ScriptLoader::AddAsyncRequest(ScriptLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->IsAsyncScript()); + MOZ_ASSERT(!aRequest->mInDeferList && !aRequest->mInAsyncList); + + aRequest->mInAsyncList = true; + if (aRequest->IsReadyToRun()) { + mLoadedAsyncRequests.AppendElement(aRequest); + } else { + mLoadingAsyncRequests.AppendElement(aRequest); + } +} + +void ScriptLoader::MaybeMoveToLoadedList(ScriptLoadRequest* aRequest) { + MOZ_ASSERT(aRequest->IsReadyToRun()); + + // If it's async, move it to the loaded list. aRequest->mInAsyncList really + // _should_ be in a list, but the consequences if it's not are bad enough we + // want to avoid trying to move it if it's not. + if (aRequest->mInAsyncList) { + MOZ_ASSERT(aRequest->isInList()); + if (aRequest->isInList()) { + RefPtr<ScriptLoadRequest> req = mLoadingAsyncRequests.Steal(aRequest); + mLoadedAsyncRequests.AppendElement(req); + } + } +} + +bool ScriptLoader::MaybeRemovedDeferRequests() { + if (mDeferRequests.isEmpty() && mDocument && mBlockingDOMContentLoaded) { + mBlockingDOMContentLoaded = false; + mDocument->UnblockDOMContentLoaded(); + return true; + } + return false; +} + +DocGroup* ScriptLoader::GetDocGroup() const { return mDocument->GetDocGroup(); } + +void ScriptLoader::BeginDeferringScripts() { + mDeferEnabled = true; + if (mDeferCheckpointReached) { + // We already completed a parse and were just waiting for some async + // scripts to load (and were already blocking the load event waiting for + // that to happen), when document.open() happened and now we're doing a + // new parse. We shouldn't block the load event again, but _should_ reset + // mDeferCheckpointReached to false. It'll get set to true again when the + // DeferCheckpointReached call that corresponds to this + // BeginDeferringScripts call happens (on document.close()), since we just + // set mDeferEnabled to true. + mDeferCheckpointReached = false; + } else { + if (mDocument) { + mDocument->BlockOnload(); + } + } +} + +nsAutoScriptLoaderDisabler::nsAutoScriptLoaderDisabler(Document* aDoc) { + mLoader = aDoc->ScriptLoader(); + mWasEnabled = mLoader->GetEnabled(); + if (mWasEnabled) { + mLoader->SetEnabled(false); + } +} + +nsAutoScriptLoaderDisabler::~nsAutoScriptLoaderDisabler() { + if (mWasEnabled) { + mLoader->SetEnabled(true); + } +} + +#undef TRACE_FOR_TEST +#undef TRACE_FOR_TEST_BOOL +#undef TRACE_FOR_TEST_NONE + +#undef LOG + +} // namespace dom +} // namespace mozilla diff --git a/dom/script/ScriptLoader.h b/dom/script/ScriptLoader.h new file mode 100644 index 0000000000..4228f7c758 --- /dev/null +++ b/dom/script/ScriptLoader.h @@ -0,0 +1,780 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_ScriptLoader_h +#define mozilla_dom_ScriptLoader_h + +#include "js/TypeDecls.h" +#include "nsCOMPtr.h" +#include "nsRefPtrHashtable.h" +#include "nsIScriptElement.h" +#include "nsCOMArray.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTArray.h" +#include "nsINode.h" +#include "nsIObserver.h" +#include "nsIScriptLoaderObserver.h" +#include "nsURIHashKey.h" +#include "mozilla/CORSMode.h" +#include "mozilla/dom/LoadedScript.h" +#include "mozilla/dom/ScriptLoadRequest.h" +#include "mozilla/MaybeOneOf.h" +#include "mozilla/MozPromise.h" +#include "ScriptKind.h" + +class nsCycleCollectionTraversalCallback; +class nsIChannel; +class nsIConsoleReportCollector; +class nsIContent; +class nsIIncrementalStreamLoader; +class nsIPrincipal; +class nsIScriptGlobalObject; +class nsIURI; + +namespace JS { + +class CompileOptions; + +template <typename UnitT> +class SourceText; + +} // namespace JS + +namespace mozilla { + +class LazyLogModule; +union Utf8Unit; + +namespace dom { + +class AutoJSAPI; +class DocGroup; +class Document; +class LoadedScript; +class ModuleLoadRequest; +class ModuleScript; +class SRICheckDataVerifier; +class SRIMetadata; +class ScriptLoadHandler; +class ScriptLoader; +class ScriptRequestProcessor; + +enum class ReferrerPolicy : uint8_t; + +class AsyncCompileShutdownObserver final : public nsIObserver { + ~AsyncCompileShutdownObserver() { Unregister(); } + + public: + explicit AsyncCompileShutdownObserver(ScriptLoader* aLoader) + : mScriptLoader(aLoader) {} + + void OnShutdown(); + void Unregister(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + private: + // Defined during registration in ScriptLoader constructor, and + // cleared during destructor, ScriptLoader::Destroy() or Shutdown. + ScriptLoader* mScriptLoader; +}; + +////////////////////////////////////////////////////////////// +// Script loader implementation +////////////////////////////////////////////////////////////// + +class ScriptLoader final : public nsISupports { + class MOZ_STACK_CLASS AutoCurrentScriptUpdater { + public: + AutoCurrentScriptUpdater(ScriptLoader* aScriptLoader, + nsIScriptElement* aCurrentScript) + : mOldScript(aScriptLoader->mCurrentScript), + mScriptLoader(aScriptLoader) { + nsCOMPtr<nsINode> node = do_QueryInterface(aCurrentScript); + mScriptLoader->mCurrentScript = + node && !node->IsInShadowTree() ? aCurrentScript : nullptr; + } + + ~AutoCurrentScriptUpdater() { + mScriptLoader->mCurrentScript.swap(mOldScript); + } + + private: + nsCOMPtr<nsIScriptElement> mOldScript; + ScriptLoader* mScriptLoader; + }; + + friend class ModuleLoadRequest; + friend class ScriptRequestProcessor; + friend class ScriptLoadHandler; + friend class AutoCurrentScriptUpdater; + + public: + explicit ScriptLoader(Document* aDocument); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(ScriptLoader) + + /** + * The loader maintains a weak reference to the document with + * which it is initialized. This call forces the reference to + * be dropped. + */ + void DropDocumentReference() { mDocument = nullptr; } + + /** + * Add an observer for all scripts loaded through this loader. + * + * @param aObserver observer for all script processing. + */ + nsresult AddObserver(nsIScriptLoaderObserver* aObserver) { + return mObservers.AppendObject(aObserver) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; + } + + /** + * Remove an observer. + * + * @param aObserver observer to be removed + */ + void RemoveObserver(nsIScriptLoaderObserver* aObserver) { + mObservers.RemoveObject(aObserver); + } + + /** + * Process a script element. This will include both loading the + * source of the element if it is not inline and evaluating + * the script itself. + * + * If the script is an inline script that can be executed immediately + * (i.e. there are no other scripts pending) then ScriptAvailable + * and ScriptEvaluated will be called before the function returns. + * + * If true is returned the script could not be executed immediately. + * In this case ScriptAvailable is guaranteed to be called at a later + * point (as well as possibly ScriptEvaluated). + * + * @param aElement The element representing the script to be loaded and + * evaluated. + */ + bool ProcessScriptElement(nsIScriptElement* aElement); + + /** + * Gets the currently executing script. This is useful if you want to + * generate a unique key based on the currently executing script. + */ + nsIScriptElement* GetCurrentScript() { return mCurrentScript; } + + nsIScriptElement* GetCurrentParserInsertedScript() { + return mCurrentParserInsertedScript; + } + + /** + * Whether the loader is enabled or not. + * When disabled, processing of new script elements is disabled. + * Any call to ProcessScriptElement() will return false. Note that + * this DOES NOT disable currently loading or executing scripts. + */ + bool GetEnabled() { return mEnabled; } + + void SetEnabled(bool aEnabled) { + if (!mEnabled && aEnabled) { + ProcessPendingRequestsAsync(); + } + mEnabled = aEnabled; + } + + /** + * Check whether to speculatively OMT parse scripts as soon as + * they are fetched, even if not a parser blocking request. + * Controlled by + * dom.script_loader.external_scripts.speculative_omt_parse_enabled + */ + bool SpeculativeOMTParsingEnabled() const { + return mSpeculativeOMTParsingEnabled; + } + + /** + * Add/remove a blocker for parser-blocking scripts (and XSLT + * scripts). Blockers will stop such scripts from executing, but not from + * loading. + */ + void AddParserBlockingScriptExecutionBlocker() { + ++mParserBlockingBlockerCount; + } + + void RemoveParserBlockingScriptExecutionBlocker() { + if (!--mParserBlockingBlockerCount && ReadyToExecuteScripts()) { + ProcessPendingRequestsAsync(); + } + } + + /** + * Add/remove a blocker for execution of all scripts. Blockers will stop + * scripts from executing, but not from loading. + */ + void AddExecuteBlocker() { ++mBlockerCount; } + + void RemoveExecuteBlocker() { + MOZ_ASSERT(mBlockerCount); + if (!--mBlockerCount) { + ProcessPendingRequestsAsync(); + } + } + + /** + * Convert the given buffer to a UTF-16 string. If the buffer begins with a + * BOM, it is interpreted as that encoding; otherwise the first of |aChannel|, + * |aHintCharset|, or |aDocument| that provides a recognized encoding is used, + * or Windows-1252 if none of them do. + * + * Encoding errors in the buffer are converted to replacement characters, so + * allocation failure is the only way this function can fail. + * + * @param aChannel Channel corresponding to the data. May be null. + * @param aData The data to convert + * @param aLength Length of the data + * @param aHintCharset Character set hint (e.g., from a charset attribute). + * @param aDocument Document which the data is loaded for. May be null. + * @param aBufOut [out] fresh char16_t array containing data converted to + * Unicode. Caller must js_free() this data when finished + * with it. + * @param aLengthOut [out] Length of array returned in aBufOut in number + * of char16_t code units. + */ + static nsresult ConvertToUTF16(nsIChannel* aChannel, const uint8_t* aData, + uint32_t aLength, + const nsAString& aHintCharset, + Document* aDocument, char16_t*& aBufOut, + size_t& aLengthOut); + + static inline nsresult ConvertToUTF16(nsIChannel* aChannel, + const uint8_t* aData, uint32_t aLength, + const nsAString& aHintCharset, + Document* aDocument, + JS::UniqueTwoByteChars& aBufOut, + size_t& aLengthOut) { + char16_t* bufOut; + nsresult rv = ConvertToUTF16(aChannel, aData, aLength, aHintCharset, + aDocument, bufOut, aLengthOut); + if (NS_SUCCEEDED(rv)) { + aBufOut.reset(bufOut); + } + return rv; + }; + + /** + * Convert the given buffer to a UTF-8 string. If the buffer begins with a + * BOM, it is interpreted as that encoding; otherwise the first of |aChannel|, + * |aHintCharset|, or |aDocument| that provides a recognized encoding is used, + * or Windows-1252 if none of them do. + * + * Encoding errors in the buffer are converted to replacement characters, so + * allocation failure is the only way this function can fail. + * + * @param aChannel Channel corresponding to the data. May be null. + * @param aData The data to convert + * @param aLength Length of the data + * @param aHintCharset Character set hint (e.g., from a charset attribute). + * @param aDocument Document which the data is loaded for. May be null. + * @param aBufOut [out] fresh Utf8Unit array containing data converted to + * Unicode. Caller must js_free() this data when finished + * with it. + * @param aLengthOut [out] Length of array returned in aBufOut in UTF-8 code + * units (i.e. in bytes). + */ + static nsresult ConvertToUTF8(nsIChannel* aChannel, const uint8_t* aData, + uint32_t aLength, const nsAString& aHintCharset, + Document* aDocument, Utf8Unit*& aBufOut, + size_t& aLengthOut); + + /** + * Handle the completion of a stream. This is called by the + * ScriptLoadHandler object which observes the IncrementalStreamLoader + * loading the script. The streamed content is expected to be stored on the + * aRequest argument. + */ + nsresult OnStreamComplete(nsIIncrementalStreamLoader* aLoader, + ScriptLoadRequest* aRequest, + nsresult aChannelStatus, nsresult aSRIStatus, + SRICheckDataVerifier* aSRIDataVerifier); + + /** + * Returns wether any request is queued, and not executed yet. + */ + bool HasPendingRequests(); + + /** + * Processes any pending requests that are ready for processing. + */ + void ProcessPendingRequests(); + + /** + * Starts deferring deferred scripts and puts them in the mDeferredRequests + * queue instead. + */ + void BeginDeferringScripts(); + + /** + * Notifies the script loader that parsing is done. If aTerminated is true, + * this will drop any pending scripts that haven't run yet, otherwise it will + * do nothing. + */ + void ParsingComplete(bool aTerminated); + + /** + * Notifies the script loader that the checkpoint to begin execution of defer + * scripts has been reached. This is either the end of of the document parse + * or the end of loading of parser-inserted stylesheets, whatever happens + * last. + * + * Otherwise, it will stop deferring scripts and immediately processes the + * mDeferredRequests queue. + * + * WARNING: This function will synchronously execute content scripts, so be + * prepared that the world might change around you. + */ + void DeferCheckpointReached(); + + /** + * Returns the number of pending scripts, deferred or not. + */ + uint32_t HasPendingOrCurrentScripts() { + return mCurrentScript || mParserBlockingRequest; + } + + /** + * Adds aURI to the preload list and starts loading it. + * + * @param aURI The URI of the external script. + * @param aCharset The charset parameter for the script. + * @param aType The type parameter for the script. + * @param aCrossOrigin The crossorigin attribute for the script. + * Void if not present. + * @param aIntegrity The expect hash url, if avail, of the request + * @param aScriptFromHead Whether or not the script was a child of head + */ + virtual void PreloadURI(nsIURI* aURI, const nsAString& aCharset, + const nsAString& aType, const nsAString& aCrossOrigin, + const nsAString& aIntegrity, bool aScriptFromHead, + bool aAsync, bool aDefer, bool aNoModule, + bool aLinkPreload, + const ReferrerPolicy aReferrerPolicy); + + /** + * Process a request that was deferred so that the script could be compiled + * off thread. + */ + nsresult ProcessOffThreadRequest(ScriptLoadRequest* aRequest); + + bool AddPendingChildLoader(ScriptLoader* aChild) { + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. Else, change the return type to void. + mPendingChildLoaders.AppendElement(aChild); + return true; + } + + mozilla::dom::DocGroup* GetDocGroup() const; + + /** + * Register the fact that we saw the load event, and that we need to save the + * bytecode at the next loop cycle unless new scripts are waiting in the + * pipeline. + */ + void LoadEventFired(); + + /** + * Destroy and prevent the ScriptLoader or the ScriptLoadRequests from owning + * any references to the JSScript or to the Request which might be used for + * caching the encoded bytecode. + */ + void Destroy(); + + /** + * Implement the HostResolveImportedModule abstract operation. + * + * Resolve a module specifier string and look this up in the module + * map, returning the result. This is only called for previously + * loaded modules and always succeeds. + * + * @param aReferencingPrivate A JS::Value which is either undefined + * or contains a LoadedScript private pointer. + * @param aSpecifier The module specifier. + * @param aModuleOut This is set to the module found. + */ + static void ResolveImportedModule(JSContext* aCx, + JS::Handle<JS::Value> aReferencingPrivate, + JS::Handle<JSString*> aSpecifier, + JS::MutableHandle<JSObject*> aModuleOut); + + void StartDynamicImport(ModuleLoadRequest* aRequest); + + /** + * Shorthand Wrapper for JSAPI FinishDynamicImport function for the reject + * case where we do not have `aEvaluationPromise`. As there is no evaluation + * Promise, JS::FinishDynamicImport will always reject. + * + * @param aRequest + * The module load request for the dynamic module. + * @param aResult + * The result of running ModuleEvaluate -- If this is successful, then + * we can await the associated EvaluationPromise. + */ + void FinishDynamicImportAndReject(ModuleLoadRequest* aRequest, + nsresult aResult); + + /** + * Wrapper for JSAPI FinishDynamicImport function. Takes an optional argument + * `aEvaluationPromise` which, if null, exits early. + * + * This is the non-tla version, which works with modules which return + * completion records. + * + * @param aCX + * The JSContext for the module. + * @param aRequest + * The module load request for the dynamic module. + * @param aResult + * The result of running ModuleEvaluate + */ + void FinishDynamicImport_NoTLA(JSContext* aCx, ModuleLoadRequest* aRequest, + nsresult aResult); + + /** + * Wrapper for JSAPI FinishDynamicImport function. Takes an optional argument + * `aEvaluationPromise` which, if null, exits early. + * + * This is the Top Level Await version, which works with modules which return + * promises. + * + * @param aCX + * The JSContext for the module. + * @param aRequest + * The module load request for the dynamic module. + * @param aResult + * The result of running ModuleEvaluate -- If this is successful, then + * we can await the associated EvaluationPromise. + * @param aEvaluationPromise + * The evaluation promise returned from evaluating the module. If this + * is null, JS::FinishDynamicImport will reject the dynamic import + * module promise. + */ + void FinishDynamicImport(JSContext* aCx, ModuleLoadRequest* aRequest, + nsresult aResult, + JS::Handle<JSObject*> aEvaluationPromise); + + /* + * Get the currently active script. This is used as the initiating script when + * executing timeout handler scripts. + */ + static LoadedScript* GetActiveScript(JSContext* aCx); + + Document* GetDocument() const { return mDocument; } + + /** + * Called by shutdown observer. + */ + void Shutdown(); + + private: + virtual ~ScriptLoader(); + + void EnsureModuleHooksInitialized(); + + ScriptLoadRequest* CreateLoadRequest(ScriptKind aKind, nsIURI* aURI, + nsIScriptElement* aElement, + nsIPrincipal* aTriggeringPrincipal, + mozilla::CORSMode aCORSMode, + const SRIMetadata& aIntegrity, + ReferrerPolicy aReferrerPolicy); + + /** + * Unblocks the creator parser of the parser-blocking scripts. + */ + void UnblockParser(ScriptLoadRequest* aParserBlockingRequest); + + /** + * Asynchronously resumes the creator parser of the parser-blocking scripts. + */ + void ContinueParserAsync(ScriptLoadRequest* aParserBlockingRequest); + + bool ProcessExternalScript(nsIScriptElement* aElement, ScriptKind aScriptKind, + nsAutoString aTypeAttr, + nsIContent* aScriptContent); + + bool ProcessInlineScript(nsIScriptElement* aElement, ScriptKind aScriptKind); + + ScriptLoadRequest* LookupPreloadRequest(nsIScriptElement* aElement, + ScriptKind aScriptKind, + const SRIMetadata& aSRIMetadata); + + void GetSRIMetadata(const nsAString& aIntegrityAttr, + SRIMetadata* aMetadataOut); + + /** + * Given a script element, get the referrer policy should be applied to load + * requests. + */ + ReferrerPolicy GetReferrerPolicy(nsIScriptElement* aElement); + + /** + * Helper function to check the content policy for a given request. + */ + static nsresult CheckContentPolicy(Document* aDocument, nsISupports* aContext, + const nsAString& aType, + ScriptLoadRequest* aRequest); + + /** + * Helper function to determine whether an about: page loads a chrome: URI. + * Please note that this function only returns true if: + * * the about: page uses a ContentPrincipal with scheme about: + * * the about: page is not linkable from content + * (e.g. the function will return false for about:blank or about:srcdoc) + */ + static bool IsAboutPageLoadingChromeURI(ScriptLoadRequest* aRequest, + Document* aDocument); + + /** + * Start a load for aRequest's URI. + */ + nsresult StartLoad(ScriptLoadRequest* aRequest); + + /** + * Abort the current stream, and re-start with a new load request from scratch + * without requesting any alternate data. Returns NS_BINDING_RETARGETED on + * success, as this error code is used to abort the input stream. + */ + nsresult RestartLoad(ScriptLoadRequest* aRequest); + + void HandleLoadError(ScriptLoadRequest* aRequest, nsresult aResult); + + /** + * Process any pending requests asynchronously (i.e. off an event) if there + * are any. Note that this is a no-op if there aren't any currently pending + * requests. + * + * This function is virtual to allow cross-library calls to SetEnabled() + */ + virtual void ProcessPendingRequestsAsync(); + + /** + * If true, the loader is ready to execute parser-blocking scripts, and so are + * all its ancestors. If the loader itself is ready but some ancestor is not, + * this function will add an execute blocker and ask the ancestor to remove it + * once it becomes ready. + */ + bool ReadyToExecuteParserBlockingScripts(); + + /** + * Return whether just this loader is ready to execute parser-blocking + * scripts. + */ + bool SelfReadyToExecuteParserBlockingScripts() { + return ReadyToExecuteScripts() && !mParserBlockingBlockerCount; + } + + /** + * Return whether this loader is ready to execute scripts in general. + */ + bool ReadyToExecuteScripts() { return mEnabled && !mBlockerCount; } + + nsresult VerifySRI(ScriptLoadRequest* aRequest, + nsIIncrementalStreamLoader* aLoader, nsresult aSRIStatus, + SRICheckDataVerifier* aSRIDataVerifier) const; + + nsresult SaveSRIHash(ScriptLoadRequest* aRequest, + SRICheckDataVerifier* aSRIDataVerifier, + uint32_t* sriLength) const; + + void ReportErrorToConsole(ScriptLoadRequest* aRequest, + nsresult aResult) const; + void ReportPreloadErrorsToConsole(ScriptLoadRequest* aRequest); + + nsresult AttemptAsyncScriptCompile(ScriptLoadRequest* aRequest, + bool* aCouldCompileOut); + nsresult ProcessRequest(ScriptLoadRequest* aRequest); + void ProcessDynamicImport(ModuleLoadRequest* aRequest); + nsresult CompileOffThreadOrProcessRequest(ScriptLoadRequest* aRequest); + void FireScriptAvailable(nsresult aResult, ScriptLoadRequest* aRequest); + void FireScriptEvaluated(nsresult aResult, ScriptLoadRequest* aRequest); + nsresult EvaluateScript(ScriptLoadRequest* aRequest); + + /** + * Queue the current script load request to be saved, when the page + * initialization ends. The page initialization end is defined as being the + * time when the load event got received, and when no more scripts are waiting + * to be executed. + */ + void RegisterForBytecodeEncoding(ScriptLoadRequest* aRequest); + + /** + * Check if all conditions are met, i-e that the onLoad event fired and that + * no more script have to be processed. If all conditions are met, queue an + * event to encode all the bytecode and save them on the cache. + */ + void MaybeTriggerBytecodeEncoding(); + + /** + * Iterate over all script load request and save the bytecode of executed + * functions on the cache provided by the channel. + */ + void EncodeBytecode(); + void EncodeRequestBytecode(JSContext* aCx, ScriptLoadRequest* aRequest); + + void GiveUpBytecodeEncoding(); + + already_AddRefed<nsIScriptGlobalObject> GetScriptGlobalObject(); + nsresult FillCompileOptionsForRequest(const mozilla::dom::AutoJSAPI& jsapi, + ScriptLoadRequest* aRequest, + JS::Handle<JSObject*> aScopeChain, + JS::CompileOptions* aOptions); + + uint32_t NumberOfProcessors(); + nsresult PrepareLoadedRequest(ScriptLoadRequest* aRequest, + nsIIncrementalStreamLoader* aLoader, + nsresult aStatus); + + void AddDeferRequest(ScriptLoadRequest* aRequest); + void AddAsyncRequest(ScriptLoadRequest* aRequest); + bool MaybeRemovedDeferRequests(); + + bool ShouldCompileOffThread(ScriptLoadRequest* aRequest); + + void MaybeMoveToLoadedList(ScriptLoadRequest* aRequest); + + using MaybeSourceText = + mozilla::MaybeOneOf<JS::SourceText<char16_t>, JS::SourceText<Utf8Unit>>; + + // Get source text. On success |aMaybeSource| will contain either UTF-8 or + // UTF-16 source; on failure it will remain in its initial state. + MOZ_MUST_USE nsresult GetScriptSource(JSContext* aCx, + ScriptLoadRequest* aRequest, + MaybeSourceText* aMaybeSource); + + void SetModuleFetchStarted(ModuleLoadRequest* aRequest); + void SetModuleFetchFinishedAndResumeWaitingRequests( + ModuleLoadRequest* aRequest, nsresult aResult); + + bool IsFetchingModule(ModuleLoadRequest* aRequest) const; + + bool ModuleMapContainsURL(nsIURI* aURL) const; + RefPtr<mozilla::GenericNonExclusivePromise> WaitForModuleFetch(nsIURI* aURL); + ModuleScript* GetFetchedModule(nsIURI* aURL) const; + + friend JSObject* HostResolveImportedModule( + JSContext* aCx, JS::Handle<JS::Value> aReferencingPrivate, + JS::Handle<JSString*> aSpecifier); + + // Returns wether we should save the bytecode of this script after the + // execution of the script. + static bool ShouldCacheBytecode(ScriptLoadRequest* aRequest); + + nsresult CreateModuleScript(ModuleLoadRequest* aRequest); + nsresult ProcessFetchedModuleSource(ModuleLoadRequest* aRequest); + void CheckModuleDependenciesLoaded(ModuleLoadRequest* aRequest); + void ProcessLoadedModuleTree(ModuleLoadRequest* aRequest); + bool InstantiateModuleTree(ModuleLoadRequest* aRequest); + JS::Value FindFirstParseError(ModuleLoadRequest* aRequest); + void StartFetchingModuleDependencies(ModuleLoadRequest* aRequest); + + RefPtr<mozilla::GenericPromise> StartFetchingModuleAndDependencies( + ModuleLoadRequest* aParent, nsIURI* aURI); + + nsresult InitDebuggerDataForModuleTree(JSContext* aCx, + ModuleLoadRequest* aRequest); + + void RunScriptWhenSafe(ScriptLoadRequest* aRequest); + + /** + * Wait for any unused off thread compilations to finish and then + * cancel them. + */ + void CancelScriptLoadRequests(); + + Document* mDocument; // [WEAK] + nsCOMArray<nsIScriptLoaderObserver> mObservers; + ScriptLoadRequestList mNonAsyncExternalScriptInsertedRequests; + // mLoadingAsyncRequests holds async requests while they're loading; when they + // have been loaded they are moved to mLoadedAsyncRequests. + ScriptLoadRequestList mLoadingAsyncRequests; + ScriptLoadRequestList mLoadedAsyncRequests; + ScriptLoadRequestList mDeferRequests; + ScriptLoadRequestList mXSLTRequests; + ScriptLoadRequestList mDynamicImportRequests; + RefPtr<ScriptLoadRequest> mParserBlockingRequest; + + // List of script load request that are holding a buffer which has to be saved + // on the cache. + ScriptLoadRequestList mBytecodeEncodingQueue; + + // In mRequests, the additional information here is stored by the element. + struct PreloadInfo { + RefPtr<ScriptLoadRequest> mRequest; + nsString mCharset; + }; + + friend void ImplCycleCollectionUnlink(ScriptLoader::PreloadInfo& aField); + friend void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, + ScriptLoader::PreloadInfo& aField, const char* aName, uint32_t aFlags); + + struct PreloadRequestComparator { + bool Equals(const PreloadInfo& aPi, + ScriptLoadRequest* const& aRequest) const { + return aRequest == aPi.mRequest; + } + }; + + struct PreloadURIComparator { + bool Equals(const PreloadInfo& aPi, nsIURI* const& aURI) const; + }; + + nsTArray<PreloadInfo> mPreloads; + + nsCOMPtr<nsIScriptElement> mCurrentScript; + nsCOMPtr<nsIScriptElement> mCurrentParserInsertedScript; + nsTArray<RefPtr<ScriptLoader>> mPendingChildLoaders; + uint32_t mParserBlockingBlockerCount; + uint32_t mBlockerCount; + uint32_t mNumberOfProcessors; + bool mEnabled; + bool mDeferEnabled; + bool mSpeculativeOMTParsingEnabled; + bool mDeferCheckpointReached; + bool mBlockingDOMContentLoaded; + bool mLoadEventFired; + bool mGiveUpEncoding; + + // Module map + nsRefPtrHashtable<nsURIHashKey, mozilla::GenericNonExclusivePromise::Private> + mFetchingModules; + nsRefPtrHashtable<nsURIHashKey, ModuleScript> mFetchedModules; + + nsCOMPtr<nsIConsoleReportCollector> mReporter; + + // ShutdownObserver for off thread compilations + RefPtr<AsyncCompileShutdownObserver> mShutdownObserver; + + // Logging + public: + static LazyLogModule gCspPRLog; + static LazyLogModule gScriptLoaderLog; +}; + +class nsAutoScriptLoaderDisabler { + public: + explicit nsAutoScriptLoaderDisabler(Document* aDoc); + + ~nsAutoScriptLoaderDisabler(); + + bool mWasEnabled; + RefPtr<ScriptLoader> mLoader; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ScriptLoader_h diff --git a/dom/script/ScriptSettings.cpp b/dom/script/ScriptSettings.cpp new file mode 100644 index 0000000000..589b46999c --- /dev/null +++ b/dom/script/ScriptSettings.cpp @@ -0,0 +1,841 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "mozilla/dom/ScriptSettings.h" +#include "LoadedScript.h" +#include "mozilla/Assertions.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/ThreadLocal.h" +#include "mozilla/Maybe.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/JSExecutionManager.h" +#include "mozilla/dom/WorkerPrivate.h" + +#include "jsapi.h" +#include "js/CompilationAndEvaluation.h" +#include "js/friend/ErrorMessages.h" // JSMSG_OUT_OF_MEMORY +#include "js/Warnings.h" // JS::{Get,}WarningReporter +#include "xpcpublic.h" +#include "nsIGlobalObject.h" +#include "nsIDocShell.h" +#include "nsIScriptGlobalObject.h" +#include "nsIScriptContext.h" +#include "nsContentUtils.h" +#include "nsGlobalWindow.h" +#include "nsPIDOMWindow.h" +#include "nsTArray.h" +#include "nsJSUtils.h" +#include "nsDOMJSUtils.h" + +namespace mozilla { +namespace dom { + +JSObject* GetElementCallback(JSContext* aCx, JS::HandleValue aValue) { + JS::RootedValue privateValue(aCx, aValue); + MOZ_ASSERT(!privateValue.isObjectOrNull() && !privateValue.isUndefined()); + LoadedScript* script = static_cast<LoadedScript*>(privateValue.toPrivate()); + + if (!script->GetFetchOptions()) { + return nullptr; + } + + JS::Rooted<JS::Value> elementValue(aCx); + { + nsCOMPtr<Element> domElement = script->GetFetchOptions()->mElement; + if (!domElement) { + return nullptr; + } + + JSObject* globalObject = + domElement->OwnerDoc()->GetScopeObject()->GetGlobalJSObject(); + JSAutoRealm ar(aCx, globalObject); + + nsresult rv = nsContentUtils::WrapNative(aCx, domElement, &elementValue, + /* aAllowWrapping = */ true); + if (NS_FAILED(rv)) { + return nullptr; + } + } + return elementValue.toObjectOrNull(); +} + +static MOZ_THREAD_LOCAL(ScriptSettingsStackEntry*) sScriptSettingsTLS; + +// Assert if it's not safe to run script. The helper class +// AutoAllowLegacyScriptExecution allows to allow-list +// legacy cases where it's actually not safe to run script. +#ifdef DEBUG +static void AssertIfNotSafeToRunScript() { + // if it's safe to run script, then there is nothing to do here. + if (nsContentUtils::IsSafeToRunScript()) { + return; + } + + // auto allowing legacy script execution is fine for now. + if (AutoAllowLegacyScriptExecution::IsAllowed()) { + return; + } + + MOZ_ASSERT(false, "is it safe to run script?"); +} +#endif + +class ScriptSettingsStack { + public: + static ScriptSettingsStackEntry* Top() { return sScriptSettingsTLS.get(); } + + static void Push(ScriptSettingsStackEntry* aEntry) { + MOZ_ASSERT(!aEntry->mOlder); + // Whenever JSAPI use is disabled, the next stack entry pushed must + // not be an AutoIncumbentScript. + MOZ_ASSERT_IF(!Top() || Top()->NoJSAPI(), !aEntry->IsIncumbentScript()); + // Whenever the top entry is not an incumbent canidate, the next stack entry + // pushed must not be an AutoIncumbentScript. + MOZ_ASSERT_IF(Top() && !Top()->IsIncumbentCandidate(), + !aEntry->IsIncumbentScript()); + + aEntry->mOlder = Top(); + sScriptSettingsTLS.set(aEntry); + } + + static void Pop(ScriptSettingsStackEntry* aEntry) { + MOZ_ASSERT(aEntry == Top()); + sScriptSettingsTLS.set(aEntry->mOlder); + } + + static nsIGlobalObject* IncumbentGlobal() { + ScriptSettingsStackEntry* entry = Top(); + while (entry) { + if (entry->IsIncumbentCandidate()) { + return entry->mGlobalObject; + } + entry = entry->mOlder; + } + return nullptr; + } + + static ScriptSettingsStackEntry* EntryPoint() { + ScriptSettingsStackEntry* entry = Top(); + while (entry) { + if (entry->IsEntryCandidate()) { + return entry; + } + entry = entry->mOlder; + } + return nullptr; + } + + static nsIGlobalObject* EntryGlobal() { + ScriptSettingsStackEntry* entry = EntryPoint(); + if (!entry) { + return nullptr; + } + return entry->mGlobalObject; + } + +#ifdef DEBUG + static ScriptSettingsStackEntry* TopNonIncumbentScript() { + ScriptSettingsStackEntry* entry = Top(); + while (entry) { + if (!entry->IsIncumbentScript()) { + return entry; + } + entry = entry->mOlder; + } + return nullptr; + } +#endif // DEBUG +}; + +static unsigned long gRunToCompletionListeners = 0; + +void UseEntryScriptProfiling() { + MOZ_ASSERT(NS_IsMainThread()); + ++gRunToCompletionListeners; +} + +void UnuseEntryScriptProfiling() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(gRunToCompletionListeners > 0); + --gRunToCompletionListeners; +} + +void InitScriptSettings() { + bool success = sScriptSettingsTLS.init(); + if (!success) { + MOZ_CRASH(); + } + + sScriptSettingsTLS.set(nullptr); +} + +void DestroyScriptSettings() { + MOZ_ASSERT(sScriptSettingsTLS.get() == nullptr); +} + +ScriptSettingsStackEntry::ScriptSettingsStackEntry(nsIGlobalObject* aGlobal, + Type aType) + : mGlobalObject(aGlobal), mType(aType), mOlder(nullptr) { + MOZ_ASSERT_IF(IsIncumbentCandidate() && !NoJSAPI(), mGlobalObject); + MOZ_ASSERT(!mGlobalObject || mGlobalObject->HasJSGlobal(), + "Must have an actual JS global for the duration on the stack"); + MOZ_ASSERT( + !mGlobalObject || + JS_IsGlobalObject(mGlobalObject->GetGlobalJSObjectPreserveColor()), + "No outer windows allowed"); +} + +ScriptSettingsStackEntry::~ScriptSettingsStackEntry() { + // We must have an actual JS global for the entire time this is on the stack. + MOZ_ASSERT_IF(mGlobalObject, mGlobalObject->HasJSGlobal()); +} + +// If the entry or incumbent global ends up being something that the subject +// principal doesn't subsume, we don't want to use it. This never happens on +// the web, but can happen with asymmetric privilege relationships (i.e. +// ExpandedPrincipal and System Principal). +// +// The most correct thing to use instead would be the topmost global on the +// callstack whose principal is subsumed by the subject principal. But that's +// hard to compute, so we just substitute the global of the current +// compartment. In practice, this is fine. +// +// Note that in particular things like: +// +// |SpecialPowers.wrap(crossOriginWindow).eval(open())| +// +// trigger this case. Although both the entry global and the current global +// have normal principals, the use of Gecko-specific System-Principaled JS +// puts the code from two different origins on the callstack at once, which +// doesn't happen normally on the web. +static nsIGlobalObject* ClampToSubject(nsIGlobalObject* aGlobalOrNull) { + if (!aGlobalOrNull || !NS_IsMainThread()) { + return aGlobalOrNull; + } + + nsIPrincipal* globalPrin = aGlobalOrNull->PrincipalOrNull(); + NS_ENSURE_TRUE(globalPrin, GetCurrentGlobal()); + if (!nsContentUtils::SubjectPrincipalOrSystemIfNativeCaller() + ->SubsumesConsideringDomain(globalPrin)) { + return GetCurrentGlobal(); + } + + return aGlobalOrNull; +} + +nsIGlobalObject* GetEntryGlobal() { + return ClampToSubject(ScriptSettingsStack::EntryGlobal()); +} + +Document* GetEntryDocument() { + nsIGlobalObject* global = GetEntryGlobal(); + nsCOMPtr<nsPIDOMWindowInner> entryWin = do_QueryInterface(global); + + return entryWin ? entryWin->GetExtantDoc() : nullptr; +} + +nsIGlobalObject* GetIncumbentGlobal() { + // We need the current JSContext in order to check the JS for + // scripted frames that may have appeared since anyone last + // manipulated the stack. If it's null, that means that there + // must be no entry global on the stack, and therefore no incumbent + // global either. + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (!cx) { + MOZ_ASSERT(ScriptSettingsStack::EntryGlobal() == nullptr); + return nullptr; + } + + // See what the JS engine has to say. If we've got a scripted caller + // override in place, the JS engine will lie to us and pretend that + // there's nothing on the JS stack, which will cause us to check the + // incumbent script stack below. + if (JSObject* global = JS::GetScriptedCallerGlobal(cx)) { + return ClampToSubject(xpc::NativeGlobal(global)); + } + + // Ok, nothing from the JS engine. Let's use whatever's on the + // explicit stack. + return ClampToSubject(ScriptSettingsStack::IncumbentGlobal()); +} + +nsIGlobalObject* GetCurrentGlobal() { + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (!cx) { + return nullptr; + } + + JSObject* global = JS::CurrentGlobalOrNull(cx); + if (!global) { + return nullptr; + } + + return xpc::NativeGlobal(global); +} + +nsIPrincipal* GetWebIDLCallerPrincipal() { + MOZ_ASSERT(NS_IsMainThread()); + ScriptSettingsStackEntry* entry = ScriptSettingsStack::EntryPoint(); + + // If we have an entry point that is not NoJSAPI, we know it must be an + // AutoEntryScript. + if (!entry || entry->NoJSAPI()) { + return nullptr; + } + AutoEntryScript* aes = static_cast<AutoEntryScript*>(entry); + + return aes->mWebIDLCallerPrincipal; +} + +bool IsJSAPIActive() { + ScriptSettingsStackEntry* topEntry = ScriptSettingsStack::Top(); + return topEntry && !topEntry->NoJSAPI(); +} + +namespace danger { +JSContext* GetJSContext() { return CycleCollectedJSContext::Get()->Context(); } +} // namespace danger + +JS::RootingContext* RootingCx() { + return CycleCollectedJSContext::Get()->RootingCx(); +} + +AutoJSAPI::AutoJSAPI() + : ScriptSettingsStackEntry(nullptr, eJSAPI), + mCx(nullptr), + mIsMainThread(false) // For lack of anything better +{} + +AutoJSAPI::~AutoJSAPI() { + if (!mCx) { + // No need to do anything here: we never managed to Init, so can't have an + // exception on our (nonexistent) JSContext. We also don't need to restore + // any state on it. Finally, we never made it to pushing outselves onto the + // ScriptSettingsStack, so shouldn't pop. + MOZ_ASSERT(ScriptSettingsStack::Top() != this); + return; + } + + ReportException(); + + if (mOldWarningReporter.isSome()) { + JS::SetWarningReporter(cx(), mOldWarningReporter.value()); + } + + ScriptSettingsStack::Pop(this); +} + +void WarningOnlyErrorReporter(JSContext* aCx, JSErrorReport* aRep); + +void AutoJSAPI::InitInternal(nsIGlobalObject* aGlobalObject, JSObject* aGlobal, + JSContext* aCx, bool aIsMainThread) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aCx == danger::GetJSContext()); + MOZ_ASSERT(aIsMainThread == NS_IsMainThread()); + MOZ_ASSERT(bool(aGlobalObject) == bool(aGlobal)); + MOZ_ASSERT_IF(aGlobalObject, + aGlobalObject->GetGlobalJSObjectPreserveColor() == aGlobal); +#ifdef DEBUG + bool haveException = JS_IsExceptionPending(aCx); +#endif // DEBUG + + mCx = aCx; + mIsMainThread = aIsMainThread; + if (aGlobal) { + JS::AssertObjectIsNotGray(aGlobal); + } + mAutoNullableRealm.emplace(mCx, aGlobal); + mGlobalObject = aGlobalObject; + + ScriptSettingsStack::Push(this); + + mOldWarningReporter.emplace(JS::GetWarningReporter(aCx)); + + JS::SetWarningReporter(aCx, WarningOnlyErrorReporter); + JS::SetGetElementCallback(aCx, &GetElementCallback); + +#ifdef DEBUG + if (haveException) { + JS::Rooted<JS::Value> exn(aCx); + JS_GetPendingException(aCx, &exn); + + JS_ClearPendingException(aCx); + if (exn.isObject()) { + JS::Rooted<JSObject*> exnObj(aCx, &exn.toObject()); + + // Make sure we can actually read things from it. This UncheckedUwrap is + // safe because we're only getting data for a debug printf. In + // particular, we do not expose this data to anyone, which is very + // important; otherwise it could be a cross-origin information leak. + exnObj = js::UncheckedUnwrap(exnObj); + JSAutoRealm ar(aCx, exnObj); + + nsAutoJSString stack, filename, name, message; + int32_t line; + + JS::Rooted<JS::Value> tmp(aCx); + if (!JS_GetProperty(aCx, exnObj, "filename", &tmp)) { + JS_ClearPendingException(aCx); + } + if (tmp.isUndefined()) { + if (!JS_GetProperty(aCx, exnObj, "fileName", &tmp)) { + JS_ClearPendingException(aCx); + } + } + + if (!filename.init(aCx, tmp)) { + JS_ClearPendingException(aCx); + } + + if (!JS_GetProperty(aCx, exnObj, "stack", &tmp) || + !stack.init(aCx, tmp)) { + JS_ClearPendingException(aCx); + } + + if (!JS_GetProperty(aCx, exnObj, "name", &tmp) || !name.init(aCx, tmp)) { + JS_ClearPendingException(aCx); + } + + if (!JS_GetProperty(aCx, exnObj, "message", &tmp) || + !message.init(aCx, tmp)) { + JS_ClearPendingException(aCx); + } + + if (!JS_GetProperty(aCx, exnObj, "lineNumber", &tmp) || + !JS::ToInt32(aCx, tmp, &line)) { + JS_ClearPendingException(aCx); + line = 0; + } + + printf_stderr("PREEXISTING EXCEPTION OBJECT: '%s: %s'\n%s:%d\n%s\n", + NS_ConvertUTF16toUTF8(name).get(), + NS_ConvertUTF16toUTF8(message).get(), + NS_ConvertUTF16toUTF8(filename).get(), line, + NS_ConvertUTF16toUTF8(stack).get()); + } else { + // It's a primitive... not much we can do other than stringify it. + nsAutoJSString exnStr; + if (!exnStr.init(aCx, exn)) { + JS_ClearPendingException(aCx); + } + + printf_stderr("PREEXISTING EXCEPTION PRIMITIVE: %s\n", + NS_ConvertUTF16toUTF8(exnStr).get()); + } + MOZ_ASSERT(false, "We had an exception; we should not have"); + } +#endif // DEBUG +} + +AutoJSAPI::AutoJSAPI(nsIGlobalObject* aGlobalObject, bool aIsMainThread, + Type aType) + : ScriptSettingsStackEntry(aGlobalObject, aType), + mIsMainThread(aIsMainThread) { + MOZ_ASSERT(aGlobalObject); + MOZ_ASSERT(aGlobalObject->HasJSGlobal(), "Must have a JS global"); + MOZ_ASSERT(aIsMainThread == NS_IsMainThread()); + + InitInternal(aGlobalObject, aGlobalObject->GetGlobalJSObject(), + danger::GetJSContext(), aIsMainThread); +} + +void AutoJSAPI::Init() { + MOZ_ASSERT(!mCx, "An AutoJSAPI should only be initialised once"); + + InitInternal(/* aGlobalObject */ nullptr, /* aGlobal */ nullptr, + danger::GetJSContext(), NS_IsMainThread()); +} + +bool AutoJSAPI::Init(nsIGlobalObject* aGlobalObject, JSContext* aCx) { + MOZ_ASSERT(!mCx, "An AutoJSAPI should only be initialised once"); + MOZ_ASSERT(aCx); + + if (NS_WARN_IF(!aGlobalObject)) { + return false; + } + + JSObject* global = aGlobalObject->GetGlobalJSObject(); + if (NS_WARN_IF(!global)) { + return false; + } + + InitInternal(aGlobalObject, global, aCx, NS_IsMainThread()); + return true; +} + +bool AutoJSAPI::Init(nsIGlobalObject* aGlobalObject) { + return Init(aGlobalObject, danger::GetJSContext()); +} + +bool AutoJSAPI::Init(JSObject* aObject) { + MOZ_ASSERT(!js::IsCrossCompartmentWrapper(aObject)); + return Init(xpc::NativeGlobal(aObject)); +} + +bool AutoJSAPI::Init(nsPIDOMWindowInner* aWindow, JSContext* aCx) { + return Init(nsGlobalWindowInner::Cast(aWindow), aCx); +} + +bool AutoJSAPI::Init(nsPIDOMWindowInner* aWindow) { + return Init(nsGlobalWindowInner::Cast(aWindow)); +} + +bool AutoJSAPI::Init(nsGlobalWindowInner* aWindow, JSContext* aCx) { + return Init(static_cast<nsIGlobalObject*>(aWindow), aCx); +} + +bool AutoJSAPI::Init(nsGlobalWindowInner* aWindow) { + return Init(static_cast<nsIGlobalObject*>(aWindow)); +} + +// Even with autoJSAPIOwnsErrorReporting, the JS engine still sends warning +// reports to the JSErrorReporter as soon as they are generated. These go +// directly to the console, so we can handle them easily here. +// +// Eventually, SpiderMonkey will have a special-purpose callback for warnings +// only. +void WarningOnlyErrorReporter(JSContext* aCx, JSErrorReport* aRep) { + MOZ_ASSERT(aRep->isWarning()); + if (!NS_IsMainThread()) { + // Reporting a warning on workers is a bit complicated because we have to + // climb our parent chain until we get to the main thread. So go ahead and + // just go through the worker or worklet ReportError codepath here. + // + // That said, it feels like we should be able to short-circuit things a bit + // here by posting an appropriate runnable to the main thread directly... + // Worth looking into sometime. + CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::GetFor(aCx); + MOZ_ASSERT(ccjscx); + + ccjscx->ReportError(aRep, JS::ConstUTF8CharsZ()); + return; + } + + RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport(); + nsGlobalWindowInner* win = xpc::CurrentWindowOrNull(aCx); + xpcReport->Init(aRep, nullptr, nsContentUtils::IsSystemCaller(aCx), + win ? win->WindowID() : 0); + xpcReport->LogToConsole(); +} + +void AutoJSAPI::ReportException() { + if (!HasException()) { + return; + } + + // AutoJSAPI uses a JSAutoNullableRealm, and may be in a null realm + // when the destructor is called. However, the JS engine requires us + // to be in a realm when we fetch the pending exception. In this case, + // we enter the privileged junk scope and don't dispatch any error events. + JS::Rooted<JSObject*> errorGlobal(cx(), JS::CurrentGlobalOrNull(cx())); + if (!errorGlobal) { + if (mIsMainThread) { + errorGlobal = xpc::PrivilegedJunkScope(); + } else { + errorGlobal = GetCurrentThreadWorkerGlobal(); + if (!errorGlobal) { + // We might be reporting an error in debugger code that ran before the + // worker's global was created. Use the debugger global instead. + errorGlobal = GetCurrentThreadWorkerDebuggerGlobal(); + if (NS_WARN_IF(!errorGlobal)) { + // An exception may have been thrown on attempt to create a global + // and now there is no realm from which to fetch the exception. + // Give up. + ClearException(); + return; + } + } + } + } + MOZ_ASSERT(JS_IsGlobalObject(errorGlobal)); + JSAutoRealm ar(cx(), errorGlobal); + JS::ExceptionStack exnStack(cx()); + JS::ErrorReportBuilder jsReport(cx()); + if (StealExceptionAndStack(&exnStack) && + jsReport.init(cx(), exnStack, JS::ErrorReportBuilder::WithSideEffects)) { + if (mIsMainThread) { + RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport(); + + RefPtr<nsGlobalWindowInner> inner = xpc::WindowOrNull(errorGlobal); + bool isChrome = + nsContentUtils::ObjectPrincipal(errorGlobal)->IsSystemPrincipal(); + xpcReport->Init(jsReport.report(), jsReport.toStringResult().c_str(), + isChrome, inner ? inner->WindowID() : 0); + if (inner && jsReport.report()->errorNumber != JSMSG_OUT_OF_MEMORY) { + JS::RootingContext* rcx = JS::RootingContext::get(cx()); + DispatchScriptErrorEvent(inner, rcx, xpcReport, exnStack.exception(), + exnStack.stack()); + } else { + JS::Rooted<JSObject*> stack(cx()); + JS::Rooted<JSObject*> stackGlobal(cx()); + xpc::FindExceptionStackForConsoleReport(inner, exnStack.exception(), + exnStack.stack(), &stack, + &stackGlobal); + // This error is not associated with a specific window, + // so omit the exception value to mitigate potential leaks. + xpcReport->LogToConsoleWithStack(inner, JS::NothingHandleValue, stack, + stackGlobal); + } + } else { + // On a worker or worklet, we just use the error reporting mechanism and + // don't bother with xpc::ErrorReport. This will ensure that all the + // right worker events (which are a lot more complicated than in the + // window case) get fired. + CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::GetFor(cx()); + MOZ_ASSERT(ccjscx); + // Before invoking ReportError, put the exception back on the context, + // because it may want to put it in its error events and has no other way + // to get hold of it. After we invoke ReportError, clear the exception on + // cx(), just in case ReportError didn't. + JS::SetPendingExceptionStack(cx(), exnStack); + ccjscx->ReportError(jsReport.report(), jsReport.toStringResult()); + ClearException(); + } + } else { + NS_WARNING("OOMed while acquiring uncaught exception from JSAPI"); + ClearException(); + } +} + +bool AutoJSAPI::PeekException(JS::MutableHandle<JS::Value> aVal) { + MOZ_ASSERT_IF(mIsMainThread, IsStackTop()); + MOZ_ASSERT(HasException()); + MOZ_ASSERT(js::GetContextRealm(cx())); + return JS_GetPendingException(cx(), aVal); +} + +bool AutoJSAPI::StealException(JS::MutableHandle<JS::Value> aVal) { + JS::ExceptionStack exnStack(cx()); + if (!StealExceptionAndStack(&exnStack)) { + return false; + } + aVal.set(exnStack.exception()); + return true; +} + +bool AutoJSAPI::StealExceptionAndStack(JS::ExceptionStack* aExnStack) { + MOZ_ASSERT_IF(mIsMainThread, IsStackTop()); + MOZ_ASSERT(HasException()); + MOZ_ASSERT(js::GetContextRealm(cx())); + + return JS::StealPendingExceptionStack(cx(), aExnStack); +} + +#ifdef DEBUG +bool AutoJSAPI::IsStackTop() const { + return ScriptSettingsStack::TopNonIncumbentScript() == this; +} +#endif // DEBUG + +AutoEntryScript::AutoEntryScript(nsIGlobalObject* aGlobalObject, + const char* aReason, bool aIsMainThread) + : AutoJSAPI(aGlobalObject, aIsMainThread, eEntryScript), + mWebIDLCallerPrincipal(nullptr) + // This relies on us having a cx() because the AutoJSAPI constructor + // already ran. + , + mCallerOverride(cx()) +#ifdef MOZ_GECKO_PROFILER + , + mAutoProfilerLabel( + "", aReason, JS::ProfilingCategoryPair::JS, + uint32_t(js::ProfilingStackFrame::Flags::RELEVANT_FOR_JS)) +#endif + , + mJSThreadExecution(aGlobalObject, aIsMainThread) { + MOZ_ASSERT(aGlobalObject); + + if (aIsMainThread) { +#ifdef DEBUG + AssertIfNotSafeToRunScript(); +#endif + if (gRunToCompletionListeners > 0) { + mDocShellEntryMonitor.emplace(cx(), aReason); + } + mScriptActivity.emplace(true); + } +} + +AutoEntryScript::AutoEntryScript(JSObject* aObject, const char* aReason, + bool aIsMainThread) + : AutoEntryScript(xpc::NativeGlobal(aObject), aReason, aIsMainThread) { + // xpc::NativeGlobal uses JS::GetNonCCWObjectGlobal, which asserts that + // aObject is not a CCW. +} + +AutoEntryScript::~AutoEntryScript() = default; + +AutoEntryScript::DocshellEntryMonitor::DocshellEntryMonitor(JSContext* aCx, + const char* aReason) + : JS::dbg::AutoEntryMonitor(aCx), mReason(aReason) {} + +void AutoEntryScript::DocshellEntryMonitor::Entry( + JSContext* aCx, JSFunction* aFunction, JSScript* aScript, + JS::Handle<JS::Value> aAsyncStack, const char* aAsyncCause) { + JS::Rooted<JSFunction*> rootedFunction(aCx); + if (aFunction) { + rootedFunction = aFunction; + } + JS::Rooted<JSScript*> rootedScript(aCx); + if (aScript) { + rootedScript = aScript; + } + + nsCOMPtr<nsPIDOMWindowInner> window = xpc::CurrentWindowOrNull(aCx); + if (!window || !window->GetDocShell() || + !window->GetDocShell()->GetRecordProfileTimelineMarkers()) { + return; + } + + nsCOMPtr<nsIDocShell> docShellForJSRunToCompletion = window->GetDocShell(); + + nsAutoJSString functionName; + if (rootedFunction) { + JS::Rooted<JSString*> displayId(aCx, + JS_GetFunctionDisplayId(rootedFunction)); + if (displayId) { + if (!functionName.init(aCx, displayId)) { + JS_ClearPendingException(aCx); + return; + } + } + } + + nsString filename; + uint32_t lineNumber = 0; + if (!rootedScript) { + rootedScript = JS_GetFunctionScript(aCx, rootedFunction); + } + if (rootedScript) { + CopyUTF8toUTF16(MakeStringSpan(JS_GetScriptFilename(rootedScript)), + filename); + lineNumber = JS_GetScriptBaseLineNumber(aCx, rootedScript); + } + + if (!filename.IsEmpty() || !functionName.IsEmpty()) { + docShellForJSRunToCompletion->NotifyJSRunToCompletionStart( + mReason, functionName, filename, lineNumber, aAsyncStack, aAsyncCause); + } +} + +void AutoEntryScript::DocshellEntryMonitor::Exit(JSContext* aCx) { + nsCOMPtr<nsPIDOMWindowInner> window = xpc::CurrentWindowOrNull(aCx); + // Not really worth checking GetRecordProfileTimelineMarkers here. + if (window && window->GetDocShell()) { + nsCOMPtr<nsIDocShell> docShellForJSRunToCompletion = window->GetDocShell(); + docShellForJSRunToCompletion->NotifyJSRunToCompletionStop(); + } +} + +AutoIncumbentScript::AutoIncumbentScript(nsIGlobalObject* aGlobalObject) + : ScriptSettingsStackEntry(aGlobalObject, eIncumbentScript), + mCallerOverride(nsContentUtils::GetCurrentJSContext()) { + ScriptSettingsStack::Push(this); +} + +AutoIncumbentScript::~AutoIncumbentScript() { ScriptSettingsStack::Pop(this); } + +AutoNoJSAPI::AutoNoJSAPI(JSContext* aCx) + : ScriptSettingsStackEntry(nullptr, eNoJSAPI), + JSAutoNullableRealm(aCx, nullptr), + mCx(aCx) { + // Make sure we don't seem to have an incumbent global due to + // whatever script is running right now. + JS::HideScriptedCaller(aCx); + + // Make sure the fallback GetIncumbentGlobal() behavior and + // GetEntryGlobal() both return null. + ScriptSettingsStack::Push(this); +} + +AutoNoJSAPI::~AutoNoJSAPI() { + ScriptSettingsStack::Pop(this); + JS::UnhideScriptedCaller(mCx); +} + +} // namespace dom + +AutoJSContext::AutoJSContext() : mCx(nullptr) { + JS::AutoSuppressGCAnalysis nogc; + MOZ_ASSERT(!mCx, "mCx should not be initialized!"); + MOZ_ASSERT(NS_IsMainThread()); + + if (dom::IsJSAPIActive()) { + mCx = dom::danger::GetJSContext(); + } else { + mJSAPI.Init(); + mCx = mJSAPI.cx(); + } +} + +AutoJSContext::operator JSContext*() const { return mCx; } + +AutoSafeJSContext::AutoSafeJSContext() : AutoJSAPI() { + MOZ_ASSERT(NS_IsMainThread()); + + DebugOnly<bool> ok = Init(xpc::UnprivilegedJunkScope()); + MOZ_ASSERT(ok, + "This is quite odd. We should have crashed in the " + "xpc::NativeGlobal() call if xpc::UnprivilegedJunkScope() " + "returned null, and inited correctly otherwise!"); +} + +AutoSlowOperation::AutoSlowOperation() : mIsMainThread(NS_IsMainThread()) { + if (mIsMainThread) { + mScriptActivity.emplace(true); + } +} + +void AutoSlowOperation::CheckForInterrupt() { + // For now we support only main thread! + if (mIsMainThread) { + // JS_CheckForInterrupt expects us to be in a realm, so we use a junk scope. + // In principle, it doesn't matter which one we use, since we aren't really + // running scripts here, and none of our interrupt callbacks can stop + // scripts in a junk scope anyway. In practice, though, the privileged junk + // scope is the same as the JSM global, and therefore always exists, while + // the unprivileged junk scope is created lazily, and may not exist until we + // try to use it. So we use the former for the sake of efficiency. + dom::AutoJSAPI jsapi; + MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope())); + JS_CheckForInterrupt(jsapi.cx()); + } +} + +AutoAllowLegacyScriptExecution::AutoAllowLegacyScriptExecution() { +#ifdef DEBUG + // no need to do that dance if we are off the main thread, + // because we only assert if we are on the main thread! + if (!NS_IsMainThread()) { + return; + } + sAutoAllowLegacyScriptExecution++; +#endif +} + +AutoAllowLegacyScriptExecution::~AutoAllowLegacyScriptExecution() { +#ifdef DEBUG + // no need to do that dance if we are off the main thread, + // because we only assert if we are on the main thread! + if (!NS_IsMainThread()) { + return; + } + sAutoAllowLegacyScriptExecution--; + MOZ_ASSERT(sAutoAllowLegacyScriptExecution >= 0, + "how can the stack guard produce a value less than 0?"); +#endif +} + +int AutoAllowLegacyScriptExecution::sAutoAllowLegacyScriptExecution = 0; + +/*static*/ +bool AutoAllowLegacyScriptExecution::IsAllowed() { + return sAutoAllowLegacyScriptExecution > 0; +} + +} // namespace mozilla diff --git a/dom/script/ScriptSettings.h b/dom/script/ScriptSettings.h new file mode 100644 index 0000000000..289ebe41ff --- /dev/null +++ b/dom/script/ScriptSettings.h @@ -0,0 +1,527 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +/* Utilities for managing the script settings object stack defined in webapps */ + +#ifndef mozilla_dom_ScriptSettings_h +#define mozilla_dom_ScriptSettings_h + +#include "MainThreadUtils.h" +#include "xpcpublic.h" + +#include "mozilla/dom/JSExecutionManager.h" +#include "mozilla/Maybe.h" + +#include "jsapi.h" +#include "js/Debug.h" +#include "js/Warnings.h" // JS::WarningReporter + +#ifdef MOZ_GECKO_PROFILER +# include "GeckoProfiler.h" +#endif + +class JSFunction; +class JSObject; +class JSScript; +class nsIGlobalObject; +class nsIPrincipal; +class nsPIDOMWindowInner; +class nsGlobalWindowInner; +class nsIScriptContext; +struct JSContext; + +namespace JS { +class ExceptionStack; +class Value; +} // namespace JS + +namespace mozilla { +namespace dom { + +class Document; + +/* + * Per thread setup/teardown routines. Init and Destroy should be invoked + * once each, at startup and shutdown of the script runtime (respectively). + */ +void InitScriptSettings(); +void DestroyScriptSettings(); + +/* + * Static helpers in ScriptSettings which track the number of listeners + * of Javascript RunToCompletion events. These should be used by the code in + * nsDocShell::SetRecordProfileTimelineMarkers to indicate to script + * settings that script run-to-completion needs to be monitored. + * SHOULD BE CALLED ONLY BY MAIN THREAD. + */ +void UseEntryScriptProfiling(); +void UnuseEntryScriptProfiling(); + +// To implement a web-compatible browser, it is often necessary to obtain the +// global object that is "associated" with the currently-running code. This +// process is made more complicated by the fact that, historically, different +// algorithms have operated with different definitions of the "associated" +// global. +// +// HTML5 formalizes this into two concepts: the "incumbent global" and the +// "entry global". The incumbent global corresponds to the global of the +// current script being executed, whereas the entry global corresponds to the +// global of the script where the current JS execution began. +// +// There is also a potentially-distinct third global that is determined by the +// current compartment. This roughly corresponds with the notion of Realms in +// ECMAScript. +// +// Suppose some event triggers an event listener in window |A|, which invokes a +// scripted function in window |B|, which invokes the |window.location.href| +// setter in window |C|. The entry global would be |A|, the incumbent global +// would be |B|, and the current compartment would be that of |C|. +// +// In general, it's best to use to use the most-closely-associated global +// unless the spec says to do otherwise. In 95% of the cases, the global of +// the current compartment (GetCurrentGlobal()) is the right thing. For +// example, WebIDL constructors (new C.XMLHttpRequest()) are initialized with +// the global of the current compartment (i.e. |C|). +// +// The incumbent global is very similar, but differs in a few edge cases. For +// example, if window |B| does |C.location.href = "..."|, the incumbent global +// used for the navigation algorithm is B, because no script from |C| was ever +// run. +// +// The entry global is used for various things like computing base URIs, mostly +// for historical reasons. +// +// Note that all of these functions return bonafide global objects. This means +// that, for Windows, they always return the inner. + +// Returns the global associated with the top-most Candidate Entry Point on +// the Script Settings Stack. See the HTML spec. This may be null. +nsIGlobalObject* GetEntryGlobal(); + +// If the entry global is a window, returns its extant document. Otherwise, +// returns null. +Document* GetEntryDocument(); + +// Returns the global associated with the top-most entry of the the Script +// Settings Stack. See the HTML spec. This may be null. +nsIGlobalObject* GetIncumbentGlobal(); + +// Returns the global associated with the current compartment. This may be null. +nsIGlobalObject* GetCurrentGlobal(); + +// JS-implemented WebIDL presents an interesting situation with respect to the +// subject principal. A regular C++-implemented API can simply examine the +// compartment of the most-recently-executed script, and use that to infer the +// responsible party. However, JS-implemented APIs are run with system +// principal, and thus clobber the subject principal of the script that +// invoked the API. So we have to do some extra work to keep track of this +// information. +// +// We therefore implement the following behavior: +// * Each Script Settings Object has an optional WebIDL Caller Principal field. +// This defaults to null. +// * When we push an Entry Point in preparation to run a JS-implemented WebIDL +// callback, we grab the subject principal at the time of invocation, and +// store that as the WebIDL Caller Principal. +// * When non-null, callers can query this principal from script via an API on +// Components.utils. +nsIPrincipal* GetWebIDLCallerPrincipal(); + +// Returns whether JSAPI is active right now. If it is not, working with a +// JSContext you grab from somewhere random is not OK and you should be doing +// AutoJSAPI or AutoEntryScript to get yourself a properly set up JSContext. +bool IsJSAPIActive(); + +namespace danger { + +// Get the JSContext for this thread. This is in the "danger" namespace because +// we generally want people using AutoJSAPI instead, unless they really know +// what they're doing. +JSContext* GetJSContext(); + +} // namespace danger + +JS::RootingContext* RootingCx(); + +class ScriptSettingsStack; +class ScriptSettingsStackEntry { + friend class ScriptSettingsStack; + + public: + ~ScriptSettingsStackEntry(); + + bool NoJSAPI() const { return mType == eNoJSAPI; } + bool IsEntryCandidate() const { + return mType == eEntryScript || mType == eNoJSAPI; + } + bool IsIncumbentCandidate() { return mType != eJSAPI; } + bool IsIncumbentScript() { return mType == eIncumbentScript; } + + protected: + enum Type { eEntryScript, eIncumbentScript, eJSAPI, eNoJSAPI }; + + ScriptSettingsStackEntry(nsIGlobalObject* aGlobal, Type aEntryType); + + nsCOMPtr<nsIGlobalObject> mGlobalObject; + Type mType; + + private: + ScriptSettingsStackEntry* mOlder; +}; + +/* + * For any interaction with JSAPI, an AutoJSAPI (or one of its subclasses) + * must be on the stack. + * + * This base class should be instantiated as-is when the caller wants to use + * JSAPI but doesn't expect to run script. The caller must then call one of its + * Init functions before being able to access the JSContext through cx(). + * Its current duties are as-follows (see individual Init comments for details): + * + * * Grabbing an appropriate JSContext, and, on the main thread, pushing it onto + * the JSContext stack. + * * Entering an initial (possibly null) compartment, to ensure that the + * previously entered compartment for that JSContext is not used by mistake. + * * Reporting any exceptions left on the JSRuntime, unless the caller steals + * or silences them. + * + * Additionally, the following duties are planned, but not yet implemented: + * + * * De-poisoning the JSRuntime to allow manipulation of JSAPI. This requires + * implementing the poisoning first. For now, this de-poisoning + * effectively corresponds to having a non-null cx on the stack. + * + * In situations where the consumer expects to run script, AutoEntryScript + * should be used, which does additional manipulation of the script settings + * stack. In bug 991758, we'll add hard invariants to SpiderMonkey, such that + * any attempt to run script without an AutoEntryScript on the stack will + * fail. This prevents system code from accidentally triggering script + * execution at inopportune moments via surreptitious getters and proxies. + */ +class MOZ_STACK_CLASS AutoJSAPI : protected ScriptSettingsStackEntry { + public: + // Trivial constructor. One of the Init functions must be called before + // accessing the JSContext through cx(). + AutoJSAPI(); + + ~AutoJSAPI(); + + // This uses the SafeJSContext (or worker equivalent), and enters a null + // compartment, so that the consumer is forced to select a compartment to + // enter before manipulating objects. + // + // This variant will ensure that any errors reported by this AutoJSAPI as it + // comes off the stack will not fire error events or be associated with any + // particular web-visible global. + void Init(); + + // This uses the SafeJSContext (or worker equivalent), and enters the + // compartment of aGlobalObject. + // If aGlobalObject or its associated JS global are null then it returns + // false and use of cx() will cause an assertion. + // + // If aGlobalObject represents a web-visible global, errors reported by this + // AutoJSAPI as it comes off the stack will fire the relevant error events and + // show up in the corresponding web console. + // + // Successfully initializing the AutoJSAPI will ensure that it enters the + // Realm of aGlobalObject's JSObject and exposes that JSObject to active JS. + MOZ_MUST_USE bool Init(nsIGlobalObject* aGlobalObject); + + // This is a helper that grabs the native global associated with aObject and + // invokes the above Init() with that. aObject must not be a cross-compartment + // wrapper: CCWs are not associated with a single global. + MOZ_MUST_USE bool Init(JSObject* aObject); + + // Unsurprisingly, this uses aCx and enters the compartment of aGlobalObject. + // If aGlobalObject or its associated JS global are null then it returns + // false and use of cx() will cause an assertion. + // If aCx is null it will cause an assertion. + // + // If aGlobalObject represents a web-visible global, errors reported by this + // AutoJSAPI as it comes off the stack will fire the relevant error events and + // show up in the corresponding web console. + MOZ_MUST_USE bool Init(nsIGlobalObject* aGlobalObject, JSContext* aCx); + + // Convenience functions to take an nsPIDOMWindowInner or nsGlobalWindowInner, + // when it is more easily available than an nsIGlobalObject. + MOZ_MUST_USE bool Init(nsPIDOMWindowInner* aWindow); + MOZ_MUST_USE bool Init(nsPIDOMWindowInner* aWindow, JSContext* aCx); + + MOZ_MUST_USE bool Init(nsGlobalWindowInner* aWindow); + MOZ_MUST_USE bool Init(nsGlobalWindowInner* aWindow, JSContext* aCx); + + JSContext* cx() const { + MOZ_ASSERT(mCx, "Must call Init before using an AutoJSAPI"); + MOZ_ASSERT(IsStackTop()); + return mCx; + } + +#ifdef DEBUG + bool IsStackTop() const; +#endif + + // If HasException, report it. Otherwise, a no-op. + void ReportException(); + + bool HasException() const { + MOZ_ASSERT(IsStackTop()); + return JS_IsExceptionPending(cx()); + }; + + // Transfers ownership of the current exception from the JS engine to the + // caller. Callers must ensure that HasException() is true, and that cx() + // is in a non-null compartment. + // + // Note that this fails if and only if we OOM while wrapping the exception + // into the current compartment. + MOZ_MUST_USE bool StealException(JS::MutableHandle<JS::Value> aVal); + + // As for StealException(), but uses the JS::ExceptionStack class to also + // include the exception's stack, represented by SavedFrames. + MOZ_MUST_USE bool StealExceptionAndStack(JS::ExceptionStack* aExnStack); + + // Peek the current exception from the JS engine, without stealing it. + // Callers must ensure that HasException() is true, and that cx() is in a + // non-null compartment. + // + // Note that this fails if and only if we OOM while wrapping the exception + // into the current compartment. + MOZ_MUST_USE bool PeekException(JS::MutableHandle<JS::Value> aVal); + + void ClearException() { + MOZ_ASSERT(IsStackTop()); + JS_ClearPendingException(cx()); + } + + protected: + // Protected constructor for subclasses. This constructor initialises the + // AutoJSAPI, so Init must NOT be called on subclasses that use this. + AutoJSAPI(nsIGlobalObject* aGlobalObject, bool aIsMainThread, Type aType); + + mozilla::Maybe<JSAutoNullableRealm> mAutoNullableRealm; + JSContext* mCx; + + // Whether we're mainthread or not; set when we're initialized. + bool mIsMainThread; + Maybe<JS::WarningReporter> mOldWarningReporter; + + private: + void InitInternal(nsIGlobalObject* aGlobalObject, JSObject* aGlobal, + JSContext* aCx, bool aIsMainThread); + + AutoJSAPI(const AutoJSAPI&) = delete; + AutoJSAPI& operator=(const AutoJSAPI&) = delete; +}; + +/* + * A class that represents a new script entry point. + * + * |aReason| should be a statically-allocated C string naming the reason we're + * invoking JavaScript code: "setTimeout", "event", and so on. The devtools use + * these strings to label JS execution in timeline and profiling displays. + * + */ +class MOZ_STACK_CLASS AutoEntryScript : public AutoJSAPI { + public: + // Constructing the AutoEntryScript will ensure that it enters the + // Realm of aGlobalObject's JSObject and exposes that JSObject to active JS. + AutoEntryScript(nsIGlobalObject* aGlobalObject, const char* aReason, + bool aIsMainThread = NS_IsMainThread()); + + // aObject can be any object from the relevant global. It must not be a + // cross-compartment wrapper because CCWs are not associated with a single + // global. + // + // Constructing the AutoEntryScript will ensure that it enters the + // Realm of aObject JSObject and exposes aObject's global to active JS. + AutoEntryScript(JSObject* aObject, const char* aReason, + bool aIsMainThread = NS_IsMainThread()); + + ~AutoEntryScript(); + + void SetWebIDLCallerPrincipal(nsIPrincipal* aPrincipal) { + mWebIDLCallerPrincipal = aPrincipal; + } + + private: + // A subclass of AutoEntryMonitor that notifies the docshell. + class DocshellEntryMonitor final : public JS::dbg::AutoEntryMonitor { + public: + DocshellEntryMonitor(JSContext* aCx, const char* aReason); + + // Please note that |aAsyncCause| here is owned by the caller, and its + // lifetime must outlive the lifetime of the DocshellEntryMonitor object. + // In practice, |aAsyncCause| is identical to |aReason| passed into + // the AutoEntryScript constructor, so the lifetime requirements are + // trivially satisfied by |aReason| being a statically allocated string. + void Entry(JSContext* aCx, JSFunction* aFunction, + JS::Handle<JS::Value> aAsyncStack, + const char* aAsyncCause) override { + Entry(aCx, aFunction, nullptr, aAsyncStack, aAsyncCause); + } + + void Entry(JSContext* aCx, JSScript* aScript, + JS::Handle<JS::Value> aAsyncStack, + const char* aAsyncCause) override { + Entry(aCx, nullptr, aScript, aAsyncStack, aAsyncCause); + } + + void Exit(JSContext* aCx) override; + + private: + void Entry(JSContext* aCx, JSFunction* aFunction, JSScript* aScript, + JS::Handle<JS::Value> aAsyncStack, const char* aAsyncCause); + + const char* mReason; + }; + + // It's safe to make this a weak pointer, since it's the subject principal + // when we go on the stack, so can't go away until after we're gone. In + // particular, this is only used from the CallSetup constructor, and only in + // the aIsJSImplementedWebIDL case. And in that case, the subject principal + // is the principal of the callee function that is part of the CallArgs just a + // bit up the stack, and which will outlive us. So we know the principal + // can't go away until then either. + nsIPrincipal* MOZ_NON_OWNING_REF mWebIDLCallerPrincipal; + friend nsIPrincipal* GetWebIDLCallerPrincipal(); + + Maybe<DocshellEntryMonitor> mDocShellEntryMonitor; + Maybe<xpc::AutoScriptActivity> mScriptActivity; + JS::AutoHideScriptedCaller mCallerOverride; +#ifdef MOZ_GECKO_PROFILER + AutoProfilerLabel mAutoProfilerLabel; +#endif + AutoRequestJSThreadExecution mJSThreadExecution; +}; + +/* + * A class that can be used to force a particular incumbent script on the stack. + */ +class AutoIncumbentScript : protected ScriptSettingsStackEntry { + public: + explicit AutoIncumbentScript(nsIGlobalObject* aGlobalObject); + ~AutoIncumbentScript(); + + private: + JS::AutoHideScriptedCaller mCallerOverride; +}; + +/* + * A class to put the JS engine in an unusable state. The subject principal + * will become System, the information on the script settings stack is + * rendered inaccessible, and JSAPI may not be manipulated until the class is + * either popped or an AutoJSAPI instance is subsequently pushed. + * + * This class may not be instantiated if an exception is pending. + */ +class AutoNoJSAPI : protected ScriptSettingsStackEntry, + protected JSAutoNullableRealm { + public: + AutoNoJSAPI() : AutoNoJSAPI(danger::GetJSContext()) {} + ~AutoNoJSAPI(); + + private: + // Helper constructor to avoid doing GetJSContext() multiple times + // during construction. + explicit AutoNoJSAPI(JSContext* aCx); + + // Stashed JSContext* so we don't need to GetJSContext in our destructor. + // It's probably safe to hold on to this, in the sense that the world should + // not get torn down while we're on the stack, and if it's not, we'd need to + // fix JSAutoNullableRealm to not hold on to a JSContext either, or + // something. + JSContext* mCx; + + AutoYieldJSThreadExecution mExecutionYield; +}; + +} // namespace dom + +/** + * Use AutoJSContext when you need a JS context on the stack but don't have one + * passed as a parameter. AutoJSContext will take care of finding the most + * appropriate JS context and release it when leaving the stack. + */ +class MOZ_RAII AutoJSContext { + public: + explicit AutoJSContext(); + operator JSContext*() const; + + protected: + JSContext* mCx; + dom::AutoJSAPI mJSAPI; +}; + +/** + * AutoSafeJSContext is similar to AutoJSContext but will only return the safe + * JS context. That means it will never call + * nsContentUtils::GetCurrentJSContext(). + * + * Note - This is deprecated. Please use AutoJSAPI instead. + */ +class MOZ_RAII AutoSafeJSContext : public dom::AutoJSAPI { + public: + explicit AutoSafeJSContext(); + operator JSContext*() const { return cx(); } + + private: +}; + +/** + * Use AutoSlowOperation when native side calls many JS callbacks in a row + * and slow script dialog should be activated if too much time is spent going + * through those callbacks. + * AutoSlowOperation puts an AutoScriptActivity on the stack so that we don't + * continue to reset the watchdog. CheckForInterrupt can then be used to check + * whether JS execution should be interrupted. + * This class (including CheckForInterrupt) is a no-op when used off the main + * thread. + */ +class MOZ_RAII AutoSlowOperation { + public: + explicit AutoSlowOperation(); + void CheckForInterrupt(); + + private: + bool mIsMainThread; + Maybe<xpc::AutoScriptActivity> mScriptActivity; +}; + +/** + * A class to disable interrupt callback temporary. + */ +class MOZ_RAII AutoDisableJSInterruptCallback { + public: + explicit AutoDisableJSInterruptCallback(JSContext* aCx) + : mCx(aCx), mOld(JS_DisableInterruptCallback(aCx)) {} + + ~AutoDisableJSInterruptCallback() { JS_ResetInterruptCallback(mCx, mOld); } + + private: + JSContext* mCx; + bool mOld; +}; + +/** + * A helper class which allows to allow-list legacy callers executing script + * in the AutoEntryScript constructor. The goal is to remove these exceptions + * one by one. Do not add a new one without review from a DOM peer. + */ +class MOZ_RAII AutoAllowLegacyScriptExecution { + public: + AutoAllowLegacyScriptExecution(); + ~AutoAllowLegacyScriptExecution(); + + static bool IsAllowed(); + + private: + static int sAutoAllowLegacyScriptExecution; +}; + +} // namespace mozilla + +#endif // mozilla_dom_ScriptSettings_h diff --git a/dom/script/ScriptTrace.cpp b/dom/script/ScriptTrace.cpp new file mode 100644 index 0000000000..8e29381e4e --- /dev/null +++ b/dom/script/ScriptTrace.cpp @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "ScriptTrace.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/StaticPrefs_dom.h" + +namespace mozilla { +namespace dom { +namespace script { + +static nsresult TestingDispatchEvent(nsIScriptElement* aScriptElement, + const nsAString& aEventType) { + if (!StaticPrefs::dom_expose_test_interfaces()) { + return NS_OK; + } + + nsCOMPtr<nsINode> target(do_QueryInterface(aScriptElement)); + if (!target) { + return NS_OK; + } + + RefPtr<AsyncEventDispatcher> dispatcher = new AsyncEventDispatcher( + target, aEventType, CanBubble::eYes, ChromeOnlyDispatch::eNo); + return dispatcher->PostDOMEvent(); +} + +} // namespace script +} // namespace dom +} // namespace mozilla diff --git a/dom/script/ScriptTrace.h b/dom/script/ScriptTrace.h new file mode 100644 index 0000000000..110ec15d2e --- /dev/null +++ b/dom/script/ScriptTrace.h @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_ScriptTrace_h +#define mozilla_dom_ScriptTrace_h + +#include "ScriptLoader.h" + +namespace mozilla { +namespace dom { +namespace script { + +// This macro is used to wrap a tracing mechanism which is scheduling events +// which are then used by the JavaScript code of test cases to track the code +// path to verify the optimizations are working as expected. +#define TRACE_FOR_TEST(elem, str) \ + PR_BEGIN_MACRO \ + nsresult rv = NS_OK; \ + rv = \ + script::TestingDispatchEvent(elem, NS_LITERAL_STRING_FROM_CSTRING(str)); \ + NS_ENSURE_SUCCESS(rv, rv); \ + PR_END_MACRO + +#define TRACE_FOR_TEST_BOOL(elem, str) \ + PR_BEGIN_MACRO \ + nsresult rv = NS_OK; \ + rv = \ + script::TestingDispatchEvent(elem, NS_LITERAL_STRING_FROM_CSTRING(str)); \ + NS_ENSURE_SUCCESS(rv, false); \ + PR_END_MACRO + +#define TRACE_FOR_TEST_NONE(elem, str) \ + PR_BEGIN_MACRO \ + script::TestingDispatchEvent(elem, NS_LITERAL_STRING_FROM_CSTRING(str)); \ + PR_END_MACRO + +static nsresult TestingDispatchEvent(nsIScriptElement* aScriptElement, + const nsAString& aEventType); + +} // namespace script +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ScriptTrace_h diff --git a/dom/script/moz.build b/dom/script/moz.build new file mode 100644 index 0000000000..64ac2300de --- /dev/null +++ b/dom/script/moz.build @@ -0,0 +1,48 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +XPIDL_SOURCES += [ + "nsIScriptLoaderObserver.idl", +] + +XPIDL_MODULE = "dom" + +EXPORTS += [ + "nsIScriptElement.h", +] + +EXPORTS.mozilla.dom += [ + "LoadedScript.h", + "ScriptDecoding.h", + "ScriptElement.h", + "ScriptKind.h", + "ScriptLoader.h", + "ScriptLoadRequest.h", + "ScriptSettings.h", +] + +UNIFIED_SOURCES += [ + "LoadedScript.cpp", + "ModuleLoadRequest.cpp", + "nsIScriptElement.cpp", + "ScriptElement.cpp", + "ScriptLoader.cpp", + "ScriptLoadHandler.cpp", + "ScriptLoadRequest.cpp", + "ScriptSettings.cpp", + "ScriptTrace.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/base", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/script/nsIScriptElement.cpp b/dom/script/nsIScriptElement.cpp new file mode 100644 index 0000000000..d0b6282874 --- /dev/null +++ b/dom/script/nsIScriptElement.cpp @@ -0,0 +1,53 @@ + +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "nsIScriptElement.h" + +#include "mozilla/dom/ReferrerPolicyBinding.h" +#include "nsIParser.h" +#include "nsIWeakReference.h" + +void nsIScriptElement::SetCreatorParser(nsIParser* aParser) { + mCreatorParser = do_GetWeakReference(aParser); +} + +void nsIScriptElement::UnblockParser() { + nsCOMPtr<nsIParser> parser = do_QueryReferent(mCreatorParser); + if (parser) { + parser->UnblockParser(); + } +} + +void nsIScriptElement::ContinueParserAsync() { + nsCOMPtr<nsIParser> parser = do_QueryReferent(mCreatorParser); + if (parser) { + parser->ContinueInterruptedParsingAsync(); + } +} + +void nsIScriptElement::BeginEvaluating() { + nsCOMPtr<nsIParser> parser = do_QueryReferent(mCreatorParser); + if (parser) { + parser->IncrementScriptNestingLevel(); + } +} + +void nsIScriptElement::EndEvaluating() { + nsCOMPtr<nsIParser> parser = do_QueryReferent(mCreatorParser); + if (parser) { + parser->DecrementScriptNestingLevel(); + } +} + +already_AddRefed<nsIParser> nsIScriptElement::GetCreatorParser() { + nsCOMPtr<nsIParser> parser = do_QueryReferent(mCreatorParser); + return parser.forget(); +} + +mozilla::dom::ReferrerPolicy nsIScriptElement::GetReferrerPolicy() { + return mozilla::dom::ReferrerPolicy::_empty; +} diff --git a/dom/script/nsIScriptElement.h b/dom/script/nsIScriptElement.h new file mode 100644 index 0000000000..c2fd702110 --- /dev/null +++ b/dom/script/nsIScriptElement.h @@ -0,0 +1,343 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef nsIScriptElement_h___ +#define nsIScriptElement_h___ + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/CORSMode.h" +#include "mozilla/dom/FromParser.h" +#include "nsCOMPtr.h" +#include "nsID.h" +#include "nsIScriptLoaderObserver.h" +#include "nsIWeakReferenceUtils.h" +#include "nsStringFwd.h" +#include "nscore.h" + +// XXX Avoid including this here by moving function bodies to the cpp file +#include "nsIPrincipal.h" + +class nsIParser; +class nsIPrincipal; +class nsIURI; + +namespace mozilla::dom { +class Document; +enum class ReferrerPolicy : uint8_t; +} // namespace mozilla::dom + +// Must be kept in sync with xpcom/rust/xpcom/src/interfaces/nonidl.rs +#define NS_ISCRIPTELEMENT_IID \ + { \ + 0xe60fca9b, 0x1b96, 0x4e4e, { \ + 0xa9, 0xb4, 0xdc, 0x98, 0x4f, 0x88, 0x3f, 0x9c \ + } \ + } + +/** + * Internal interface implemented by script elements + */ +class nsIScriptElement : public nsIScriptLoaderObserver { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_ISCRIPTELEMENT_IID) + + explicit nsIScriptElement(mozilla::dom::FromParser aFromParser) + : mLineNumber(1), + mColumnNumber(1), + mAlreadyStarted(false), + mMalformed(false), + mDoneAddingChildren(aFromParser == mozilla::dom::NOT_FROM_PARSER || + aFromParser == mozilla::dom::FROM_PARSER_FRAGMENT), + mForceAsync(aFromParser == mozilla::dom::NOT_FROM_PARSER || + aFromParser == mozilla::dom::FROM_PARSER_FRAGMENT), + mFrozen(false), + mIsModule(false), + mDefer(false), + mAsync(false), + mExternal(false), + mParserCreated(aFromParser == mozilla::dom::FROM_PARSER_FRAGMENT + ? mozilla::dom::NOT_FROM_PARSER + : aFromParser), + // Fragment parser-created scripts (if executable) + // behave like script-created scripts. + mCreatorParser(nullptr) {} + + /** + * Content type identifying the scripting language. Can be empty, in + * which case javascript will be assumed. + * Return false if type attribute is not found. + */ + virtual bool GetScriptType(nsAString& type) = 0; + + /** + * Location of script source text. Can return null, in which case + * this is assumed to be an inline script element. + */ + nsIURI* GetScriptURI() { + MOZ_ASSERT(mFrozen, "Not ready for this call yet!"); + return mUri; + } + + nsIPrincipal* GetScriptURITriggeringPrincipal() { + MOZ_ASSERT(mFrozen, "Not ready for this call yet!"); + return mSrcTriggeringPrincipal; + } + + /** + * Script source text for inline script elements. + */ + virtual void GetScriptText(nsAString& text) = 0; + + virtual void GetScriptCharset(nsAString& charset) = 0; + + /** + * Freezes the return values of the following methods so that subsequent + * modifications to the attributes don't change execution behavior: + * - GetScriptIsModule() + * - GetScriptDeferred() + * - GetScriptAsync() + * - GetScriptURI() + * - GetScriptExternal() + */ + virtual void FreezeExecutionAttrs(mozilla::dom::Document*) = 0; + + /** + * Is the script a module script. Currently only supported by HTML scripts. + */ + bool GetScriptIsModule() { + MOZ_ASSERT(mFrozen, "Not ready for this call yet!"); + return mIsModule; + } + + /** + * Is the script deferred. Currently only supported by HTML scripts. + */ + bool GetScriptDeferred() { + MOZ_ASSERT(mFrozen, "Not ready for this call yet!"); + return mDefer; + } + + /** + * Is the script async. Currently only supported by HTML scripts. + */ + bool GetScriptAsync() { + MOZ_ASSERT(mFrozen, "Not ready for this call yet!"); + return mAsync; + } + + /** + * Is the script an external script? + */ + bool GetScriptExternal() { + MOZ_ASSERT(mFrozen, "Not ready for this call yet!"); + return mExternal; + } + + /** + * Returns how the element was created. + */ + mozilla::dom::FromParser GetParserCreated() { return mParserCreated; } + + void SetScriptLineNumber(uint32_t aLineNumber) { mLineNumber = aLineNumber; } + + uint32_t GetScriptLineNumber() { return mLineNumber; } + + void SetScriptColumnNumber(uint32_t aColumnNumber) { + mColumnNumber = aColumnNumber; + } + + uint32_t GetScriptColumnNumber() { return mColumnNumber; } + + void SetIsMalformed() { mMalformed = true; } + + bool IsMalformed() { return mMalformed; } + + void PreventExecution() { mAlreadyStarted = true; } + + void LoseParserInsertedness() { + mUri = nullptr; + mCreatorParser = nullptr; + mParserCreated = mozilla::dom::NOT_FROM_PARSER; + mForceAsync = !GetAsyncState(); + + // Reset state set by FreezeExecutionAttrs(). + mFrozen = false; + mIsModule = false; + mExternal = false; + mAsync = false; + mDefer = false; + } + + void SetCreatorParser(nsIParser* aParser); + + /** + * Unblocks the creator parser + */ + void UnblockParser(); + + /** + * Attempts to resume parsing asynchronously + */ + void ContinueParserAsync(); + + /** + * Informs the creator parser that the evaluation of this script is starting + */ + void BeginEvaluating(); + + /** + * Informs the creator parser that the evaluation of this script is ending + */ + void EndEvaluating(); + + /** + * Retrieves a pointer to the creator parser if this has one or null if not + */ + already_AddRefed<nsIParser> GetCreatorParser(); + + /** + * This method is called when the parser finishes creating the script + * element's children, if any are present. + * + * @return whether the parser will be blocked while this script is being + * loaded + */ + bool AttemptToExecute() { + mDoneAddingChildren = true; + bool block = MaybeProcessScript(); + if (!mAlreadyStarted) { + // Need to lose parser-insertedness here to allow another script to cause + // execution later. + LoseParserInsertedness(); + } + return block; + } + + /** + * Get the CORS mode of the script element + */ + virtual mozilla::CORSMode GetCORSMode() const { + /* Default to no CORS */ + return mozilla::CORS_NONE; + } + + /** + * Get referrer policy of the script element + */ + virtual mozilla::dom::ReferrerPolicy GetReferrerPolicy(); + + /** + * Fire an error event + */ + virtual nsresult FireErrorEvent() = 0; + + protected: + /** + * Processes the script if it's in the document-tree and links to or + * contains a script. Once it has been evaluated there is no way to make it + * reevaluate the script, you'll have to create a new element. This also means + * that when adding a src attribute to an element that already contains an + * inline script, the script referenced by the src attribute will not be + * loaded. + * + * In order to be able to use multiple childNodes, or to use the + * fallback mechanism of using both inline script and linked script you have + * to add all attributes and childNodes before adding the element to the + * document-tree. + * + * @return whether the parser will be blocked while this script is being + * loaded + */ + virtual bool MaybeProcessScript() = 0; + + /** + * Since we've removed the XPCOM interface to HTML elements, we need a way to + * retreive async state from script elements without bringing the type in. + */ + virtual bool GetAsyncState() = 0; + + /** + * The start line number of the script. + */ + uint32_t mLineNumber; + + /** + * The start column number of the script. + */ + uint32_t mColumnNumber; + + /** + * The "already started" flag per HTML5. + */ + bool mAlreadyStarted; + + /** + * The script didn't have an end tag. + */ + bool mMalformed; + + /** + * False if parser-inserted but the parser hasn't triggered running yet. + */ + bool mDoneAddingChildren; + + /** + * If true, the .async property returns true instead of reflecting the + * content attribute. + */ + bool mForceAsync; + + /** + * Whether src, defer and async are frozen. + */ + bool mFrozen; + + /** + * The effective moduleness. + */ + bool mIsModule; + + /** + * The effective deferredness. + */ + bool mDefer; + + /** + * The effective asyncness. + */ + bool mAsync; + + /** + * The effective externalness. A script can be external with mUri being null + * if the src attribute contained an invalid URL string. + */ + bool mExternal; + + /** + * Whether this element was parser-created. + */ + mozilla::dom::FromParser mParserCreated; + + /** + * The effective src (or null if no src). + */ + nsCOMPtr<nsIURI> mUri; + + /** + * The triggering principal for the src URL. + */ + nsCOMPtr<nsIPrincipal> mSrcTriggeringPrincipal; + + /** + * The creator parser of a non-defer, non-async parser-inserted script. + */ + nsWeakPtr mCreatorParser; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIScriptElement, NS_ISCRIPTELEMENT_IID) + +#endif // nsIScriptElement_h___ diff --git a/dom/script/nsIScriptLoaderObserver.idl b/dom/script/nsIScriptLoaderObserver.idl new file mode 100644 index 0000000000..8fafa201cc --- /dev/null +++ b/dom/script/nsIScriptLoaderObserver.idl @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "nsISupports.idl" + +interface nsIScriptElement; +interface nsIURI; + +[scriptable, uuid(7b787204-76fb-4764-96f1-fb7a666db4f4)] +interface nsIScriptLoaderObserver : nsISupports { + + /** + * The script is available for evaluation. For inline scripts, this + * method will be called synchronously. For externally loaded scripts, + * this method will be called when the load completes. + * + * @param aResult A result code representing the result of loading + * a script. If this is a failure code, script evaluation + * will not occur. + * @param aElement The element being processed. + * @param aIsInline Is this an inline classic script (as opposed to an + * externally loaded classic script or module script)? + * @param aURI What is the URI of the script (the document URI if + * it is inline). + * @param aLineNo At what line does the script appear (generally 1 + * if it is a loaded script). + */ + void scriptAvailable(in nsresult aResult, + in nsIScriptElement aElement, + in boolean aIsInlineClassicScript, + in nsIURI aURI, + in int32_t aLineNo); + + /** + * The script has been evaluated. + * + * @param aResult A result code representing the success or failure of + * the script evaluation. + * @param aElement The element being processed. + * @param aIsInline Is this an inline script or externally loaded? + */ + void scriptEvaluated(in nsresult aResult, + in nsIScriptElement aElement, + in boolean aIsInline); +}; |