diff options
Diffstat (limited to 'dom/workers/ServiceWorkerScriptCache.cpp')
-rw-r--r-- | dom/workers/ServiceWorkerScriptCache.cpp | 1060 |
1 files changed, 1060 insertions, 0 deletions
diff --git a/dom/workers/ServiceWorkerScriptCache.cpp b/dom/workers/ServiceWorkerScriptCache.cpp new file mode 100644 index 000000000..f44bb673c --- /dev/null +++ b/dom/workers/ServiceWorkerScriptCache.cpp @@ -0,0 +1,1060 @@ +/* -*- 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 "ServiceWorkerScriptCache.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/CacheBinding.h" +#include "mozilla/dom/cache/CacheStorage.h" +#include "mozilla/dom/cache/Cache.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseWorkerProxy.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "nsICacheInfoChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIStreamLoader.h" +#include "nsIThreadRetargetableRequest.h" + +#include "nsIInputStreamPump.h" +#include "nsIPrincipal.h" +#include "nsIScriptError.h" +#include "nsContentUtils.h" +#include "nsNetUtil.h" +#include "nsScriptLoader.h" +#include "ServiceWorkerManager.h" +#include "Workers.h" +#include "nsStringStream.h" + +using mozilla::dom::cache::Cache; +using mozilla::dom::cache::CacheStorage; + +BEGIN_WORKERS_NAMESPACE + +namespace serviceWorkerScriptCache { + +namespace { + +// XXX A sandbox nsIGlobalObject does not preserve its reflector, so |aSandbox| +// must be kept alive as long as the CacheStorage if you want to ensure that +// the CacheStorage will continue to work. Failures will manifest as errors +// like "JavaScript error: , line 0: TypeError: The expression cannot be +// converted to return the specified type." +already_AddRefed<CacheStorage> +CreateCacheStorage(JSContext* aCx, nsIPrincipal* aPrincipal, ErrorResult& aRv, + JS::MutableHandle<JSObject*> aSandbox) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + aRv = xpc->CreateSandbox(aCx, aPrincipal, aSandbox.address()); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + nsCOMPtr<nsIGlobalObject> sandboxGlobalObject = xpc::NativeGlobal(aSandbox); + if (!sandboxGlobalObject) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + // We assume private browsing is not enabled here. The ScriptLoader + // explicitly fails for private browsing so there should never be + // a service worker running in private browsing mode. Therefore if + // we are purging scripts or running a comparison algorithm we cannot + // be in private browing. + // + // Also, bypass the CacheStorage trusted origin checks. The ServiceWorker + // has validated the origin prior to this point. All the information + // to revalidate is not available now. + return CacheStorage::CreateOnMainThread(cache::CHROME_ONLY_NAMESPACE, + sandboxGlobalObject, aPrincipal, + false /* private browsing */, + true /* force trusted origin */, + aRv); +} + +class CompareManager; + +// This class downloads a URL from the network and then it calls +// NetworkFinished() in the CompareManager. +class CompareNetwork final : public nsIStreamLoaderObserver, + public nsIRequestObserver +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLOADEROBSERVER + NS_DECL_NSIREQUESTOBSERVER + + explicit CompareNetwork(CompareManager* aManager) + : mManager(aManager) + { + MOZ_ASSERT(aManager); + AssertIsOnMainThread(); + } + + nsresult + Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, nsILoadGroup* aLoadGroup); + + void + Abort() + { + AssertIsOnMainThread(); + + MOZ_ASSERT(mChannel); + mChannel->Cancel(NS_BINDING_ABORTED); + mChannel = nullptr; + } + + const nsString& Buffer() const + { + AssertIsOnMainThread(); + return mBuffer; + } + +private: + ~CompareNetwork() + { + AssertIsOnMainThread(); + } + + RefPtr<CompareManager> mManager; + nsCOMPtr<nsIChannel> mChannel; + nsString mBuffer; +}; + +NS_IMPL_ISUPPORTS(CompareNetwork, nsIStreamLoaderObserver, + nsIRequestObserver) + +// This class gets a cached Response from the CacheStorage and then it calls +// CacheFinished() in the CompareManager. +class CompareCache final : public PromiseNativeHandler + , public nsIStreamLoaderObserver +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLOADEROBSERVER + + explicit CompareCache(CompareManager* aManager) + : mManager(aManager) + , mState(WaitingForCache) + , mAborted(false) + { + MOZ_ASSERT(aManager); + AssertIsOnMainThread(); + } + + nsresult + Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, + const nsAString& aCacheName); + + void + Abort() + { + AssertIsOnMainThread(); + + MOZ_ASSERT(!mAborted); + mAborted = true; + + if (mPump) { + mPump->Cancel(NS_BINDING_ABORTED); + mPump = nullptr; + } + } + + // This class manages 2 promises: 1 is to retrieve cache object, and 2 is for + // the value from the cache. For this reason we have mState to know what + // reject/resolve callback we are handling. + + virtual void + ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + AssertIsOnMainThread(); + + if (mAborted) { + return; + } + + if (mState == WaitingForCache) { + ManageCacheResult(aCx, aValue); + return; + } + + MOZ_ASSERT(mState == WaitingForValue); + ManageValueResult(aCx, aValue); + } + + virtual void + RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; + + const nsString& Buffer() const + { + AssertIsOnMainThread(); + return mBuffer; + } + + const nsString& URL() const + { + AssertIsOnMainThread(); + return mURL; + } + +private: + ~CompareCache() + { + AssertIsOnMainThread(); + } + + void + ManageCacheResult(JSContext* aCx, JS::Handle<JS::Value> aValue); + + void + ManageValueResult(JSContext* aCx, JS::Handle<JS::Value> aValue); + + RefPtr<CompareManager> mManager; + nsCOMPtr<nsIInputStreamPump> mPump; + + nsString mURL; + nsString mBuffer; + + enum { + WaitingForCache, + WaitingForValue + } mState; + + bool mAborted; +}; + +NS_IMPL_ISUPPORTS(CompareCache, nsIStreamLoaderObserver) + +class CompareManager final : public PromiseNativeHandler +{ +public: + NS_DECL_ISUPPORTS + + explicit CompareManager(ServiceWorkerRegistrationInfo* aRegistration, + CompareCallback* aCallback) + : mRegistration(aRegistration) + , mCallback(aCallback) + , mState(WaitingForOpen) + , mNetworkFinished(false) + , mCacheFinished(false) + , mInCache(false) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aRegistration); + } + + nsresult + Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, + const nsAString& aCacheName, nsILoadGroup* aLoadGroup) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + + mURL = aURL; + + // Always create a CacheStorage since we want to write the network entry to + // the cache even if there isn't an existing one. + AutoJSAPI jsapi; + jsapi.Init(); + ErrorResult result; + mSandbox.init(jsapi.cx()); + mCacheStorage = CreateCacheStorage(jsapi.cx(), aPrincipal, result, &mSandbox); + if (NS_WARN_IF(result.Failed())) { + MOZ_ASSERT(!result.IsErrorWithMessage()); + Cleanup(); + return result.StealNSResult(); + } + + mCN = new CompareNetwork(this); + nsresult rv = mCN->Initialize(aPrincipal, aURL, aLoadGroup); + if (NS_WARN_IF(NS_FAILED(rv))) { + Cleanup(); + return rv; + } + + if (!aCacheName.IsEmpty()) { + mCC = new CompareCache(this); + rv = mCC->Initialize(aPrincipal, aURL, aCacheName); + if (NS_WARN_IF(NS_FAILED(rv))) { + mCN->Abort(); + Cleanup(); + return rv; + } + } + + return NS_OK; + } + + const nsString& + URL() const + { + AssertIsOnMainThread(); + return mURL; + } + + void + SetMaxScope(const nsACString& aMaxScope) + { + MOZ_ASSERT(!mNetworkFinished); + mMaxScope = aMaxScope; + } + + already_AddRefed<ServiceWorkerRegistrationInfo> + GetRegistration() + { + RefPtr<ServiceWorkerRegistrationInfo> copy = mRegistration.get(); + return copy.forget(); + } + + void + NetworkFinished(nsresult aStatus) + { + AssertIsOnMainThread(); + + mNetworkFinished = true; + + if (NS_WARN_IF(NS_FAILED(aStatus))) { + if (mCC) { + mCC->Abort(); + } + + ComparisonFinished(aStatus, false); + return; + } + + MaybeCompare(); + } + + void + CacheFinished(nsresult aStatus, bool aInCache) + { + AssertIsOnMainThread(); + + mCacheFinished = true; + mInCache = aInCache; + + if (NS_WARN_IF(NS_FAILED(aStatus))) { + if (mCN) { + mCN->Abort(); + } + + ComparisonFinished(aStatus, false); + return; + } + + MaybeCompare(); + } + + void + MaybeCompare() + { + AssertIsOnMainThread(); + + if (!mNetworkFinished || (mCC && !mCacheFinished)) { + return; + } + + if (!mCC || !mInCache) { + ComparisonFinished(NS_OK, false); + return; + } + + ComparisonFinished(NS_OK, mCC->Buffer().Equals(mCN->Buffer())); + } + + // This class manages 2 promises: 1 is to retrieve Cache object, and 2 is to + // Put the value in the cache. For this reason we have mState to know what + // callback we are handling. + void + ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + AssertIsOnMainThread(); + MOZ_ASSERT(mCallback); + + if (mState == WaitingForOpen) { + if (NS_WARN_IF(!aValue.isObject())) { + Fail(NS_ERROR_FAILURE); + return; + } + + JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); + if (NS_WARN_IF(!obj)) { + Fail(NS_ERROR_FAILURE); + return; + } + + RefPtr<Cache> cache; + nsresult rv = UNWRAP_OBJECT(Cache, obj, cache); + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(rv); + return; + } + + WriteToCache(cache); + return; + } + + MOZ_ASSERT(mState == WaitingForPut); + mCallback->ComparisonResult(NS_OK, false /* aIsEqual */, + mNewCacheName, mMaxScope); + Cleanup(); + } + + void + RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + AssertIsOnMainThread(); + if (mState == WaitingForOpen) { + NS_WARNING("Could not open cache."); + } else { + NS_WARNING("Could not write to cache."); + } + Fail(NS_ERROR_FAILURE); + } + + CacheStorage* + CacheStorage_() + { + AssertIsOnMainThread(); + MOZ_ASSERT(mCacheStorage); + return mCacheStorage; + } + + void + InitChannelInfo(nsIChannel* aChannel) + { + mChannelInfo.InitFromChannel(aChannel); + } + + nsresult + SetPrincipalInfo(nsIChannel* aChannel) + { + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + NS_ASSERTION(ssm, "Should never be null!"); + + nsCOMPtr<nsIPrincipal> channelPrincipal; + nsresult rv = ssm->GetChannelResultPrincipal(aChannel, getter_AddRefs(channelPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + UniquePtr<mozilla::ipc::PrincipalInfo> principalInfo(new mozilla::ipc::PrincipalInfo()); + rv = PrincipalToPrincipalInfo(channelPrincipal, principalInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mPrincipalInfo = Move(principalInfo); + return NS_OK; + } + +private: + ~CompareManager() + { + AssertIsOnMainThread(); + MOZ_ASSERT(!mCC); + MOZ_ASSERT(!mCN); + } + + void + Fail(nsresult aStatus) + { + AssertIsOnMainThread(); + mCallback->ComparisonResult(aStatus, false /* aIsEqual */, + EmptyString(), EmptyCString()); + Cleanup(); + } + + void + Cleanup() + { + AssertIsOnMainThread(); + MOZ_ASSERT(mCallback); + mCallback = nullptr; + mCN = nullptr; + mCC = nullptr; + } + + void + ComparisonFinished(nsresult aStatus, bool aIsEqual) + { + AssertIsOnMainThread(); + MOZ_ASSERT(mCallback); + + if (NS_WARN_IF(NS_FAILED(aStatus))) { + Fail(aStatus); + return; + } + + if (aIsEqual) { + mCallback->ComparisonResult(aStatus, aIsEqual, EmptyString(), mMaxScope); + Cleanup(); + return; + } + + // Write to Cache so ScriptLoader reads succeed. + WriteNetworkBufferToNewCache(); + } + + void + WriteNetworkBufferToNewCache() + { + AssertIsOnMainThread(); + MOZ_ASSERT(mCN); + MOZ_ASSERT(mCacheStorage); + MOZ_ASSERT(mNewCacheName.IsEmpty()); + + ErrorResult result; + result = serviceWorkerScriptCache::GenerateCacheName(mNewCacheName); + if (NS_WARN_IF(result.Failed())) { + MOZ_ASSERT(!result.IsErrorWithMessage()); + Fail(result.StealNSResult()); + return; + } + + RefPtr<Promise> cacheOpenPromise = mCacheStorage->Open(mNewCacheName, result); + if (NS_WARN_IF(result.Failed())) { + MOZ_ASSERT(!result.IsErrorWithMessage()); + Fail(result.StealNSResult()); + return; + } + + cacheOpenPromise->AppendNativeHandler(this); + } + + void + WriteToCache(Cache* aCache) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aCache); + MOZ_ASSERT(mState == WaitingForOpen); + + ErrorResult result; + nsCOMPtr<nsIInputStream> body; + result = NS_NewCStringInputStream(getter_AddRefs(body), + NS_ConvertUTF16toUTF8(mCN->Buffer())); + if (NS_WARN_IF(result.Failed())) { + MOZ_ASSERT(!result.IsErrorWithMessage()); + Fail(result.StealNSResult()); + return; + } + + RefPtr<InternalResponse> ir = + new InternalResponse(200, NS_LITERAL_CSTRING("OK")); + ir->SetBody(body, mCN->Buffer().Length()); + + ir->InitChannelInfo(mChannelInfo); + if (mPrincipalInfo) { + ir->SetPrincipalInfo(Move(mPrincipalInfo)); + } + + RefPtr<Response> response = new Response(aCache->GetGlobalObject(), ir); + + RequestOrUSVString request; + request.SetAsUSVString().Rebind(URL().Data(), URL().Length()); + + // For now we have to wait until the Put Promise is fulfilled before we can + // continue since Cache does not yet support starting a read that is being + // written to. + RefPtr<Promise> cachePromise = aCache->Put(request, *response, result); + if (NS_WARN_IF(result.Failed())) { + MOZ_ASSERT(!result.IsErrorWithMessage()); + Fail(result.StealNSResult()); + return; + } + + mState = WaitingForPut; + cachePromise->AppendNativeHandler(this); + } + + RefPtr<ServiceWorkerRegistrationInfo> mRegistration; + RefPtr<CompareCallback> mCallback; + JS::PersistentRooted<JSObject*> mSandbox; + RefPtr<CacheStorage> mCacheStorage; + + RefPtr<CompareNetwork> mCN; + RefPtr<CompareCache> mCC; + + nsString mURL; + // Only used if the network script has changed and needs to be cached. + nsString mNewCacheName; + + ChannelInfo mChannelInfo; + + UniquePtr<mozilla::ipc::PrincipalInfo> mPrincipalInfo; + + nsCString mMaxScope; + + enum { + WaitingForOpen, + WaitingForPut + } mState; + + bool mNetworkFinished; + bool mCacheFinished; + bool mInCache; +}; + +NS_IMPL_ISUPPORTS0(CompareManager) + +nsresult +CompareNetwork::Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, nsILoadGroup* aLoadGroup) +{ + MOZ_ASSERT(aPrincipal); + AssertIsOnMainThread(); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, nullptr, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsILoadGroup> loadGroup; + rv = NS_NewLoadGroup(getter_AddRefs(loadGroup), aPrincipal); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsLoadFlags flags = nsIChannel::LOAD_BYPASS_SERVICE_WORKER; + RefPtr<ServiceWorkerRegistrationInfo> registration = + mManager->GetRegistration(); + if (registration->IsLastUpdateCheckTimeOverOneDay()) { + flags |= nsIRequest::LOAD_BYPASS_CACHE; + } + + // Note that because there is no "serviceworker" RequestContext type, we can + // use the TYPE_INTERNAL_SCRIPT content policy types when loading a service + // worker. + rv = NS_NewChannel(getter_AddRefs(mChannel), + uri, aPrincipal, + nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED, + nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER, + loadGroup, + nullptr, // aCallbacks + flags); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel); + if (httpChannel) { + // Spec says no redirects allowed for SW scripts. + httpChannel->SetRedirectionLimit(0); + + httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("Service-Worker"), + NS_LITERAL_CSTRING("script"), + /* merge */ false); + } + + nsCOMPtr<nsIStreamLoader> loader; + rv = NS_NewStreamLoader(getter_AddRefs(loader), this, this); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mChannel->AsyncOpen2(loader); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +CompareNetwork::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext) +{ + AssertIsOnMainThread(); + + // If no channel, Abort() has been called. + if (!mChannel) { + return NS_OK; + } + +#ifdef DEBUG + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + MOZ_ASSERT(channel == mChannel); +#endif + + mManager->InitChannelInfo(mChannel); + nsresult rv = mManager->SetPrincipalInfo(mChannel); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +CompareNetwork::OnStopRequest(nsIRequest* aRequest, nsISupports* aContext, + nsresult aStatusCode) +{ + // Nothing to do here! + return NS_OK; +} + +NS_IMETHODIMP +CompareNetwork::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, + nsresult aStatus, uint32_t aLen, + const uint8_t* aString) +{ + AssertIsOnMainThread(); + + // If no channel, Abort() has been called. + if (!mChannel) { + return NS_OK; + } + + if (NS_WARN_IF(NS_FAILED(aStatus))) { + if (aStatus == NS_ERROR_REDIRECT_LOOP) { + mManager->NetworkFinished(NS_ERROR_DOM_SECURITY_ERR); + } else { + mManager->NetworkFinished(aStatus); + } + return NS_OK; + } + + nsCOMPtr<nsIRequest> request; + nsresult rv = aLoader->GetRequest(getter_AddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + mManager->NetworkFinished(rv); + return NS_OK; + } + + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(request); + MOZ_ASSERT(httpChannel, "How come we don't have an HTTP channel?"); + + bool requestSucceeded; + rv = httpChannel->GetRequestSucceeded(&requestSucceeded); + if (NS_WARN_IF(NS_FAILED(rv))) { + mManager->NetworkFinished(rv); + return NS_OK; + } + + if (NS_WARN_IF(!requestSucceeded)) { + // Get the stringified numeric status code, not statusText which could be + // something misleading like OK for a 404. + uint32_t status = 0; + httpChannel->GetResponseStatus(&status); // don't care if this fails, use 0. + nsAutoString statusAsText; + statusAsText.AppendInt(status); + + RefPtr<ServiceWorkerRegistrationInfo> registration = mManager->GetRegistration(); + ServiceWorkerManager::LocalizeAndReportToAllClients( + registration->mScope, "ServiceWorkerRegisterNetworkError", + nsTArray<nsString> { NS_ConvertUTF8toUTF16(registration->mScope), + statusAsText, mManager->URL() }); + mManager->NetworkFinished(NS_ERROR_FAILURE); + return NS_OK; + } + + nsAutoCString maxScope; + // Note: we explicitly don't check for the return value here, because the + // absence of the header is not an error condition. + Unused << httpChannel->GetResponseHeader(NS_LITERAL_CSTRING("Service-Worker-Allowed"), + maxScope); + + mManager->SetMaxScope(maxScope); + + bool isFromCache = false; + nsCOMPtr<nsICacheInfoChannel> cacheChannel(do_QueryInterface(httpChannel)); + if (cacheChannel) { + cacheChannel->IsFromCache(&isFromCache); + } + + // [9.2 Update]4.13, If response's cache state is not "local", + // set registration's last update check time to the current time + if (!isFromCache) { + RefPtr<ServiceWorkerRegistrationInfo> registration = + mManager->GetRegistration(); + registration->RefreshLastUpdateCheckTime(); + } + + nsAutoCString mimeType; + rv = httpChannel->GetContentType(mimeType); + if (NS_WARN_IF(NS_FAILED(rv))) { + // We should only end up here if !mResponseHead in the channel. If headers + // were received but no content type was specified, we'll be given + // UNKNOWN_CONTENT_TYPE "application/x-unknown-content-type" and so fall + // into the next case with its better error message. + mManager->NetworkFinished(NS_ERROR_DOM_SECURITY_ERR); + return rv; + } + + if (!mimeType.LowerCaseEqualsLiteral("text/javascript") && + !mimeType.LowerCaseEqualsLiteral("application/x-javascript") && + !mimeType.LowerCaseEqualsLiteral("application/javascript")) { + RefPtr<ServiceWorkerRegistrationInfo> registration = mManager->GetRegistration(); + ServiceWorkerManager::LocalizeAndReportToAllClients( + registration->mScope, "ServiceWorkerRegisterMimeTypeError", + nsTArray<nsString> { NS_ConvertUTF8toUTF16(registration->mScope), + NS_ConvertUTF8toUTF16(mimeType), mManager->URL() }); + mManager->NetworkFinished(NS_ERROR_DOM_SECURITY_ERR); + return rv; + } + + char16_t* buffer = nullptr; + size_t len = 0; + + rv = nsScriptLoader::ConvertToUTF16(httpChannel, aString, aLen, + NS_LITERAL_STRING("UTF-8"), nullptr, + buffer, len); + if (NS_WARN_IF(NS_FAILED(rv))) { + mManager->NetworkFinished(rv); + return rv; + } + + mBuffer.Adopt(buffer, len); + + mManager->NetworkFinished(NS_OK); + return NS_OK; +} + +nsresult +CompareCache::Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, + const nsAString& aCacheName) +{ + MOZ_ASSERT(aPrincipal); + AssertIsOnMainThread(); + + mURL = aURL; + + ErrorResult rv; + + RefPtr<Promise> promise = mManager->CacheStorage_()->Open(aCacheName, rv); + if (NS_WARN_IF(rv.Failed())) { + MOZ_ASSERT(!rv.IsErrorWithMessage()); + return rv.StealNSResult(); + } + + promise->AppendNativeHandler(this); + return NS_OK; +} + +NS_IMETHODIMP +CompareCache::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, + nsresult aStatus, uint32_t aLen, + const uint8_t* aString) +{ + AssertIsOnMainThread(); + + if (mAborted) { + return aStatus; + } + + if (NS_WARN_IF(NS_FAILED(aStatus))) { + mManager->CacheFinished(aStatus, false); + return aStatus; + } + + char16_t* buffer = nullptr; + size_t len = 0; + + nsresult rv = nsScriptLoader::ConvertToUTF16(nullptr, aString, aLen, + NS_LITERAL_STRING("UTF-8"), + nullptr, buffer, len); + if (NS_WARN_IF(NS_FAILED(rv))) { + mManager->CacheFinished(rv, false); + return rv; + } + + mBuffer.Adopt(buffer, len); + + mManager->CacheFinished(NS_OK, true); + return NS_OK; +} + +void +CompareCache::RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) +{ + AssertIsOnMainThread(); + + if (mAborted) { + return; + } + + mManager->CacheFinished(NS_ERROR_FAILURE, false); +} + +void +CompareCache::ManageCacheResult(JSContext* aCx, JS::Handle<JS::Value> aValue) +{ + AssertIsOnMainThread(); + + if (NS_WARN_IF(!aValue.isObject())) { + mManager->CacheFinished(NS_ERROR_FAILURE, false); + return; + } + + JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); + + Cache* cache = nullptr; + nsresult rv = UNWRAP_OBJECT(Cache, &obj, cache); + if (NS_WARN_IF(NS_FAILED(rv))) { + mManager->CacheFinished(rv, false); + return; + } + + RequestOrUSVString request; + request.SetAsUSVString().Rebind(mURL.Data(), mURL.Length()); + ErrorResult error; + CacheQueryOptions params; + RefPtr<Promise> promise = cache->Match(request, params, error); + if (NS_WARN_IF(error.Failed())) { + mManager->CacheFinished(error.StealNSResult(), false); + return; + } + + promise->AppendNativeHandler(this); + mState = WaitingForValue; +} + +void +CompareCache::ManageValueResult(JSContext* aCx, JS::Handle<JS::Value> aValue) +{ + AssertIsOnMainThread(); + + // The cache returns undefined if the object is not stored. + if (aValue.isUndefined()) { + mManager->CacheFinished(NS_OK, false); + return; + } + + MOZ_ASSERT(aValue.isObject()); + + JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); + + Response* response = nullptr; + nsresult rv = UNWRAP_OBJECT(Response, &obj, response); + if (NS_WARN_IF(NS_FAILED(rv))) { + mManager->CacheFinished(rv, false); + return; + } + + MOZ_ASSERT(response->Ok()); + + nsCOMPtr<nsIInputStream> inputStream; + response->GetBody(getter_AddRefs(inputStream)); + MOZ_ASSERT(inputStream); + + MOZ_ASSERT(!mPump); + rv = NS_NewInputStreamPump(getter_AddRefs(mPump), inputStream); + if (NS_WARN_IF(NS_FAILED(rv))) { + mManager->CacheFinished(rv, false); + return; + } + + nsCOMPtr<nsIStreamLoader> loader; + rv = NS_NewStreamLoader(getter_AddRefs(loader), this); + if (NS_WARN_IF(NS_FAILED(rv))) { + mManager->CacheFinished(rv, false); + return; + } + + rv = mPump->AsyncRead(loader, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPump = nullptr; + mManager->CacheFinished(rv, false); + return; + } + + nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(mPump); + if (rr) { + nsCOMPtr<nsIEventTarget> sts = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + rv = rr->RetargetDeliveryTo(sts); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPump = nullptr; + mManager->CacheFinished(rv, false); + return; + } + } +} + +} // namespace + +nsresult +PurgeCache(nsIPrincipal* aPrincipal, const nsAString& aCacheName) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + + if (aCacheName.IsEmpty()) { + return NS_OK; + } + + AutoJSAPI jsapi; + jsapi.Init(); + ErrorResult rv; + JS::Rooted<JSObject*> sandboxObject(jsapi.cx()); + RefPtr<CacheStorage> cacheStorage = CreateCacheStorage(jsapi.cx(), aPrincipal, rv, &sandboxObject); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + // We use the ServiceWorker scope as key for the cacheStorage. + RefPtr<Promise> promise = + cacheStorage->Delete(aCacheName, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + // We don't actually care about the result of the delete operation. + return NS_OK; +} + +nsresult +GenerateCacheName(nsAString& aName) +{ + nsresult rv; + nsCOMPtr<nsIUUIDGenerator> uuidGenerator = + do_GetService("@mozilla.org/uuid-generator;1", &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsID id; + rv = uuidGenerator->GenerateUUIDInPlace(&id); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + char chars[NSID_LENGTH]; + id.ToProvidedString(chars); + + // NSID_LENGTH counts the null terminator. + aName.AssignASCII(chars, NSID_LENGTH - 1); + + return NS_OK; +} + +nsresult +Compare(ServiceWorkerRegistrationInfo* aRegistration, + nsIPrincipal* aPrincipal, const nsAString& aCacheName, + const nsAString& aURL, CompareCallback* aCallback, + nsILoadGroup* aLoadGroup) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aRegistration); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(!aURL.IsEmpty()); + MOZ_ASSERT(aCallback); + + RefPtr<CompareManager> cm = new CompareManager(aRegistration, aCallback); + + nsresult rv = cm->Initialize(aPrincipal, aURL, aCacheName, aLoadGroup); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +} // namespace serviceWorkerScriptCache + +END_WORKERS_NAMESPACE |