/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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 "ImportManager.h" #include "mozilla/EventListenerManager.h" #include "HTMLLinkElement.h" #include "nsContentPolicyUtils.h" #include "nsContentUtils.h" #include "nsCORSListenerProxy.h" #include "nsIChannel.h" #include "nsIContentPolicy.h" #include "nsIContentSecurityPolicy.h" #include "nsIDocument.h" #include "nsIDOMDocument.h" #include "nsIDOMEvent.h" #include "nsIPrincipal.h" #include "nsIScriptObjectPrincipal.h" #include "nsScriptLoader.h" #include "nsNetUtil.h" //----------------------------------------------------------------------------- // AutoError //----------------------------------------------------------------------------- class AutoError { public: explicit AutoError(mozilla::dom::ImportLoader* loader, bool scriptsBlocked = true) : mLoader(loader) , mPassed(false) , mScriptsBlocked(scriptsBlocked) {} ~AutoError() { if (!mPassed) { mLoader->Error(mScriptsBlocked); } } void Pass() { mPassed = true; } private: mozilla::dom::ImportLoader* mLoader; bool mPassed; bool mScriptsBlocked; }; namespace mozilla { namespace dom { //----------------------------------------------------------------------------- // ImportLoader::Updater //----------------------------------------------------------------------------- void ImportLoader::Updater::GetReferrerChain(nsINode* aNode, nsTArray& aResult) { // We fill up the array backward. First the last link: aNode. MOZ_ASSERT(mLoader->mLinks.Contains(aNode)); aResult.AppendElement(aNode); nsINode* node = aNode; nsRefPtr manager = mLoader->Manager(); for (ImportLoader* referrersLoader = manager->Find(node->OwnerDoc()); referrersLoader; referrersLoader = manager->Find(node->OwnerDoc())) { // Then walking up the main referrer chain and append each link // to the array. node = referrersLoader->GetMainReferrer(); MOZ_ASSERT(node); aResult.AppendElement(node); } // The reversed order is more useful for consumers. // XXX: This should probably go to nsTArray or some generic utility // lib for our containers that we don't have... I would really like to // get rid of this part... uint32_t l = aResult.Length(); for (uint32_t i = 0; i < l / 2; i++) { Swap(aResult[i], aResult[l - i - 1]); } } bool ImportLoader::Updater::ShouldUpdate(nsTArray& aNewPath) { if (mLoader->Manager()->GetNearestPredecessor(mLoader->GetMainReferrer()) != mLoader->mBlockingPredecessor) { return true; } // Let's walk down on the main referrer chains of both the current main and // the new link, and find the last pair of links that are from the same // document. This is the junction point between the two referrer chain. Their // order in the subimport list of that document will determine if we have to // update the spanning tree or this new edge changes nothing in the script // execution order. nsTArray oldPath; GetReferrerChain(mLoader->mLinks[mLoader->mMainReferrer], oldPath); uint32_t max = std::min(oldPath.Length(), aNewPath.Length()); MOZ_ASSERT(max > 0); uint32_t lastCommonImportAncestor = 0; for (uint32_t i = 0; i < max && oldPath[i]->OwnerDoc() == aNewPath[i]->OwnerDoc(); i++) { lastCommonImportAncestor = i; } MOZ_ASSERT(lastCommonImportAncestor < max); nsINode* oldLink = oldPath[lastCommonImportAncestor]; nsINode* newLink = aNewPath[lastCommonImportAncestor]; if ((lastCommonImportAncestor == max - 1) && newLink == oldLink ) { // If one chain contains the other entirely, then this is a simple cycle, // nothing to be done here. MOZ_ASSERT(oldPath.Length() != aNewPath.Length(), "This would mean that new link == main referrer link"); return false; } MOZ_ASSERT(aNewPath != oldPath, "How could this happen?"); nsIDocument* doc = oldLink->OwnerDoc(); MOZ_ASSERT(doc->HasSubImportLink(newLink)); MOZ_ASSERT(doc->HasSubImportLink(oldLink)); return doc->IndexOfSubImportLink(newLink) < doc->IndexOfSubImportLink(oldLink); } void ImportLoader::Updater::UpdateMainReferrer(uint32_t aNewIdx) { MOZ_ASSERT(aNewIdx < mLoader->mLinks.Length()); nsINode* newMainReferrer = mLoader->mLinks[aNewIdx]; // This new link means we have to execute our scripts sooner... // Let's make sure that unblocking a loader does not trigger a script execution. // So we start with placing the new blockers and only then will we remove any // blockers. if (mLoader->IsBlocking()) { // Our import parent is changed, let's block the new one and later unblock // the old one. newMainReferrer->OwnerDoc()->ScriptLoader()->AddExecuteBlocker(); newMainReferrer->OwnerDoc()->BlockDOMContentLoaded(); } if (mLoader->mDocument) { // Our nearest predecessor has changed. So let's add the ScriptLoader to the // new one if there is any. And remove it from the old one. nsRefPtr manager = mLoader->Manager(); nsScriptLoader* loader = mLoader->mDocument->ScriptLoader(); ImportLoader*& pred = mLoader->mBlockingPredecessor; ImportLoader* newPred = manager->GetNearestPredecessor(newMainReferrer); if (pred) { if (newPred) { newPred->AddBlockedScriptLoader(loader); } pred->RemoveBlockedScriptLoader(loader); } } if (mLoader->IsBlocking()) { mLoader->mImportParent->ScriptLoader()->RemoveExecuteBlocker(); mLoader->mImportParent->UnblockDOMContentLoaded(); } // Finally update mMainReferrer to point to the newly added link. mLoader->mMainReferrer = aNewIdx; mLoader->mImportParent = newMainReferrer->OwnerDoc(); } nsINode* ImportLoader::Updater::NextDependant(nsINode* aCurrentLink, nsTArray& aPath, NodeTable& aVisitedNodes, bool aSkipChildren) { // Depth first graph traversal. if (!aSkipChildren) { // "first child" ImportLoader* loader = mLoader->Manager()->Find(aCurrentLink); if (loader && loader->GetDocument()) { nsINode* firstSubImport = loader->GetDocument()->GetSubImportLink(0); if (firstSubImport && !aVisitedNodes.Contains(firstSubImport)) { aPath.AppendElement(aCurrentLink); aVisitedNodes.PutEntry(firstSubImport); return firstSubImport; } } } aPath.AppendElement(aCurrentLink); // "(parent's) next sibling" while(aPath.Length() > 1) { aCurrentLink = aPath[aPath.Length() - 1]; aPath.RemoveElementAt(aPath.Length() - 1); // Let's find the next "sibling" ImportLoader* loader = mLoader->Manager()->Find(aCurrentLink->OwnerDoc()); MOZ_ASSERT(loader && loader->GetDocument(), "How can this happend?"); nsIDocument* doc = loader->GetDocument(); MOZ_ASSERT(doc->HasSubImportLink(aCurrentLink)); uint32_t idx = doc->IndexOfSubImportLink(aCurrentLink); nsINode* next = doc->GetSubImportLink(idx + 1); if (next) { // Note: If we found an already visited link that means the parent links has // closed the circle it's always the "first child" section that should find // the first already visited node. Let's just assert that. MOZ_ASSERT(!aVisitedNodes.Contains(next)); aVisitedNodes.PutEntry(next); return next; } } return nullptr; } void ImportLoader::Updater::UpdateDependants(nsINode* aNode, nsTArray& aPath) { NodeTable visitedNodes; nsINode* current = aNode; uint32_t initialLength = aPath.Length(); bool neededUpdate = true; while ((current = NextDependant(current, aPath, visitedNodes, !neededUpdate))) { if (!current || aPath.Length() <= initialLength) { break; } ImportLoader* loader = mLoader->Manager()->Find(current); if (!loader) { continue; } Updater& updater = loader->mUpdater; neededUpdate = updater.ShouldUpdate(aPath); if (neededUpdate) { updater.UpdateMainReferrer(loader->mLinks.IndexOf(current)); } } } void ImportLoader::Updater::UpdateSpanningTree(nsINode* aNode) { if (mLoader->mReady || mLoader->mStopped) { // Scripts already executed, nothing to be done here. return; } if (mLoader->mLinks.Length() == 1) { // If this is the first referrer, let's mark it. mLoader->mMainReferrer = 0; return; } nsTArray newReferrerChain; GetReferrerChain(aNode, newReferrerChain); if (ShouldUpdate(newReferrerChain)) { UpdateMainReferrer(mLoader->mLinks.Length() - 1); UpdateDependants(aNode, newReferrerChain); } } //----------------------------------------------------------------------------- // ImportLoader //----------------------------------------------------------------------------- NS_INTERFACE_MAP_BEGIN(ImportLoader) NS_INTERFACE_MAP_ENTRY(nsIStreamListener) NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(ImportLoader) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(ImportLoader) NS_IMPL_CYCLE_COLLECTING_RELEASE(ImportLoader) NS_IMPL_CYCLE_COLLECTION(ImportLoader, mDocument, mImportParent, mLinks) ImportLoader::ImportLoader(nsIURI* aURI, nsIDocument* aImportParent) : mURI(aURI) , mImportParent(aImportParent) , mBlockingPredecessor(nullptr) , mReady(false) , mStopped(false) , mBlockingScripts(false) , mUpdater(this) { } void ImportLoader::BlockScripts() { MOZ_ASSERT(!mBlockingScripts); mImportParent->ScriptLoader()->AddExecuteBlocker(); mImportParent->BlockDOMContentLoaded(); mBlockingScripts = true; } void ImportLoader::UnblockScripts() { MOZ_ASSERT(mBlockingScripts); mImportParent->ScriptLoader()->RemoveExecuteBlocker(); mImportParent->UnblockDOMContentLoaded(); for (uint32_t i = 0; i < mBlockedScriptLoaders.Length(); i++) { mBlockedScriptLoaders[i]->RemoveExecuteBlocker(); } mBlockedScriptLoaders.Clear(); mBlockingScripts = false; } void ImportLoader::SetBlockingPredecessor(ImportLoader* aLoader) { mBlockingPredecessor = aLoader; } void ImportLoader::DispatchEventIfFinished(nsINode* aNode) { MOZ_ASSERT(!(mReady && mStopped)); if (mReady) { DispatchLoadEvent(aNode); } if (mStopped) { DispatchErrorEvent(aNode); } } void ImportLoader::AddBlockedScriptLoader(nsScriptLoader* aScriptLoader) { if (mBlockedScriptLoaders.Contains(aScriptLoader)) { return; } aScriptLoader->AddExecuteBlocker(); // Let's keep track of the pending script loaders. mBlockedScriptLoaders.AppendElement(aScriptLoader); } bool ImportLoader::RemoveBlockedScriptLoader(nsScriptLoader* aScriptLoader) { aScriptLoader->RemoveExecuteBlocker(); return mBlockedScriptLoaders.RemoveElement(aScriptLoader); } void ImportLoader::AddLinkElement(nsINode* aNode) { // If a new link element is added to the import tree that // refers to an import that is already finished loading or // stopped trying, we need to fire the corresponding event // on it. mLinks.AppendElement(aNode); mUpdater.UpdateSpanningTree(aNode); DispatchEventIfFinished(aNode); } void ImportLoader::RemoveLinkElement(nsINode* aNode) { mLinks.RemoveElement(aNode); } // Events has to be fired with a script runner, so mImport can // be set on the link element before the load event is fired even // if ImportLoader::Get returns an already loaded import and we // fire the load event immediately on the new referring link element. class AsyncEvent : public nsRunnable { public: AsyncEvent(nsINode* aNode, bool aSuccess) : mNode(aNode) , mSuccess(aSuccess) { MOZ_ASSERT(mNode); } NS_IMETHOD Run() { return nsContentUtils::DispatchTrustedEvent(mNode->OwnerDoc(), mNode, mSuccess ? NS_LITERAL_STRING("load") : NS_LITERAL_STRING("error"), /* aCanBubble = */ false, /* aCancelable = */ false); } private: nsCOMPtr mNode; bool mSuccess; }; void ImportLoader::DispatchErrorEvent(nsINode* aNode) { nsContentUtils::AddScriptRunner(new AsyncEvent(aNode, /* aSuccess = */ false)); } void ImportLoader::DispatchLoadEvent(nsINode* aNode) { nsContentUtils::AddScriptRunner(new AsyncEvent(aNode, /* aSuccess = */ true)); } void ImportLoader::Done() { mReady = true; uint32_t l = mLinks.Length(); for (uint32_t i = 0; i < l; i++) { DispatchLoadEvent(mLinks[i]); } UnblockScripts(); ReleaseResources(); } void ImportLoader::Error(bool aUnblockScripts) { mDocument = nullptr; mStopped = true; uint32_t l = mLinks.Length(); for (uint32_t i = 0; i < l; i++) { DispatchErrorEvent(mLinks[i]); } if (aUnblockScripts) { UnblockScripts(); } ReleaseResources(); } // Release all the resources we don't need after there is no more // data available on the channel, and the parser is done. void ImportLoader::ReleaseResources() { mParserStreamListener = nullptr; mImportParent = nullptr; } nsIPrincipal* ImportLoader::Principal() { MOZ_ASSERT(mImportParent); nsCOMPtr master = mImportParent->MasterDocument(); nsCOMPtr sop = do_QueryInterface(master); MOZ_ASSERT(sop); return sop->GetPrincipal(); } void ImportLoader::Open() { AutoError ae(this, false); // Imports should obey to the master documents CSP. nsCOMPtr master = mImportParent->MasterDocument(); nsIPrincipal* principal = Principal(); int16_t shouldLoad = nsIContentPolicy::ACCEPT; nsresult rv = NS_CheckContentLoadPolicy(nsIContentPolicy::TYPE_SUBDOCUMENT, mURI, principal, mImportParent, NS_LITERAL_CSTRING("text/html"), /* extra = */ nullptr, &shouldLoad, nsContentUtils::GetContentPolicy(), nsContentUtils::GetSecurityManager()); if (NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad)) { NS_WARN_IF_FALSE(NS_CP_ACCEPTED(shouldLoad), "ImportLoader rejected by CSP"); return; } nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager(); rv = secMan->CheckLoadURIWithPrincipal(principal, mURI, nsIScriptSecurityManager::STANDARD); NS_ENSURE_SUCCESS_VOID(rv); nsCOMPtr loadGroup = master->GetDocumentLoadGroup(); nsCOMPtr channel; rv = NS_NewChannel(getter_AddRefs(channel), mURI, mImportParent, nsILoadInfo::SEC_NORMAL, nsIContentPolicy::TYPE_SUBDOCUMENT, loadGroup, nullptr, // aCallbacks nsIRequest::LOAD_BACKGROUND); NS_ENSURE_SUCCESS_VOID(rv); // Init CORSListenerProxy and omit credentials. nsRefPtr corsListener = new nsCORSListenerProxy(this, principal, /* aWithCredentials */ false); rv = corsListener->Init(channel, true); NS_ENSURE_SUCCESS_VOID(rv); rv = channel->AsyncOpen(corsListener, nullptr); NS_ENSURE_SUCCESS_VOID(rv); BlockScripts(); ae.Pass(); } NS_IMETHODIMP ImportLoader::OnDataAvailable(nsIRequest* aRequest, nsISupports* aContext, nsIInputStream* aStream, uint64_t aOffset, uint32_t aCount) { MOZ_ASSERT(mParserStreamListener); AutoError ae(this); nsresult rv; nsCOMPtr channel = do_QueryInterface(aRequest, &rv); NS_ENSURE_SUCCESS(rv, rv); rv = mParserStreamListener->OnDataAvailable(channel, aContext, aStream, aOffset, aCount); NS_ENSURE_SUCCESS(rv, rv); ae.Pass(); return rv; } NS_IMETHODIMP ImportLoader::HandleEvent(nsIDOMEvent *aEvent) { #ifdef DEBUG nsAutoString type; aEvent->GetType(type); MOZ_ASSERT(type.EqualsLiteral("DOMContentLoaded")); #endif Done(); return NS_OK; } NS_IMETHODIMP ImportLoader::OnStopRequest(nsIRequest* aRequest, nsISupports* aContext, nsresult aStatus) { // OnStartRequest throws a special error code to let us know that we // shouldn't do anything else. if (aStatus == NS_ERROR_DOM_ABORT_ERR) { // We failed in OnStartRequest, nothing more to do (we've already // dispatched an error event) just return here. MOZ_ASSERT(mStopped); return NS_OK; } if (mParserStreamListener) { mParserStreamListener->OnStopRequest(aRequest, aContext, aStatus); } if (!mDocument) { // If at this point we don't have a document, then the error was // already reported. return NS_ERROR_DOM_ABORT_ERR; } nsCOMPtr eventTarget = do_QueryInterface(mDocument); EventListenerManager* manager = eventTarget->GetOrCreateListenerManager(); manager->AddEventListenerByType(this, NS_LITERAL_STRING("DOMContentLoaded"), TrustedEventsAtSystemGroupBubble()); return NS_OK; } NS_IMETHODIMP ImportLoader::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext) { AutoError ae(this); nsIPrincipal* principal = Principal(); nsCOMPtr channel = do_QueryInterface(aRequest); if (!channel) { return NS_ERROR_DOM_ABORT_ERR; } if (nsContentUtils::IsSystemPrincipal(principal)) { // We should never import non-system documents and run their scripts with system principal! nsCOMPtr channelPrincipal; nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal(channel, getter_AddRefs(channelPrincipal)); if (!nsContentUtils::IsSystemPrincipal(channelPrincipal)) { return NS_ERROR_FAILURE; } } channel->SetOwner(principal); nsAutoCString type; channel->GetContentType(type); if (!type.EqualsLiteral("text/html")) { NS_WARNING("ImportLoader wrong content type"); return NS_ERROR_DOM_ABORT_ERR; } // The scope object is same for all the imports in an import tree, // let's get it form the import parent. nsCOMPtr global = mImportParent->GetScopeObject(); nsCOMPtr importDoc; nsCOMPtr baseURI = mImportParent->GetBaseURI(); const nsAString& emptyStr = EmptyString(); nsresult rv = NS_NewDOMDocument(getter_AddRefs(importDoc), emptyStr, emptyStr, nullptr, mURI, baseURI, principal, false, global, DocumentFlavorHTML); NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_ABORT_ERR); // The imported document must know which master document it belongs to. mDocument = do_QueryInterface(importDoc); nsCOMPtr master = mImportParent->MasterDocument(); mDocument->SetMasterDocument(master); // We want to inherit the sandbox flags from the master document. mDocument->SetSandboxFlags(master->GetSandboxFlags()); // We have to connect the blank document we created with the channel we opened, // and create its own LoadGroup for it. nsCOMPtr listener; nsCOMPtr loadGroup; channel->GetLoadGroup(getter_AddRefs(loadGroup)); nsCOMPtr newLoadGroup = do_CreateInstance(NS_LOADGROUP_CONTRACTID); NS_ENSURE_TRUE(newLoadGroup, NS_ERROR_OUT_OF_MEMORY); newLoadGroup->SetLoadGroup(loadGroup); rv = mDocument->StartDocumentLoad("import", channel, newLoadGroup, nullptr, getter_AddRefs(listener), true); NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_ABORT_ERR); nsCOMPtr originalURI; rv = channel->GetOriginalURI(getter_AddRefs(originalURI)); NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_ABORT_ERR); nsCOMPtr URI; rv = channel->GetURI(getter_AddRefs(URI)); NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_ABORT_ERR); MOZ_ASSERT(URI, "URI of a channel should never be null"); bool equals; rv = URI->Equals(originalURI, &equals); NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_ABORT_ERR); if (!equals) { // In case of a redirection we must add the new URI to the import map. Manager()->AddLoaderWithNewURI(this, URI); } // Let's start the parser. mParserStreamListener = listener; rv = listener->OnStartRequest(aRequest, aContext); NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_ABORT_ERR); ae.Pass(); return NS_OK; } //----------------------------------------------------------------------------- // ImportManager //----------------------------------------------------------------------------- NS_IMPL_CYCLE_COLLECTION(ImportManager, mImports) NS_INTERFACE_MAP_BEGIN(ImportManager) NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(ImportManager) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTING_ADDREF(ImportManager) NS_IMPL_CYCLE_COLLECTING_RELEASE(ImportManager) already_AddRefed ImportManager::Get(nsIURI* aURI, nsINode* aNode, nsIDocument* aOrigDocument) { // Check if we have a loader for that URI, if not create one, // and start it up. nsRefPtr loader; mImports.Get(aURI, getter_AddRefs(loader)); bool needToStart = false; if (!loader) { loader = new ImportLoader(aURI, aOrigDocument); mImports.Put(aURI, loader); needToStart = true; } MOZ_ASSERT(loader); // Let's keep track of the sub imports links in each document. It will // be used later for scrip execution order calculation. (see UpdateSpanningTree) // NOTE: removing and adding back the link to the tree somewhere else will // NOT have an effect on script execution order. if (!aOrigDocument->HasSubImportLink(aNode)) { aOrigDocument->AddSubImportLink(aNode); } loader->AddLinkElement(aNode); if (needToStart) { loader->Open(); } return loader.forget(); } ImportLoader* ImportManager::Find(nsIDocument* aImport) { return mImports.GetWeak(aImport->GetDocumentURIObject()); } ImportLoader* ImportManager::Find(nsINode* aLink) { HTMLLinkElement* linkElement = static_cast(aLink); nsCOMPtr uri = linkElement->GetHrefURI(); return mImports.GetWeak(uri); } void ImportManager::AddLoaderWithNewURI(ImportLoader* aLoader, nsIURI* aNewURI) { mImports.Put(aNewURI, aLoader); } nsRefPtr ImportManager::GetNearestPredecessor(nsINode* aNode) { // Return the previous link if there is any in the same document. nsIDocument* doc = aNode->OwnerDoc(); int32_t idx = doc->IndexOfSubImportLink(aNode); MOZ_ASSERT(idx != -1, "aNode must be a sub import link of its owner document"); for (; idx > 0; idx--) { HTMLLinkElement* link = static_cast(doc->GetSubImportLink(idx - 1)); nsCOMPtr uri = link->GetHrefURI(); nsRefPtr ret; mImports.Get(uri, getter_AddRefs(ret)); // Only main referrer links are interesting. if (ret->GetMainReferrer() == link) { return ret; } } if (idx == 0) { if (doc->IsMasterDocument()) { // If there is no previous one, and it was the master document, then // there is no predecessor. return nullptr; } // Else we find the main referrer of the import parent of the link's document. // And do a recursion. ImportLoader* owner = Find(doc); MOZ_ASSERT(owner); nsCOMPtr mainReferrer = owner->GetMainReferrer(); return GetNearestPredecessor(mainReferrer); } return nullptr; } } // namespace dom } // namespace mozilla