diff options
Diffstat (limited to 'dom/workers')
730 files changed, 66297 insertions, 0 deletions
diff --git a/dom/workers/ChromeWorkerScope.cpp b/dom/workers/ChromeWorkerScope.cpp new file mode 100644 index 000000000..68ebebfc3 --- /dev/null +++ b/dom/workers/ChromeWorkerScope.cpp @@ -0,0 +1,74 @@ +/* -*- 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 "ChromeWorkerScope.h" + +#include "jsapi.h" + +#include "nsXPCOM.h" +#include "nsNativeCharsetUtils.h" +#include "nsString.h" + +#include "WorkerPrivate.h" + +using namespace mozilla::dom; +USING_WORKERS_NAMESPACE + +namespace { + +#ifdef BUILD_CTYPES + +char* +UnicodeToNative(JSContext* aCx, const char16_t* aSource, size_t aSourceLen) +{ + nsDependentString unicode(aSource, aSourceLen); + + nsAutoCString native; + if (NS_FAILED(NS_CopyUnicodeToNative(unicode, native))) { + JS_ReportErrorASCII(aCx, "Could not convert string to native charset!"); + return nullptr; + } + + char* result = static_cast<char*>(JS_malloc(aCx, native.Length() + 1)); + if (!result) { + return nullptr; + } + + memcpy(result, native.get(), native.Length()); + result[native.Length()] = 0; + return result; +} + +#endif // BUILD_CTYPES + +} // namespace + +BEGIN_WORKERS_NAMESPACE + +bool +DefineChromeWorkerFunctions(JSContext* aCx, JS::Handle<JSObject*> aGlobal) +{ + // Currently ctypes is the only special property given to ChromeWorkers. +#ifdef BUILD_CTYPES + { + JS::Rooted<JS::Value> ctypes(aCx); + if (!JS_InitCTypesClass(aCx, aGlobal) || + !JS_GetProperty(aCx, aGlobal, "ctypes", &ctypes)) { + return false; + } + + static const JSCTypesCallbacks callbacks = { + UnicodeToNative + }; + + JS_SetCTypesCallbacks(ctypes.toObjectOrNull(), &callbacks); + } +#endif // BUILD_CTYPES + + return true; +} + +END_WORKERS_NAMESPACE diff --git a/dom/workers/ChromeWorkerScope.h b/dom/workers/ChromeWorkerScope.h new file mode 100644 index 000000000..306397b66 --- /dev/null +++ b/dom/workers/ChromeWorkerScope.h @@ -0,0 +1,19 @@ +/* -*- 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_workers_chromeworkerscope_h__ +#define mozilla_dom_workers_chromeworkerscope_h__ + +#include "Workers.h" + +BEGIN_WORKERS_NAMESPACE + +bool +DefineChromeWorkerFunctions(JSContext* aCx, JS::Handle<JSObject*> aGlobal); + +END_WORKERS_NAMESPACE + +#endif // mozilla_dom_workers_chromeworkerscope_h__ diff --git a/dom/workers/FileReaderSync.cpp b/dom/workers/FileReaderSync.cpp new file mode 100644 index 000000000..18efcb194 --- /dev/null +++ b/dom/workers/FileReaderSync.cpp @@ -0,0 +1,264 @@ +/* -*- 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 "FileReaderSync.h" + +#include "jsfriendapi.h" +#include "mozilla/Unused.h" +#include "mozilla/Base64.h" +#include "mozilla/dom/EncodingUtils.h" +#include "mozilla/dom/File.h" +#include "nsContentUtils.h" +#include "mozilla/dom/FileReaderSyncBinding.h" +#include "nsCExternalHandlerService.h" +#include "nsComponentManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsDOMClassInfoID.h" +#include "nsError.h" +#include "nsIConverterInputStream.h" +#include "nsIInputStream.h" +#include "nsISeekableStream.h" +#include "nsISupportsImpl.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" + +#include "RuntimeService.h" + +using namespace mozilla; +using namespace mozilla::dom; +using mozilla::dom::Optional; +using mozilla::dom::GlobalObject; + +// static +already_AddRefed<FileReaderSync> +FileReaderSync::Constructor(const GlobalObject& aGlobal, ErrorResult& aRv) +{ + RefPtr<FileReaderSync> frs = new FileReaderSync(); + + return frs.forget(); +} + +bool +FileReaderSync::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto, + JS::MutableHandle<JSObject*> aReflector) +{ + return FileReaderSyncBinding::Wrap(aCx, this, aGivenProto, aReflector); +} + +void +FileReaderSync::ReadAsArrayBuffer(JSContext* aCx, + JS::Handle<JSObject*> aScopeObj, + Blob& aBlob, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) +{ + uint64_t blobSize = aBlob.GetSize(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + UniquePtr<char[], JS::FreePolicy> bufferData(js_pod_malloc<char>(blobSize)); + if (!bufferData) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + nsCOMPtr<nsIInputStream> stream; + aBlob.GetInternalStream(getter_AddRefs(stream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + uint32_t numRead; + aRv = stream->Read(bufferData.get(), blobSize, &numRead); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + NS_ASSERTION(numRead == blobSize, "failed to read data"); + + JSObject* arrayBuffer = JS_NewArrayBufferWithContents(aCx, blobSize, bufferData.get()); + if (!arrayBuffer) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + // arrayBuffer takes the ownership when it is not null. Otherwise we + // need to release it explicitly. + mozilla::Unused << bufferData.release(); + + aRetval.set(arrayBuffer); +} + +void +FileReaderSync::ReadAsBinaryString(Blob& aBlob, + nsAString& aResult, + ErrorResult& aRv) +{ + nsCOMPtr<nsIInputStream> stream; + aBlob.GetInternalStream(getter_AddRefs(stream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + uint32_t numRead; + do { + char readBuf[4096]; + aRv = stream->Read(readBuf, sizeof(readBuf), &numRead); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + uint32_t oldLength = aResult.Length(); + AppendASCIItoUTF16(Substring(readBuf, readBuf + numRead), aResult); + if (aResult.Length() - oldLength != numRead) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } while (numRead > 0); +} + +void +FileReaderSync::ReadAsText(Blob& aBlob, + const Optional<nsAString>& aEncoding, + nsAString& aResult, + ErrorResult& aRv) +{ + nsCOMPtr<nsIInputStream> stream; + aBlob.GetInternalStream(getter_AddRefs(stream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsAutoCString encoding; + unsigned char sniffBuf[3] = { 0, 0, 0 }; + uint32_t numRead; + aRv = stream->Read(reinterpret_cast<char*>(sniffBuf), + sizeof(sniffBuf), &numRead); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + // The BOM sniffing is baked into the "decode" part of the Encoding + // Standard, which the File API references. + if (!nsContentUtils::CheckForBOM(sniffBuf, numRead, encoding)) { + // BOM sniffing failed. Try the API argument. + if (!aEncoding.WasPassed() || + !EncodingUtils::FindEncodingForLabel(aEncoding.Value(), + encoding)) { + // API argument failed. Try the type property of the blob. + nsAutoString type16; + aBlob.GetType(type16); + NS_ConvertUTF16toUTF8 type(type16); + nsAutoCString specifiedCharset; + bool haveCharset; + int32_t charsetStart, charsetEnd; + NS_ExtractCharsetFromContentType(type, + specifiedCharset, + &haveCharset, + &charsetStart, + &charsetEnd); + if (!EncodingUtils::FindEncodingForLabel(specifiedCharset, encoding)) { + // Type property failed. Use UTF-8. + encoding.AssignLiteral("UTF-8"); + } + } + } + + nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(stream); + if (!seekable) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + // Seek to 0 because to undo the BOM sniffing advance. UTF-8 and UTF-16 + // decoders will swallow the BOM. + aRv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + aRv = ConvertStream(stream, encoding.get(), aResult); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} + +void +FileReaderSync::ReadAsDataURL(Blob& aBlob, nsAString& aResult, + ErrorResult& aRv) +{ + nsAutoString scratchResult; + scratchResult.AssignLiteral("data:"); + + nsString contentType; + aBlob.GetType(contentType); + + if (contentType.IsEmpty()) { + scratchResult.AppendLiteral("application/octet-stream"); + } else { + scratchResult.Append(contentType); + } + scratchResult.AppendLiteral(";base64,"); + + nsCOMPtr<nsIInputStream> stream; + aBlob.GetInternalStream(getter_AddRefs(stream), aRv); + if (NS_WARN_IF(aRv.Failed())){ + return; + } + + uint64_t size = aBlob.GetSize(aRv); + if (NS_WARN_IF(aRv.Failed())){ + return; + } + + nsCOMPtr<nsIInputStream> bufferedStream; + aRv = NS_NewBufferedInputStream(getter_AddRefs(bufferedStream), stream, size); + if (NS_WARN_IF(aRv.Failed())){ + return; + } + + nsAutoString encodedData; + aRv = Base64EncodeInputStream(bufferedStream, encodedData, size); + if (NS_WARN_IF(aRv.Failed())){ + return; + } + + scratchResult.Append(encodedData); + + aResult = scratchResult; +} + +nsresult +FileReaderSync::ConvertStream(nsIInputStream *aStream, + const char *aCharset, + nsAString &aResult) +{ + nsCOMPtr<nsIConverterInputStream> converterStream = + do_CreateInstance("@mozilla.org/intl/converter-input-stream;1"); + NS_ENSURE_TRUE(converterStream, NS_ERROR_FAILURE); + + nsresult rv = converterStream->Init(aStream, aCharset, 8192, + nsIConverterInputStream::DEFAULT_REPLACEMENT_CHARACTER); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIUnicharInputStream> unicharStream = + do_QueryInterface(converterStream); + NS_ENSURE_TRUE(unicharStream, NS_ERROR_FAILURE); + + uint32_t numChars; + nsString result; + while (NS_SUCCEEDED(unicharStream->ReadString(8192, result, &numChars)) && + numChars > 0) { + uint32_t oldLength = aResult.Length(); + aResult.Append(result); + if (aResult.Length() - oldLength != result.Length()) { + return NS_ERROR_OUT_OF_MEMORY; + } + } + + return rv; +} + diff --git a/dom/workers/FileReaderSync.h b/dom/workers/FileReaderSync.h new file mode 100644 index 000000000..db8f9d574 --- /dev/null +++ b/dom/workers/FileReaderSync.h @@ -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/. */ + +#ifndef mozilla_dom_filereadersync_h__ +#define mozilla_dom_filereadersync_h__ + +#include "Workers.h" + +class nsIInputStream; + +namespace mozilla { +class ErrorResult; + +namespace dom { +class Blob; +class GlobalObject; +template<typename> class Optional; + +class FileReaderSync final +{ + NS_INLINE_DECL_REFCOUNTING(FileReaderSync) + +private: + // Private destructor, to discourage deletion outside of Release(): + ~FileReaderSync() + { + } + + nsresult ConvertStream(nsIInputStream *aStream, const char *aCharset, + nsAString &aResult); + +public: + static already_AddRefed<FileReaderSync> + Constructor(const GlobalObject& aGlobal, ErrorResult& aRv); + + bool WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto, JS::MutableHandle<JSObject*> aReflector); + + void ReadAsArrayBuffer(JSContext* aCx, JS::Handle<JSObject*> aScopeObj, + Blob& aBlob, JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv); + void ReadAsBinaryString(Blob& aBlob, nsAString& aResult, ErrorResult& aRv); + void ReadAsText(Blob& aBlob, const Optional<nsAString>& aEncoding, + nsAString& aResult, ErrorResult& aRv); + void ReadAsDataURL(Blob& aBlob, nsAString& aResult, ErrorResult& aRv); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_filereadersync_h__ diff --git a/dom/workers/PServiceWorkerManager.ipdl b/dom/workers/PServiceWorkerManager.ipdl new file mode 100644 index 000000000..e7b97672d --- /dev/null +++ b/dom/workers/PServiceWorkerManager.ipdl @@ -0,0 +1,45 @@ +/* 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 protocol PBackground; + +include PBackgroundSharedTypes; +include ServiceWorkerRegistrarTypes; + +using mozilla::PrincipalOriginAttributes from "mozilla/ipc/BackgroundUtils.h"; + +namespace mozilla { +namespace dom { + +protocol PServiceWorkerManager +{ + manager PBackground; + +parent: + async Register(ServiceWorkerRegistrationData data); + + async Unregister(PrincipalInfo principalInfo, nsString scope); + + async PropagateSoftUpdate(PrincipalOriginAttributes originAttributes, + nsString scope); + async PropagateUnregister(PrincipalInfo principalInfo, nsString scope); + + async PropagateRemove(nsCString host); + + async PropagateRemoveAll(); + + async Shutdown(); + +child: + async NotifyRegister(ServiceWorkerRegistrationData data); + async NotifySoftUpdate(PrincipalOriginAttributes originAttributes, nsString scope); + async NotifyUnregister(PrincipalInfo principalInfo, nsString scope); + async NotifyRemove(nsCString host); + async NotifyRemoveAll(); + + async __delete__(); +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/Principal.cpp b/dom/workers/Principal.cpp new file mode 100644 index 000000000..8fcd9ed10 --- /dev/null +++ b/dom/workers/Principal.cpp @@ -0,0 +1,51 @@ +/* -*- 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 "Principal.h" + +#include "jsapi.h" +#include "mozilla/Assertions.h" + +BEGIN_WORKERS_NAMESPACE + +struct WorkerPrincipal final : public JSPrincipals +{ + bool write(JSContext* aCx, JSStructuredCloneWriter* aWriter) override { + MOZ_CRASH("WorkerPrincipal::write not implemented"); + return false; + } +}; + +JSPrincipals* +GetWorkerPrincipal() +{ + static WorkerPrincipal sPrincipal; + + /* + * To make sure the the principals refcount is initialized to one, atomically + * increment it on every pass though this function. If we discover this wasn't + * the first time, decrement it again. This avoids the need for + * synchronization. + */ + int32_t prevRefcount = sPrincipal.refcount++; + if (prevRefcount > 0) { + --sPrincipal.refcount; + } else { +#ifdef DEBUG + sPrincipal.debugToken = kJSPrincipalsDebugToken; +#endif + } + + return &sPrincipal; +} + +void +DestroyWorkerPrincipals(JSPrincipals* aPrincipals) +{ + MOZ_ASSERT_UNREACHABLE("Worker principals refcount should never fall below one"); +} + +END_WORKERS_NAMESPACE diff --git a/dom/workers/Principal.h b/dom/workers/Principal.h new file mode 100644 index 000000000..5bfe19443 --- /dev/null +++ b/dom/workers/Principal.h @@ -0,0 +1,22 @@ +/* -*- 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_workers_principal_h__ +#define mozilla_dom_workers_principal_h__ + +#include "Workers.h" + +BEGIN_WORKERS_NAMESPACE + +JSPrincipals* +GetWorkerPrincipal(); + +void +DestroyWorkerPrincipals(JSPrincipals* aPrincipals); + +END_WORKERS_NAMESPACE + +#endif /* mozilla_dom_workers_principal_h__ */ diff --git a/dom/workers/Queue.h b/dom/workers/Queue.h new file mode 100644 index 000000000..c7a99158b --- /dev/null +++ b/dom/workers/Queue.h @@ -0,0 +1,203 @@ +/* -*- 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_workers_queue_h__ +#define mozilla_dom_workers_queue_h__ + +#include "Workers.h" + +#include "mozilla/Mutex.h" +#include "nsTArray.h" + +BEGIN_WORKERS_NAMESPACE + +template <typename T, int TCount> +struct StorageWithTArray +{ + typedef AutoTArray<T, TCount> StorageType; + + static void Reverse(StorageType& aStorage) + { + uint32_t length = aStorage.Length(); + for (uint32_t index = 0; index < length / 2; index++) { + uint32_t reverseIndex = length - 1 - index; + + T t1 = aStorage.ElementAt(index); + T t2 = aStorage.ElementAt(reverseIndex); + + aStorage.ReplaceElementsAt(index, 1, t2); + aStorage.ReplaceElementsAt(reverseIndex, 1, t1); + } + } + + static bool IsEmpty(const StorageType& aStorage) + { + return !!aStorage.IsEmpty(); + } + + static bool Push(StorageType& aStorage, const T& aEntry) + { + return !!aStorage.AppendElement(aEntry); + } + + static bool Pop(StorageType& aStorage, T& aEntry) + { + if (IsEmpty(aStorage)) { + return false; + } + + uint32_t index = aStorage.Length() - 1; + aEntry = aStorage.ElementAt(index); + aStorage.RemoveElementAt(index); + return true; + } + + static void Clear(StorageType& aStorage) + { + aStorage.Clear(); + } + + static void Compact(StorageType& aStorage) + { + aStorage.Compact(); + } +}; + +class LockingWithMutex +{ + mozilla::Mutex mMutex; + +protected: + LockingWithMutex() + : mMutex("LockingWithMutex::mMutex") + { } + + void Lock() + { + mMutex.Lock(); + } + + void Unlock() + { + mMutex.Unlock(); + } + + class AutoLock + { + LockingWithMutex& mHost; + + public: + explicit AutoLock(LockingWithMutex& aHost) + : mHost(aHost) + { + mHost.Lock(); + } + + ~AutoLock() + { + mHost.Unlock(); + } + }; + + friend class AutoLock; +}; + +class NoLocking +{ +protected: + void Lock() + { } + + void Unlock() + { } + + class AutoLock + { + public: + explicit AutoLock(NoLocking& aHost) + { } + + ~AutoLock() + { } + }; +}; + +template <typename T, + int TCount = 256, + class LockingPolicy = NoLocking, + class StoragePolicy = StorageWithTArray<T, TCount % 2 ? + TCount / 2 + 1 : + TCount / 2> > +class Queue : public LockingPolicy +{ + typedef typename StoragePolicy::StorageType StorageType; + typedef typename LockingPolicy::AutoLock AutoLock; + + StorageType mStorage1; + StorageType mStorage2; + + StorageType* mFront; + StorageType* mBack; + +public: + Queue() + : mFront(&mStorage1), mBack(&mStorage2) + { } + + bool IsEmpty() + { + AutoLock lock(*this); + return StoragePolicy::IsEmpty(*mFront) && + StoragePolicy::IsEmpty(*mBack); + } + + bool Push(const T& aEntry) + { + AutoLock lock(*this); + return StoragePolicy::Push(*mBack, aEntry); + } + + bool Pop(T& aEntry) + { + AutoLock lock(*this); + if (StoragePolicy::IsEmpty(*mFront)) { + StoragePolicy::Compact(*mFront); + StoragePolicy::Reverse(*mBack); + StorageType* tmp = mFront; + mFront = mBack; + mBack = tmp; + } + return StoragePolicy::Pop(*mFront, aEntry); + } + + void Clear() + { + AutoLock lock(*this); + StoragePolicy::Clear(*mFront); + StoragePolicy::Clear(*mBack); + } + + // XXX Do we need this? + void Lock() + { + LockingPolicy::Lock(); + } + + // XXX Do we need this? + void Unlock() + { + LockingPolicy::Unlock(); + } + +private: + // Queue is not copyable. + Queue(const Queue&); + Queue & operator=(const Queue&); +}; + +END_WORKERS_NAMESPACE + +#endif /* mozilla_dom_workers_queue_h__ */ diff --git a/dom/workers/RegisterBindings.cpp b/dom/workers/RegisterBindings.cpp new file mode 100644 index 000000000..b6c1e9cfd --- /dev/null +++ b/dom/workers/RegisterBindings.cpp @@ -0,0 +1,55 @@ +/* -*- 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 "WorkerPrivate.h" +#include "ChromeWorkerScope.h" +#include "RuntimeService.h" + +#include "jsapi.h" +#include "mozilla/dom/RegisterWorkerBindings.h" +#include "mozilla/dom/RegisterWorkerDebuggerBindings.h" +#include "mozilla/OSFileConstants.h" + +USING_WORKERS_NAMESPACE +using namespace mozilla::dom; + +bool +WorkerPrivate::RegisterBindings(JSContext* aCx, JS::Handle<JSObject*> aGlobal) +{ + // Init Web IDL bindings + if (!RegisterWorkerBindings(aCx, aGlobal)) { + return false; + } + + if (IsChromeWorker()) { + if (!DefineChromeWorkerFunctions(aCx, aGlobal) || + !DefineOSFileConstants(aCx, aGlobal)) { + return false; + } + } + + if (!JS_DefineProfilingFunctions(aCx, aGlobal)) { + return false; + } + + return true; +} + +bool +WorkerPrivate::RegisterDebuggerBindings(JSContext* aCx, + JS::Handle<JSObject*> aGlobal) +{ + // Init Web IDL bindings + if (!RegisterWorkerDebuggerBindings(aCx, aGlobal)) { + return false; + } + + if (!JS_DefineDebuggerObject(aCx, aGlobal)) { + return false; + } + + return true; +} diff --git a/dom/workers/RuntimeService.cpp b/dom/workers/RuntimeService.cpp new file mode 100644 index 000000000..d1d76e3d1 --- /dev/null +++ b/dom/workers/RuntimeService.cpp @@ -0,0 +1,2966 @@ +/* -*- 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 "RuntimeService.h" + +#include "nsAutoPtr.h" +#include "nsIChannel.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIDocument.h" +#include "nsIDOMChromeWindow.h" +#include "nsIEffectiveTLDService.h" +#include "nsIObserverService.h" +#include "nsIPrincipal.h" +#include "nsIScriptContext.h" +#include "nsIScriptError.h" +#include "nsIScriptSecurityManager.h" +#include "nsISupportsPriority.h" +#include "nsITimer.h" +#include "nsIURI.h" +#include "nsPIDOMWindow.h" + +#include <algorithm> +#include "BackgroundChild.h" +#include "GeckoProfiler.h" +#include "jsfriendapi.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/Atomics.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/asmjscache/AsmJSCache.h" +#include "mozilla/dom/AtomList.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/ErrorEventBinding.h" +#include "mozilla/dom/EventTargetBinding.h" +#include "mozilla/dom/MessageChannel.h" +#include "mozilla/dom/MessageEventBinding.h" +#include "mozilla/dom/WorkerBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/IndexedDatabaseManager.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/Navigator.h" +#include "nsContentUtils.h" +#include "nsCycleCollector.h" +#include "nsDOMJSUtils.h" +#include "nsIIPCBackgroundChildCreateCallback.h" +#include "nsISupportsImpl.h" +#include "nsLayoutStatics.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" +#include "nsXPCOM.h" +#include "nsXPCOMPrivate.h" +#include "OSFileConstants.h" +#include "xpcpublic.h" + +#include "Principal.h" +#include "SharedWorker.h" +#include "WorkerDebuggerManager.h" +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" +#include "WorkerScope.h" +#include "WorkerThread.h" +#include "prsystem.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::ipc; + +USING_WORKERS_NAMESPACE + +using mozilla::MutexAutoLock; +using mozilla::MutexAutoUnlock; +using mozilla::Preferences; + +// The size of the worker runtime heaps in bytes. May be changed via pref. +#define WORKER_DEFAULT_RUNTIME_HEAPSIZE 32 * 1024 * 1024 + +// The size of the generational GC nursery for workers, in bytes. +#define WORKER_DEFAULT_NURSERY_SIZE 1 * 1024 * 1024 + +// The size of the worker JS allocation threshold in MB. May be changed via pref. +#define WORKER_DEFAULT_ALLOCATION_THRESHOLD 30 + +// Half the size of the actual C stack, to be safe. +#define WORKER_CONTEXT_NATIVE_STACK_LIMIT 128 * sizeof(size_t) * 1024 + +// The maximum number of hardware concurrency, overridable via pref. +#define MAX_HARDWARE_CONCURRENCY 8 + +// The maximum number of threads to use for workers, overridable via pref. +#define MAX_WORKERS_PER_DOMAIN 512 + +static_assert(MAX_WORKERS_PER_DOMAIN >= 1, + "We should allow at least one worker per domain."); + +// The default number of seconds that close handlers will be allowed to run for +// content workers. +#define MAX_SCRIPT_RUN_TIME_SEC 10 + +// The number of seconds that idle threads can hang around before being killed. +#define IDLE_THREAD_TIMEOUT_SEC 30 + +// The maximum number of threads that can be idle at one time. +#define MAX_IDLE_THREADS 20 + +#define PREF_WORKERS_PREFIX "dom.workers." +#define PREF_WORKERS_MAX_PER_DOMAIN PREF_WORKERS_PREFIX "maxPerDomain" +#define PREF_WORKERS_MAX_HARDWARE_CONCURRENCY "dom.maxHardwareConcurrency" + +#define PREF_MAX_SCRIPT_RUN_TIME_CONTENT "dom.max_script_run_time" +#define PREF_MAX_SCRIPT_RUN_TIME_CHROME "dom.max_chrome_script_run_time" + +#define GC_REQUEST_OBSERVER_TOPIC "child-gc-request" +#define CC_REQUEST_OBSERVER_TOPIC "child-cc-request" +#define MEMORY_PRESSURE_OBSERVER_TOPIC "memory-pressure" + +#define BROADCAST_ALL_WORKERS(_func, ...) \ + PR_BEGIN_MACRO \ + AssertIsOnMainThread(); \ + \ + AutoTArray<WorkerPrivate*, 100> workers; \ + { \ + MutexAutoLock lock(mMutex); \ + \ + AddAllTopLevelWorkersToArray(workers); \ + } \ + \ + if (!workers.IsEmpty()) { \ + for (uint32_t index = 0; index < workers.Length(); index++) { \ + workers[index]-> _func (__VA_ARGS__); \ + } \ + } \ + PR_END_MACRO + +// Prefixes for observing preference changes. +#define PREF_JS_OPTIONS_PREFIX "javascript.options." +#define PREF_WORKERS_OPTIONS_PREFIX PREF_WORKERS_PREFIX "options." +#define PREF_MEM_OPTIONS_PREFIX "mem." +#define PREF_GCZEAL "gcZeal" + +namespace { + +const uint32_t kNoIndex = uint32_t(-1); + +uint32_t gMaxWorkersPerDomain = MAX_WORKERS_PER_DOMAIN; +uint32_t gMaxHardwareConcurrency = MAX_HARDWARE_CONCURRENCY; + +// Does not hold an owning reference. +RuntimeService* gRuntimeService = nullptr; + +// Only true during the call to Init. +bool gRuntimeServiceDuringInit = false; + +class LiteralRebindingCString : public nsDependentCString +{ +public: + template<int N> + void RebindLiteral(const char (&aStr)[N]) + { + Rebind(aStr, N-1); + } +}; + +template <typename T> +struct PrefTraits; + +template <> +struct PrefTraits<bool> +{ + typedef bool PrefValueType; + + static const PrefValueType kDefaultValue = false; + + static inline PrefValueType + Get(const char* aPref) + { + AssertIsOnMainThread(); + return Preferences::GetBool(aPref); + } + + static inline bool + Exists(const char* aPref) + { + AssertIsOnMainThread(); + return Preferences::GetType(aPref) == nsIPrefBranch::PREF_BOOL; + } +}; + +template <> +struct PrefTraits<int32_t> +{ + typedef int32_t PrefValueType; + + static inline PrefValueType + Get(const char* aPref) + { + AssertIsOnMainThread(); + return Preferences::GetInt(aPref); + } + + static inline bool + Exists(const char* aPref) + { + AssertIsOnMainThread(); + return Preferences::GetType(aPref) == nsIPrefBranch::PREF_INT; + } +}; + +template <typename T> +T +GetWorkerPref(const nsACString& aPref, + const T aDefault = PrefTraits<T>::kDefaultValue) +{ + AssertIsOnMainThread(); + + typedef PrefTraits<T> PrefHelper; + + T result; + + nsAutoCString prefName; + prefName.AssignLiteral(PREF_WORKERS_OPTIONS_PREFIX); + prefName.Append(aPref); + + if (PrefHelper::Exists(prefName.get())) { + result = PrefHelper::Get(prefName.get()); + } + else { + prefName.AssignLiteral(PREF_JS_OPTIONS_PREFIX); + prefName.Append(aPref); + + if (PrefHelper::Exists(prefName.get())) { + result = PrefHelper::Get(prefName.get()); + } + else { + result = aDefault; + } + } + + return result; +} + +// This fn creates a key for a SharedWorker that contains the name, script +// spec, and the serialized origin attributes: +// "name|scriptSpec^key1=val1&key2=val2&key3=val3" +void +GenerateSharedWorkerKey(const nsACString& aScriptSpec, + const nsACString& aName, + const PrincipalOriginAttributes& aAttrs, + nsCString& aKey) +{ + nsAutoCString suffix; + aAttrs.CreateSuffix(suffix); + + aKey.Truncate(); + aKey.SetCapacity(aName.Length() + aScriptSpec.Length() + suffix.Length() + 2); + aKey.Append(aName); + aKey.Append('|'); + aKey.Append(aScriptSpec); + aKey.Append(suffix); +} + +void +LoadContextOptions(const char* aPrefName, void* /* aClosure */) +{ + AssertIsOnMainThread(); + + RuntimeService* rts = RuntimeService::GetService(); + if (!rts) { + // May be shutting down, just bail. + return; + } + + const nsDependentCString prefName(aPrefName); + + // Several other pref branches will get included here so bail out if there is + // another callback that will handle this change. + if (StringBeginsWith(prefName, + NS_LITERAL_CSTRING(PREF_JS_OPTIONS_PREFIX + PREF_MEM_OPTIONS_PREFIX)) || + StringBeginsWith(prefName, + NS_LITERAL_CSTRING(PREF_WORKERS_OPTIONS_PREFIX + PREF_MEM_OPTIONS_PREFIX))) { + return; + } + +#ifdef JS_GC_ZEAL + if (prefName.EqualsLiteral(PREF_JS_OPTIONS_PREFIX PREF_GCZEAL) || + prefName.EqualsLiteral(PREF_WORKERS_OPTIONS_PREFIX PREF_GCZEAL)) { + return; + } +#endif + + // Context options. + JS::ContextOptions contextOptions; + contextOptions.setAsmJS(GetWorkerPref<bool>(NS_LITERAL_CSTRING("asmjs"))) + .setWasm(GetWorkerPref<bool>(NS_LITERAL_CSTRING("wasm"))) + .setThrowOnAsmJSValidationFailure(GetWorkerPref<bool>( + NS_LITERAL_CSTRING("throw_on_asmjs_validation_failure"))) + .setBaseline(GetWorkerPref<bool>(NS_LITERAL_CSTRING("baselinejit"))) + .setIon(GetWorkerPref<bool>(NS_LITERAL_CSTRING("ion"))) + .setNativeRegExp(GetWorkerPref<bool>(NS_LITERAL_CSTRING("native_regexp"))) + .setAsyncStack(GetWorkerPref<bool>(NS_LITERAL_CSTRING("asyncstack"))) + .setWerror(GetWorkerPref<bool>(NS_LITERAL_CSTRING("werror"))) + .setExtraWarnings(GetWorkerPref<bool>(NS_LITERAL_CSTRING("strict"))); + + RuntimeService::SetDefaultContextOptions(contextOptions); + + if (rts) { + rts->UpdateAllWorkerContextOptions(); + } +} + +#ifdef JS_GC_ZEAL +void +LoadGCZealOptions(const char* /* aPrefName */, void* /* aClosure */) +{ + AssertIsOnMainThread(); + + RuntimeService* rts = RuntimeService::GetService(); + if (!rts) { + // May be shutting down, just bail. + return; + } + + int32_t gczeal = GetWorkerPref<int32_t>(NS_LITERAL_CSTRING(PREF_GCZEAL), -1); + if (gczeal < 0) { + gczeal = 0; + } + + int32_t frequency = + GetWorkerPref<int32_t>(NS_LITERAL_CSTRING("gcZeal.frequency"), -1); + if (frequency < 0) { + frequency = JS_DEFAULT_ZEAL_FREQ; + } + + RuntimeService::SetDefaultGCZeal(uint8_t(gczeal), uint32_t(frequency)); + + if (rts) { + rts->UpdateAllWorkerGCZeal(); + } +} +#endif + +void +UpdateCommonJSGCMemoryOption(RuntimeService* aRuntimeService, + const nsACString& aPrefName, JSGCParamKey aKey) +{ + AssertIsOnMainThread(); + NS_ASSERTION(!aPrefName.IsEmpty(), "Empty pref name!"); + + int32_t prefValue = GetWorkerPref(aPrefName, -1); + uint32_t value = + (prefValue < 0 || prefValue >= 10000) ? 0 : uint32_t(prefValue); + + RuntimeService::SetDefaultJSGCSettings(aKey, value); + + if (aRuntimeService) { + aRuntimeService->UpdateAllWorkerMemoryParameter(aKey, value); + } +} + +void +UpdateOtherJSGCMemoryOption(RuntimeService* aRuntimeService, + JSGCParamKey aKey, uint32_t aValue) +{ + AssertIsOnMainThread(); + + RuntimeService::SetDefaultJSGCSettings(aKey, aValue); + + if (aRuntimeService) { + aRuntimeService->UpdateAllWorkerMemoryParameter(aKey, aValue); + } +} + + +void +LoadJSGCMemoryOptions(const char* aPrefName, void* /* aClosure */) +{ + AssertIsOnMainThread(); + + RuntimeService* rts = RuntimeService::GetService(); + + if (!rts) { + // May be shutting down, just bail. + return; + } + + NS_NAMED_LITERAL_CSTRING(jsPrefix, PREF_JS_OPTIONS_PREFIX); + NS_NAMED_LITERAL_CSTRING(workersPrefix, PREF_WORKERS_OPTIONS_PREFIX); + + const nsDependentCString fullPrefName(aPrefName); + + // Pull out the string that actually distinguishes the parameter we need to + // change. + nsDependentCSubstring memPrefName; + if (StringBeginsWith(fullPrefName, jsPrefix)) { + memPrefName.Rebind(fullPrefName, jsPrefix.Length()); + } + else if (StringBeginsWith(fullPrefName, workersPrefix)) { + memPrefName.Rebind(fullPrefName, workersPrefix.Length()); + } + else { + NS_ERROR("Unknown pref name!"); + return; + } + +#ifdef DEBUG + // During Init() we get called back with a branch string here, so there should + // be no just a "mem." pref here. + if (!rts) { + NS_ASSERTION(memPrefName.EqualsLiteral(PREF_MEM_OPTIONS_PREFIX), "Huh?!"); + } +#endif + + // If we're running in Init() then do this for every pref we care about. + // Otherwise we just want to update the parameter that changed. + for (uint32_t index = !gRuntimeServiceDuringInit + ? JSSettings::kGCSettingsArraySize - 1 : 0; + index < JSSettings::kGCSettingsArraySize; + index++) { + LiteralRebindingCString matchName; + + matchName.RebindLiteral(PREF_MEM_OPTIONS_PREFIX "max"); + if (memPrefName == matchName || (gRuntimeServiceDuringInit && index == 0)) { + int32_t prefValue = GetWorkerPref(matchName, -1); + uint32_t value = (prefValue <= 0 || prefValue >= 0x1000) ? + uint32_t(-1) : + uint32_t(prefValue) * 1024 * 1024; + UpdateOtherJSGCMemoryOption(rts, JSGC_MAX_BYTES, value); + continue; + } + + matchName.RebindLiteral(PREF_MEM_OPTIONS_PREFIX "high_water_mark"); + if (memPrefName == matchName || (gRuntimeServiceDuringInit && index == 1)) { + int32_t prefValue = GetWorkerPref(matchName, 128); + UpdateOtherJSGCMemoryOption(rts, JSGC_MAX_MALLOC_BYTES, + uint32_t(prefValue) * 1024 * 1024); + continue; + } + + matchName.RebindLiteral(PREF_MEM_OPTIONS_PREFIX + "gc_high_frequency_time_limit_ms"); + if (memPrefName == matchName || (gRuntimeServiceDuringInit && index == 2)) { + UpdateCommonJSGCMemoryOption(rts, matchName, + JSGC_HIGH_FREQUENCY_TIME_LIMIT); + continue; + } + + matchName.RebindLiteral(PREF_MEM_OPTIONS_PREFIX + "gc_low_frequency_heap_growth"); + if (memPrefName == matchName || (gRuntimeServiceDuringInit && index == 3)) { + UpdateCommonJSGCMemoryOption(rts, matchName, + JSGC_LOW_FREQUENCY_HEAP_GROWTH); + continue; + } + + matchName.RebindLiteral(PREF_MEM_OPTIONS_PREFIX + "gc_high_frequency_heap_growth_min"); + if (memPrefName == matchName || (gRuntimeServiceDuringInit && index == 4)) { + UpdateCommonJSGCMemoryOption(rts, matchName, + JSGC_HIGH_FREQUENCY_HEAP_GROWTH_MIN); + continue; + } + + matchName.RebindLiteral(PREF_MEM_OPTIONS_PREFIX + "gc_high_frequency_heap_growth_max"); + if (memPrefName == matchName || (gRuntimeServiceDuringInit && index == 5)) { + UpdateCommonJSGCMemoryOption(rts, matchName, + JSGC_HIGH_FREQUENCY_HEAP_GROWTH_MAX); + continue; + } + + matchName.RebindLiteral(PREF_MEM_OPTIONS_PREFIX + "gc_high_frequency_low_limit_mb"); + if (memPrefName == matchName || (gRuntimeServiceDuringInit && index == 6)) { + UpdateCommonJSGCMemoryOption(rts, matchName, + JSGC_HIGH_FREQUENCY_LOW_LIMIT); + continue; + } + + matchName.RebindLiteral(PREF_MEM_OPTIONS_PREFIX + "gc_high_frequency_high_limit_mb"); + if (memPrefName == matchName || (gRuntimeServiceDuringInit && index == 7)) { + UpdateCommonJSGCMemoryOption(rts, matchName, + JSGC_HIGH_FREQUENCY_HIGH_LIMIT); + continue; + } + + matchName.RebindLiteral(PREF_MEM_OPTIONS_PREFIX + "gc_allocation_threshold_mb"); + if (memPrefName == matchName || (gRuntimeServiceDuringInit && index == 8)) { + UpdateCommonJSGCMemoryOption(rts, matchName, JSGC_ALLOCATION_THRESHOLD); + continue; + } + + matchName.RebindLiteral(PREF_MEM_OPTIONS_PREFIX "gc_incremental_slice_ms"); + if (memPrefName == matchName || (gRuntimeServiceDuringInit && index == 9)) { + int32_t prefValue = GetWorkerPref(matchName, -1); + uint32_t value = + (prefValue <= 0 || prefValue >= 100000) ? 0 : uint32_t(prefValue); + UpdateOtherJSGCMemoryOption(rts, JSGC_SLICE_TIME_BUDGET, value); + continue; + } + + matchName.RebindLiteral(PREF_MEM_OPTIONS_PREFIX "gc_dynamic_heap_growth"); + if (memPrefName == matchName || + (gRuntimeServiceDuringInit && index == 10)) { + bool prefValue = GetWorkerPref(matchName, false); + UpdateOtherJSGCMemoryOption(rts, JSGC_DYNAMIC_HEAP_GROWTH, + prefValue ? 0 : 1); + continue; + } + + matchName.RebindLiteral(PREF_MEM_OPTIONS_PREFIX "gc_dynamic_mark_slice"); + if (memPrefName == matchName || + (gRuntimeServiceDuringInit && index == 11)) { + bool prefValue = GetWorkerPref(matchName, false); + UpdateOtherJSGCMemoryOption(rts, JSGC_DYNAMIC_MARK_SLICE, + prefValue ? 0 : 1); + continue; + } + + matchName.RebindLiteral(PREF_MEM_OPTIONS_PREFIX "gc_min_empty_chunk_count"); + if (memPrefName == matchName || + (gRuntimeServiceDuringInit && index == 12)) { + UpdateCommonJSGCMemoryOption(rts, matchName, JSGC_MIN_EMPTY_CHUNK_COUNT); + continue; + } + + matchName.RebindLiteral(PREF_MEM_OPTIONS_PREFIX "gc_max_empty_chunk_count"); + if (memPrefName == matchName || + (gRuntimeServiceDuringInit && index == 13)) { + UpdateCommonJSGCMemoryOption(rts, matchName, JSGC_MAX_EMPTY_CHUNK_COUNT); + continue; + } + + matchName.RebindLiteral(PREF_MEM_OPTIONS_PREFIX "gc_compacting"); + if (memPrefName == matchName || + (gRuntimeServiceDuringInit && index == 14)) { + bool prefValue = GetWorkerPref(matchName, false); + UpdateOtherJSGCMemoryOption(rts, JSGC_COMPACTING_ENABLED, + prefValue ? 0 : 1); + continue; + } + + matchName.RebindLiteral(PREF_MEM_OPTIONS_PREFIX "gc_refresh_frame_slices_enabled"); + if (memPrefName == matchName || + (gRuntimeServiceDuringInit && index == 15)) { + bool prefValue = GetWorkerPref(matchName, false); + UpdateOtherJSGCMemoryOption(rts, JSGC_REFRESH_FRAME_SLICES_ENABLED, + prefValue ? 0 : 1); + continue; + } + +#ifdef DEBUG + nsAutoCString message("Workers don't support the 'mem."); + message.Append(memPrefName); + message.AppendLiteral("' preference!"); + NS_WARNING(message.get()); +#endif + } +} + +bool +InterruptCallback(JSContext* aCx) +{ + WorkerPrivate* worker = GetWorkerPrivateFromContext(aCx); + MOZ_ASSERT(worker); + + // Now is a good time to turn on profiling if it's pending. + profiler_js_operation_callback(); + + return worker->InterruptCallback(aCx); +} + +class LogViolationDetailsRunnable final : public WorkerMainThreadRunnable +{ + nsString mFileName; + uint32_t mLineNum; + +public: + LogViolationDetailsRunnable(WorkerPrivate* aWorker, + const nsString& aFileName, + uint32_t aLineNum) + : WorkerMainThreadRunnable(aWorker, + NS_LITERAL_CSTRING("RuntimeService :: LogViolationDetails")) + , mFileName(aFileName), mLineNum(aLineNum) + { + MOZ_ASSERT(aWorker); + } + + virtual bool MainThreadRun() override; + +private: + ~LogViolationDetailsRunnable() {} +}; + +bool +ContentSecurityPolicyAllows(JSContext* aCx) +{ + WorkerPrivate* worker = GetWorkerPrivateFromContext(aCx); + worker->AssertIsOnWorkerThread(); + + if (worker->GetReportCSPViolations()) { + nsString fileName; + uint32_t lineNum = 0; + + JS::AutoFilename file; + if (JS::DescribeScriptedCaller(aCx, &file, &lineNum) && file.get()) { + fileName = NS_ConvertUTF8toUTF16(file.get()); + } else { + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + } + + RefPtr<LogViolationDetailsRunnable> runnable = + new LogViolationDetailsRunnable(worker, fileName, lineNum); + + ErrorResult rv; + runnable->Dispatch(rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + } + } + + return worker->IsEvalAllowed(); +} + +void +CTypesActivityCallback(JSContext* aCx, + js::CTypesActivityType aType) +{ + WorkerPrivate* worker = GetWorkerPrivateFromContext(aCx); + worker->AssertIsOnWorkerThread(); + + switch (aType) { + case js::CTYPES_CALL_BEGIN: + worker->BeginCTypesCall(); + break; + + case js::CTYPES_CALL_END: + worker->EndCTypesCall(); + break; + + case js::CTYPES_CALLBACK_BEGIN: + worker->BeginCTypesCallback(); + break; + + case js::CTYPES_CALLBACK_END: + worker->EndCTypesCallback(); + break; + + default: + MOZ_CRASH("Unknown type flag!"); + } +} + +static nsIPrincipal* +GetPrincipalForAsmJSCacheOp() +{ + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + if (!workerPrivate) { + return nullptr; + } + + // asmjscache::OpenEntryForX guarnatee to only access the given nsIPrincipal + // from the main thread. + return workerPrivate->GetPrincipalDontAssertMainThread(); +} + +static bool +AsmJSCacheOpenEntryForRead(JS::Handle<JSObject*> aGlobal, + const char16_t* aBegin, + const char16_t* aLimit, + size_t* aSize, + const uint8_t** aMemory, + intptr_t *aHandle) +{ + nsIPrincipal* principal = GetPrincipalForAsmJSCacheOp(); + if (!principal) { + return false; + } + + return asmjscache::OpenEntryForRead(principal, aBegin, aLimit, aSize, aMemory, + aHandle); +} + +static JS::AsmJSCacheResult +AsmJSCacheOpenEntryForWrite(JS::Handle<JSObject*> aGlobal, + bool aInstalled, + const char16_t* aBegin, + const char16_t* aEnd, + size_t aSize, + uint8_t** aMemory, + intptr_t* aHandle) +{ + nsIPrincipal* principal = GetPrincipalForAsmJSCacheOp(); + if (!principal) { + return JS::AsmJSCache_InternalError; + } + + return asmjscache::OpenEntryForWrite(principal, aInstalled, aBegin, aEnd, + aSize, aMemory, aHandle); +} + +class AsyncTaskWorkerHolder final : public WorkerHolder +{ + bool Notify(Status aStatus) override + { + // The async task must complete in bounded time and there is not (currently) + // a clean way to cancel it. Async tasks do not run arbitrary content. + return true; + } + +public: + WorkerPrivate* Worker() const + { + return mWorkerPrivate; + } +}; + +template <class RunnableBase> +class AsyncTaskBase : public RunnableBase +{ + UniquePtr<AsyncTaskWorkerHolder> mHolder; + + // Disable the usual pre/post-dispatch thread assertions since we are + // dispatching from some random JS engine internal thread: + + bool PreDispatch(WorkerPrivate* aWorkerPrivate) override + { + return true; + } + + void PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override + { } + +protected: + explicit AsyncTaskBase(UniquePtr<AsyncTaskWorkerHolder> aHolder) + : RunnableBase(aHolder->Worker(), + WorkerRunnable::WorkerThreadUnchangedBusyCount) + , mHolder(Move(aHolder)) + { + MOZ_ASSERT(mHolder); + } + + ~AsyncTaskBase() + { + MOZ_ASSERT(!mHolder); + } + + void DestroyHolder() + { + MOZ_ASSERT(mHolder); + mHolder.reset(); + } + +public: + UniquePtr<AsyncTaskWorkerHolder> StealHolder() + { + return Move(mHolder); + } +}; + +class AsyncTaskRunnable final : public AsyncTaskBase<WorkerRunnable> +{ + JS::AsyncTask* mTask; + + ~AsyncTaskRunnable() + { + MOZ_ASSERT(!mTask); + } + + void PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override + { + // For the benefit of the destructor assert. + if (!aDispatchResult) { + mTask = nullptr; + } + } + +public: + AsyncTaskRunnable(UniquePtr<AsyncTaskWorkerHolder> aHolder, + JS::AsyncTask* aTask) + : AsyncTaskBase<WorkerRunnable>(Move(aHolder)) + , mTask(aTask) + { + MOZ_ASSERT(mTask); + } + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + MOZ_ASSERT(aWorkerPrivate == mWorkerPrivate); + MOZ_ASSERT(aCx == mWorkerPrivate->GetJSContext()); + MOZ_ASSERT(mTask); + + AutoJSAPI jsapi; + jsapi.Init(); + + mTask->finish(mWorkerPrivate->GetJSContext()); + mTask = nullptr; // mTask may delete itself + + DestroyHolder(); + + return true; + } + + nsresult Cancel() override + { + MOZ_ASSERT(mTask); + + AutoJSAPI jsapi; + jsapi.Init(); + + mTask->cancel(mWorkerPrivate->GetJSContext()); + mTask = nullptr; // mTask may delete itself + + DestroyHolder(); + + return WorkerRunnable::Cancel(); + } +}; + +class AsyncTaskControlRunnable final + : public AsyncTaskBase<WorkerControlRunnable> +{ +public: + explicit AsyncTaskControlRunnable(UniquePtr<AsyncTaskWorkerHolder> aHolder) + : AsyncTaskBase<WorkerControlRunnable>(Move(aHolder)) + { } + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + // See comment in FinishAsyncTaskCallback. + DestroyHolder(); + return true; + } +}; + +static bool +StartAsyncTaskCallback(JSContext* aCx, JS::AsyncTask* aTask) +{ + WorkerPrivate* worker = GetWorkerPrivateFromContext(aCx); + worker->AssertIsOnWorkerThread(); + + auto holder = MakeUnique<AsyncTaskWorkerHolder>(); + if (!holder->HoldWorker(worker, Status::Closing)) { + return false; + } + + // Matched by a UniquePtr in FinishAsyncTaskCallback which, by + // interface contract, must be called in the future. + aTask->user = holder.release(); + return true; +} + +static bool +FinishAsyncTaskCallback(JS::AsyncTask* aTask) +{ + // May execute either on the worker thread or a random JS-internal helper + // thread. + + // Match the release() in StartAsyncTaskCallback. + UniquePtr<AsyncTaskWorkerHolder> holder( + static_cast<AsyncTaskWorkerHolder*>(aTask->user)); + + RefPtr<AsyncTaskRunnable> r = new AsyncTaskRunnable(Move(holder), aTask); + + // WorkerRunnable::Dispatch() can fail during worker shutdown. In that case, + // report failure back to the JS engine but make sure to release the + // WorkerHolder on the worker thread using a control runnable. Control + // runables aren't suitable for calling AsyncTask::finish() since they are run + // via the interrupt callback which breaks JS run-to-completion. + if (!r->Dispatch()) { + RefPtr<AsyncTaskControlRunnable> cr = + new AsyncTaskControlRunnable(r->StealHolder()); + + MOZ_ALWAYS_TRUE(cr->Dispatch()); + return false; + } + + return true; +} + +class WorkerJSContext; + +class WorkerThreadContextPrivate : private PerThreadAtomCache +{ + friend class WorkerJSContext; + + WorkerPrivate* mWorkerPrivate; + +public: + // This can't return null, but we can't lose the "Get" prefix in the name or + // it will be ambiguous with the WorkerPrivate class name. + WorkerPrivate* + GetWorkerPrivate() const + { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mWorkerPrivate); + + return mWorkerPrivate; + } + +private: + explicit + WorkerThreadContextPrivate(WorkerPrivate* aWorkerPrivate) + : mWorkerPrivate(aWorkerPrivate) + { + MOZ_ASSERT(!NS_IsMainThread()); + + // Zero out the base class members. + memset(this, 0, sizeof(PerThreadAtomCache)); + + MOZ_ASSERT(mWorkerPrivate); + } + + ~WorkerThreadContextPrivate() + { + MOZ_ASSERT(!NS_IsMainThread()); + } + + WorkerThreadContextPrivate(const WorkerThreadContextPrivate&) = delete; + + WorkerThreadContextPrivate& + operator=(const WorkerThreadContextPrivate&) = delete; +}; + +bool +InitJSContextForWorker(WorkerPrivate* aWorkerPrivate, JSContext* aWorkerCx) +{ + aWorkerPrivate->AssertIsOnWorkerThread(); + NS_ASSERTION(!aWorkerPrivate->GetJSContext(), "Already has a context!"); + + JSSettings settings; + aWorkerPrivate->CopyJSSettings(settings); + + { + JS::UniqueChars defaultLocale = aWorkerPrivate->AdoptDefaultLocale(); + MOZ_ASSERT(defaultLocale, + "failure of a WorkerPrivate to have a default locale should " + "have made the worker fail to spawn"); + + if (!JS_SetDefaultLocale(aWorkerCx, defaultLocale.get())) { + NS_WARNING("failed to set workerCx's default locale"); + return false; + } + } + + JS::ContextOptionsRef(aWorkerCx) = settings.contextOptions; + + JSSettings::JSGCSettingsArray& gcSettings = settings.gcSettings; + + // This is the real place where we set the max memory for the runtime. + for (uint32_t index = 0; index < ArrayLength(gcSettings); index++) { + const JSSettings::JSGCSetting& setting = gcSettings[index]; + if (setting.IsSet()) { + NS_ASSERTION(setting.value, "Can't handle 0 values!"); + JS_SetGCParameter(aWorkerCx, setting.key, setting.value); + } + } + + JS_SetNativeStackQuota(aWorkerCx, WORKER_CONTEXT_NATIVE_STACK_LIMIT); + + // Security policy: + static const JSSecurityCallbacks securityCallbacks = { + ContentSecurityPolicyAllows + }; + JS_SetSecurityCallbacks(aWorkerCx, &securityCallbacks); + + // Set up the asm.js cache callbacks + static const JS::AsmJSCacheOps asmJSCacheOps = { + AsmJSCacheOpenEntryForRead, + asmjscache::CloseEntryForRead, + AsmJSCacheOpenEntryForWrite, + asmjscache::CloseEntryForWrite + }; + JS::SetAsmJSCacheOps(aWorkerCx, &asmJSCacheOps); + + JS::SetAsyncTaskCallbacks(aWorkerCx, StartAsyncTaskCallback, FinishAsyncTaskCallback); + + if (!JS::InitSelfHostedCode(aWorkerCx)) { + NS_WARNING("Could not init self-hosted code!"); + return false; + } + + JS_AddInterruptCallback(aWorkerCx, InterruptCallback); + + js::SetCTypesActivityCallback(aWorkerCx, CTypesActivityCallback); + +#ifdef JS_GC_ZEAL + JS_SetGCZeal(aWorkerCx, settings.gcZeal, settings.gcZealFrequency); +#endif + + return true; +} + +static bool +PreserveWrapper(JSContext *cx, JSObject *obj) +{ + MOZ_ASSERT(cx); + MOZ_ASSERT(obj); + MOZ_ASSERT(mozilla::dom::IsDOMObject(obj)); + + return mozilla::dom::TryPreserveWrapper(obj); +} + +JSObject* +Wrap(JSContext *cx, JS::HandleObject existing, JS::HandleObject obj) +{ + JSObject* targetGlobal = JS::CurrentGlobalOrNull(cx); + if (!IsDebuggerGlobal(targetGlobal) && !IsDebuggerSandbox(targetGlobal)) { + MOZ_CRASH("There should be no edges from the debuggee to the debugger."); + } + + JSObject* originGlobal = js::GetGlobalForObjectCrossCompartment(obj); + + const js::Wrapper* wrapper = nullptr; + if (IsDebuggerGlobal(originGlobal) || IsDebuggerSandbox(originGlobal)) { + wrapper = &js::CrossCompartmentWrapper::singleton; + } else { + wrapper = &js::OpaqueCrossCompartmentWrapper::singleton; + } + + if (existing) { + js::Wrapper::Renew(cx, existing, obj, wrapper); + } + return js::Wrapper::New(cx, obj, wrapper); +} + +static const JSWrapObjectCallbacks WrapObjectCallbacks = { + Wrap, + nullptr, +}; + +class MOZ_STACK_CLASS WorkerJSContext final : public mozilla::CycleCollectedJSContext +{ +public: + // The heap size passed here doesn't matter, we will change it later in the + // call to JS_SetGCParameter inside InitJSContextForWorker. + explicit WorkerJSContext(WorkerPrivate* aWorkerPrivate) + : mWorkerPrivate(aWorkerPrivate) + { + MOZ_ASSERT(aWorkerPrivate); + } + + ~WorkerJSContext() + { + JSContext* cx = MaybeContext(); + if (!cx) { + return; // Initialize() must have failed + } + + delete static_cast<WorkerThreadContextPrivate*>(JS_GetContextPrivate(cx)); + JS_SetContextPrivate(cx, nullptr); + + // The worker global should be unrooted and the shutdown cycle collection + // should break all remaining cycles. The superclass destructor will run + // the GC one final time and finalize any JSObjects that were participating + // in cycles that were broken during CC shutdown. + nsCycleCollector_shutdown(); + + // The CC is shut down, and the superclass destructor will GC, so make sure + // we don't try to CC again. + mWorkerPrivate = nullptr; + } + + nsresult Initialize(JSContext* aParentContext) + { + nsresult rv = + CycleCollectedJSContext::Initialize(aParentContext, + WORKER_DEFAULT_RUNTIME_HEAPSIZE, + WORKER_DEFAULT_NURSERY_SIZE); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + JSContext* cx = Context(); + + JS_SetContextPrivate(cx, new WorkerThreadContextPrivate(mWorkerPrivate)); + + js::SetPreserveWrapperCallback(cx, PreserveWrapper); + JS_InitDestroyPrincipalsCallback(cx, DestroyWorkerPrincipals); + JS_SetWrapObjectCallbacks(cx, &WrapObjectCallbacks); + if (mWorkerPrivate->IsDedicatedWorker()) { + JS_SetFutexCanWait(cx); + } + + return NS_OK; + } + + virtual void + PrepareForForgetSkippable() override + { + } + + virtual void + BeginCycleCollectionCallback() override + { + } + + virtual void + EndCycleCollectionCallback(CycleCollectorResults &aResults) override + { + } + + void + DispatchDeferredDeletion(bool aContinuation, bool aPurge) override + { + MOZ_ASSERT(!aContinuation); + + // Do it immediately, no need for asynchronous behavior here. + nsCycleCollector_doDeferredDeletion(); + } + + virtual void CustomGCCallback(JSGCStatus aStatus) override + { + if (!mWorkerPrivate) { + // We're shutting down, no need to do anything. + return; + } + + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (aStatus == JSGC_END) { + nsCycleCollector_collect(nullptr); + } + } + + virtual void AfterProcessTask(uint32_t aRecursionDepth) override + { + // Only perform the Promise microtask checkpoint on the outermost event + // loop. Don't run it, for example, during sync XHR or importScripts. + if (aRecursionDepth == 2) { + CycleCollectedJSContext::AfterProcessTask(aRecursionDepth); + } else if (aRecursionDepth > 2) { + AutoDisableMicroTaskCheckpoint disableMicroTaskCheckpoint; + CycleCollectedJSContext::AfterProcessTask(aRecursionDepth); + } + } + + virtual void DispatchToMicroTask(already_AddRefed<nsIRunnable> aRunnable) override + { + RefPtr<nsIRunnable> runnable(aRunnable); + + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(runnable); + + std::queue<nsCOMPtr<nsIRunnable>>* microTaskQueue = nullptr; + + JSContext* cx = GetCurrentThreadJSContext(); + NS_ASSERTION(cx, "This should never be null!"); + + JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); + NS_ASSERTION(global, "This should never be null!"); + + // On worker threads, if the current global is the worker global, we use the + // main promise micro task queue. Otherwise, the current global must be + // either the debugger global or a debugger sandbox, and we use the debugger + // promise micro task queue instead. + if (IsWorkerGlobal(global)) { + microTaskQueue = &mPromiseMicroTaskQueue; + } else { + MOZ_ASSERT(IsDebuggerGlobal(global) || IsDebuggerSandbox(global)); + + microTaskQueue = &mDebuggerPromiseMicroTaskQueue; + } + + microTaskQueue->push(runnable.forget()); + } + +private: + WorkerPrivate* mWorkerPrivate; +}; + +class WorkerThreadPrimaryRunnable final : public Runnable +{ + WorkerPrivate* mWorkerPrivate; + RefPtr<WorkerThread> mThread; + JSContext* mParentContext; + + class FinishedRunnable final : public Runnable + { + RefPtr<WorkerThread> mThread; + + public: + explicit FinishedRunnable(already_AddRefed<WorkerThread> aThread) + : mThread(aThread) + { + MOZ_ASSERT(mThread); + } + + NS_DECL_ISUPPORTS_INHERITED + + private: + ~FinishedRunnable() + { } + + NS_DECL_NSIRUNNABLE + }; + +public: + WorkerThreadPrimaryRunnable(WorkerPrivate* aWorkerPrivate, + WorkerThread* aThread, + JSContext* aParentContext) + : mWorkerPrivate(aWorkerPrivate), mThread(aThread), mParentContext(aParentContext) + { + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aThread); + } + + NS_DECL_ISUPPORTS_INHERITED + +private: + ~WorkerThreadPrimaryRunnable() + { } + + NS_DECL_NSIRUNNABLE +}; + +class WorkerTaskRunnable final : public WorkerRunnable +{ + RefPtr<WorkerTask> mTask; + +public: + WorkerTaskRunnable(WorkerPrivate* aWorkerPrivate, WorkerTask* aTask) + : WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), mTask(aTask) + { + MOZ_ASSERT(aTask); + } + +private: + virtual bool + PreDispatch(WorkerPrivate* aWorkerPrivate) override + { + // May be called on any thread! + return true; + } + + virtual void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override + { + // May be called on any thread! + } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + return mTask->RunTask(aCx); + } +}; + +void +PrefLanguagesChanged(const char* /* aPrefName */, void* /* aClosure */) +{ + AssertIsOnMainThread(); + + nsTArray<nsString> languages; + Navigator::GetAcceptLanguages(languages); + + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->UpdateAllWorkerLanguages(languages); + } +} + +void +AppNameOverrideChanged(const char* /* aPrefName */, void* /* aClosure */) +{ + AssertIsOnMainThread(); + + const nsAdoptingString& override = + mozilla::Preferences::GetString("general.appname.override"); + + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->UpdateAppNameOverridePreference(override); + } +} + +void +AppVersionOverrideChanged(const char* /* aPrefName */, void* /* aClosure */) +{ + AssertIsOnMainThread(); + + const nsAdoptingString& override = + mozilla::Preferences::GetString("general.appversion.override"); + + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->UpdateAppVersionOverridePreference(override); + } +} + +void +PlatformOverrideChanged(const char* /* aPrefName */, void* /* aClosure */) +{ + AssertIsOnMainThread(); + + const nsAdoptingString& override = + mozilla::Preferences::GetString("general.platform.override"); + + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->UpdatePlatformOverridePreference(override); + } +} + +class BackgroundChildCallback final + : public nsIIPCBackgroundChildCreateCallback +{ +public: + BackgroundChildCallback() + { + AssertIsOnMainThread(); + } + + NS_DECL_ISUPPORTS + +private: + ~BackgroundChildCallback() + { + AssertIsOnMainThread(); + } + + virtual void + ActorCreated(PBackgroundChild* aActor) override + { + AssertIsOnMainThread(); + MOZ_ASSERT(aActor); + } + + virtual void + ActorFailed() override + { + AssertIsOnMainThread(); + MOZ_CRASH("Unable to connect PBackground actor for the main thread!"); + } +}; + +NS_IMPL_ISUPPORTS(BackgroundChildCallback, nsIIPCBackgroundChildCreateCallback) + +} /* anonymous namespace */ + +BEGIN_WORKERS_NAMESPACE + +void +CancelWorkersForWindow(nsPIDOMWindowInner* aWindow) +{ + AssertIsOnMainThread(); + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->CancelWorkersForWindow(aWindow); + } +} + +void +FreezeWorkersForWindow(nsPIDOMWindowInner* aWindow) +{ + AssertIsOnMainThread(); + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->FreezeWorkersForWindow(aWindow); + } +} + +void +ThawWorkersForWindow(nsPIDOMWindowInner* aWindow) +{ + AssertIsOnMainThread(); + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->ThawWorkersForWindow(aWindow); + } +} + +void +SuspendWorkersForWindow(nsPIDOMWindowInner* aWindow) +{ + AssertIsOnMainThread(); + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->SuspendWorkersForWindow(aWindow); + } +} + +void +ResumeWorkersForWindow(nsPIDOMWindowInner* aWindow) +{ + AssertIsOnMainThread(); + RuntimeService* runtime = RuntimeService::GetService(); + if (runtime) { + runtime->ResumeWorkersForWindow(aWindow); + } +} + +WorkerCrossThreadDispatcher::WorkerCrossThreadDispatcher( + WorkerPrivate* aWorkerPrivate) +: mMutex("WorkerCrossThreadDispatcher::mMutex"), + mWorkerPrivate(aWorkerPrivate) +{ + MOZ_ASSERT(aWorkerPrivate); +} + +bool +WorkerCrossThreadDispatcher::PostTask(WorkerTask* aTask) +{ + MOZ_ASSERT(aTask); + + MutexAutoLock lock(mMutex); + + if (!mWorkerPrivate) { + NS_WARNING("Posted a task to a WorkerCrossThreadDispatcher that is no " + "longer accepting tasks!"); + return false; + } + + RefPtr<WorkerTaskRunnable> runnable = + new WorkerTaskRunnable(mWorkerPrivate, aTask); + return runnable->Dispatch(); +} + +WorkerPrivate* +GetWorkerPrivateFromContext(JSContext* aCx) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aCx); + + void* cxPrivate = JS_GetContextPrivate(aCx); + if (!cxPrivate) { + return nullptr; + } + + return + static_cast<WorkerThreadContextPrivate*>(cxPrivate)->GetWorkerPrivate(); +} + +WorkerPrivate* +GetCurrentThreadWorkerPrivate() +{ + MOZ_ASSERT(!NS_IsMainThread()); + + CycleCollectedJSContext* ccjscx = CycleCollectedJSContext::Get(); + if (!ccjscx) { + return nullptr; + } + + JSContext* cx = ccjscx->Context(); + MOZ_ASSERT(cx); + + void* cxPrivate = JS_GetContextPrivate(cx); + if (!cxPrivate) { + // This can happen if the nsCycleCollector_shutdown() in ~WorkerJSContext() + // triggers any calls to GetCurrentThreadWorkerPrivate(). At this stage + // CycleCollectedJSContext::Get() will still return a context, but + // the context private has already been cleared. + return nullptr; + } + + return + static_cast<WorkerThreadContextPrivate*>(cxPrivate)->GetWorkerPrivate(); +} + +bool +IsCurrentThreadRunningChromeWorker() +{ + return GetCurrentThreadWorkerPrivate()->UsesSystemPrincipal(); +} + +JSContext* +GetCurrentThreadJSContext() +{ + WorkerPrivate* wp = GetCurrentThreadWorkerPrivate(); + if (!wp) { + return nullptr; + } + return wp->GetJSContext(); +} + +JSObject* +GetCurrentThreadWorkerGlobal() +{ + WorkerPrivate* wp = GetCurrentThreadWorkerPrivate(); + if (!wp) { + return nullptr; + } + WorkerGlobalScope* scope = wp->GlobalScope(); + if (!scope) { + return nullptr; + } + return scope->GetGlobalJSObject(); +} + +END_WORKERS_NAMESPACE + +struct RuntimeService::IdleThreadInfo +{ + RefPtr<WorkerThread> mThread; + mozilla::TimeStamp mExpirationTime; +}; + +// This is only touched on the main thread. Initialized in Init() below. +JSSettings RuntimeService::sDefaultJSSettings; +bool RuntimeService::sDefaultPreferences[WORKERPREF_COUNT] = { false }; + +RuntimeService::RuntimeService() +: mMutex("RuntimeService::mMutex"), mObserved(false), + mShuttingDown(false), mNavigatorPropertiesLoaded(false) +{ + AssertIsOnMainThread(); + NS_ASSERTION(!gRuntimeService, "More than one service!"); +} + +RuntimeService::~RuntimeService() +{ + AssertIsOnMainThread(); + + // gRuntimeService can be null if Init() fails. + NS_ASSERTION(!gRuntimeService || gRuntimeService == this, + "More than one service!"); + + gRuntimeService = nullptr; +} + +// static +RuntimeService* +RuntimeService::GetOrCreateService() +{ + AssertIsOnMainThread(); + + if (!gRuntimeService) { + // The observer service now owns us until shutdown. + gRuntimeService = new RuntimeService(); + if (NS_FAILED(gRuntimeService->Init())) { + NS_WARNING("Failed to initialize!"); + gRuntimeService->Cleanup(); + gRuntimeService = nullptr; + return nullptr; + } + } + + return gRuntimeService; +} + +// static +RuntimeService* +RuntimeService::GetService() +{ + return gRuntimeService; +} + +bool +RuntimeService::RegisterWorker(WorkerPrivate* aWorkerPrivate) +{ + aWorkerPrivate->AssertIsOnParentThread(); + + WorkerPrivate* parent = aWorkerPrivate->GetParent(); + if (!parent) { + AssertIsOnMainThread(); + + if (mShuttingDown) { + return false; + } + } + + const bool isServiceWorker = aWorkerPrivate->IsServiceWorker(); + const bool isSharedWorker = aWorkerPrivate->IsSharedWorker(); + const bool isDedicatedWorker = aWorkerPrivate->IsDedicatedWorker(); + if (isServiceWorker) { + AssertIsOnMainThread(); + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_SPAWN_ATTEMPTS, 1); + } + + nsCString sharedWorkerScriptSpec; + if (isSharedWorker) { + AssertIsOnMainThread(); + + nsCOMPtr<nsIURI> scriptURI = aWorkerPrivate->GetResolvedScriptURI(); + NS_ASSERTION(scriptURI, "Null script URI!"); + + nsresult rv = scriptURI->GetSpec(sharedWorkerScriptSpec); + if (NS_FAILED(rv)) { + NS_WARNING("GetSpec failed?!"); + return false; + } + + NS_ASSERTION(!sharedWorkerScriptSpec.IsEmpty(), "Empty spec!"); + } + + bool exemptFromPerDomainMax = false; + if (isServiceWorker) { + AssertIsOnMainThread(); + exemptFromPerDomainMax = Preferences::GetBool("dom.serviceWorkers.exemptFromPerDomainMax", + false); + } + + const nsCString& domain = aWorkerPrivate->Domain(); + + WorkerDomainInfo* domainInfo; + bool queued = false; + { + MutexAutoLock lock(mMutex); + + if (!mDomainMap.Get(domain, &domainInfo)) { + NS_ASSERTION(!parent, "Shouldn't have a parent here!"); + + domainInfo = new WorkerDomainInfo(); + domainInfo->mDomain = domain; + mDomainMap.Put(domain, domainInfo); + } + + queued = gMaxWorkersPerDomain && + domainInfo->ActiveWorkerCount() >= gMaxWorkersPerDomain && + !domain.IsEmpty() && + !exemptFromPerDomainMax; + + if (queued) { + domainInfo->mQueuedWorkers.AppendElement(aWorkerPrivate); + + // Worker spawn gets queued due to hitting max workers per domain + // limit so let's log a warning. + WorkerPrivate::ReportErrorToConsole("HittingMaxWorkersPerDomain2"); + + if (isServiceWorker) { + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_SPAWN_GETS_QUEUED, 1); + } else if (isSharedWorker) { + Telemetry::Accumulate(Telemetry::SHARED_WORKER_SPAWN_GETS_QUEUED, 1); + } else if (isDedicatedWorker) { + Telemetry::Accumulate(Telemetry::DEDICATED_WORKER_SPAWN_GETS_QUEUED, 1); + } + } + else if (parent) { + domainInfo->mChildWorkerCount++; + } + else if (isServiceWorker) { + domainInfo->mActiveServiceWorkers.AppendElement(aWorkerPrivate); + } + else { + domainInfo->mActiveWorkers.AppendElement(aWorkerPrivate); + } + + if (isSharedWorker) { + const nsCString& sharedWorkerName = aWorkerPrivate->WorkerName(); + nsAutoCString key; + GenerateSharedWorkerKey(sharedWorkerScriptSpec, sharedWorkerName, + aWorkerPrivate->GetOriginAttributes(), key); + MOZ_ASSERT(!domainInfo->mSharedWorkerInfos.Get(key)); + + SharedWorkerInfo* sharedWorkerInfo = + new SharedWorkerInfo(aWorkerPrivate, sharedWorkerScriptSpec, + sharedWorkerName); + domainInfo->mSharedWorkerInfos.Put(key, sharedWorkerInfo); + } + } + + // From here on out we must call UnregisterWorker if something fails! + if (parent) { + if (!parent->AddChildWorker(aWorkerPrivate)) { + UnregisterWorker(aWorkerPrivate); + return false; + } + } + else { + if (!mNavigatorPropertiesLoaded) { + Navigator::AppName(mNavigatorProperties.mAppName, + false /* aUsePrefOverriddenValue */); + if (NS_FAILED(Navigator::GetAppVersion(mNavigatorProperties.mAppVersion, + false /* aUsePrefOverriddenValue */)) || + NS_FAILED(Navigator::GetPlatform(mNavigatorProperties.mPlatform, + false /* aUsePrefOverriddenValue */))) { + UnregisterWorker(aWorkerPrivate); + return false; + } + + // The navigator overridden properties should have already been read. + + Navigator::GetAcceptLanguages(mNavigatorProperties.mLanguages); + mNavigatorPropertiesLoaded = true; + } + + nsPIDOMWindowInner* window = aWorkerPrivate->GetWindow(); + + if (!isServiceWorker) { + // Service workers are excluded since their lifetime is separate from + // that of dom windows. + nsTArray<WorkerPrivate*>* windowArray; + if (!mWindowMap.Get(window, &windowArray)) { + windowArray = new nsTArray<WorkerPrivate*>(1); + mWindowMap.Put(window, windowArray); + } + + if (!windowArray->Contains(aWorkerPrivate)) { + windowArray->AppendElement(aWorkerPrivate); + } else { + MOZ_ASSERT(aWorkerPrivate->IsSharedWorker()); + } + } + } + + if (!queued && !ScheduleWorker(aWorkerPrivate)) { + return false; + } + + if (isServiceWorker) { + AssertIsOnMainThread(); + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_WAS_SPAWNED, 1); + } + return true; +} + +void +RuntimeService::RemoveSharedWorker(WorkerDomainInfo* aDomainInfo, + WorkerPrivate* aWorkerPrivate) +{ + for (auto iter = aDomainInfo->mSharedWorkerInfos.Iter(); + !iter.Done(); + iter.Next()) { + SharedWorkerInfo* data = iter.UserData(); + if (data->mWorkerPrivate == aWorkerPrivate) { +#ifdef DEBUG + nsAutoCString key; + GenerateSharedWorkerKey(data->mScriptSpec, data->mName, + aWorkerPrivate->GetOriginAttributes(), key); + MOZ_ASSERT(iter.Key() == key); +#endif + iter.Remove(); + break; + } + } +} + +void +RuntimeService::UnregisterWorker(WorkerPrivate* aWorkerPrivate) +{ + aWorkerPrivate->AssertIsOnParentThread(); + + WorkerPrivate* parent = aWorkerPrivate->GetParent(); + if (!parent) { + AssertIsOnMainThread(); + } + + const nsCString& domain = aWorkerPrivate->Domain(); + + WorkerPrivate* queuedWorker = nullptr; + { + MutexAutoLock lock(mMutex); + + WorkerDomainInfo* domainInfo; + if (!mDomainMap.Get(domain, &domainInfo)) { + NS_ERROR("Don't have an entry for this domain!"); + } + + // Remove old worker from everywhere. + uint32_t index = domainInfo->mQueuedWorkers.IndexOf(aWorkerPrivate); + if (index != kNoIndex) { + // Was queued, remove from the list. + domainInfo->mQueuedWorkers.RemoveElementAt(index); + } + else if (parent) { + MOZ_ASSERT(domainInfo->mChildWorkerCount, "Must be non-zero!"); + domainInfo->mChildWorkerCount--; + } + else if (aWorkerPrivate->IsServiceWorker()) { + MOZ_ASSERT(domainInfo->mActiveServiceWorkers.Contains(aWorkerPrivate), + "Don't know about this worker!"); + domainInfo->mActiveServiceWorkers.RemoveElement(aWorkerPrivate); + } + else { + MOZ_ASSERT(domainInfo->mActiveWorkers.Contains(aWorkerPrivate), + "Don't know about this worker!"); + domainInfo->mActiveWorkers.RemoveElement(aWorkerPrivate); + } + + if (aWorkerPrivate->IsSharedWorker()) { + RemoveSharedWorker(domainInfo, aWorkerPrivate); + } + + // See if there's a queued worker we can schedule. + if (domainInfo->ActiveWorkerCount() < gMaxWorkersPerDomain && + !domainInfo->mQueuedWorkers.IsEmpty()) { + queuedWorker = domainInfo->mQueuedWorkers[0]; + domainInfo->mQueuedWorkers.RemoveElementAt(0); + + if (queuedWorker->GetParent()) { + domainInfo->mChildWorkerCount++; + } + else if (queuedWorker->IsServiceWorker()) { + domainInfo->mActiveServiceWorkers.AppendElement(queuedWorker); + } + else { + domainInfo->mActiveWorkers.AppendElement(queuedWorker); + } + } + + if (domainInfo->HasNoWorkers()) { + MOZ_ASSERT(domainInfo->mQueuedWorkers.IsEmpty()); + mDomainMap.Remove(domain); + } + } + + if (aWorkerPrivate->IsServiceWorker()) { + AssertIsOnMainThread(); + Telemetry::AccumulateTimeDelta(Telemetry::SERVICE_WORKER_LIFE_TIME, + aWorkerPrivate->CreationTimeStamp()); + } + + if (aWorkerPrivate->IsSharedWorker() || + aWorkerPrivate->IsServiceWorker()) { + AssertIsOnMainThread(); + aWorkerPrivate->CloseAllSharedWorkers(); + } + + if (parent) { + parent->RemoveChildWorker(aWorkerPrivate); + } + else if (aWorkerPrivate->IsSharedWorker()) { + AssertIsOnMainThread(); + + for (auto iter = mWindowMap.Iter(); !iter.Done(); iter.Next()) { + nsAutoPtr<nsTArray<WorkerPrivate*>>& workers = iter.Data(); + MOZ_ASSERT(workers.get()); + + if (workers->RemoveElement(aWorkerPrivate)) { + MOZ_ASSERT(!workers->Contains(aWorkerPrivate), + "Added worker more than once!"); + + if (workers->IsEmpty()) { + iter.Remove(); + } + } + } + } + else if (aWorkerPrivate->IsDedicatedWorker()) { + // May be null. + nsPIDOMWindowInner* window = aWorkerPrivate->GetWindow(); + + nsTArray<WorkerPrivate*>* windowArray; + MOZ_ALWAYS_TRUE(mWindowMap.Get(window, &windowArray)); + + MOZ_ALWAYS_TRUE(windowArray->RemoveElement(aWorkerPrivate)); + + if (windowArray->IsEmpty()) { + mWindowMap.Remove(window); + } + } + + if (queuedWorker && !ScheduleWorker(queuedWorker)) { + UnregisterWorker(queuedWorker); + } +} + +bool +RuntimeService::ScheduleWorker(WorkerPrivate* aWorkerPrivate) +{ + if (!aWorkerPrivate->Start()) { + // This is ok, means that we didn't need to make a thread for this worker. + return true; + } + + RefPtr<WorkerThread> thread; + { + MutexAutoLock lock(mMutex); + if (!mIdleThreadArray.IsEmpty()) { + uint32_t index = mIdleThreadArray.Length() - 1; + mIdleThreadArray[index].mThread.swap(thread); + mIdleThreadArray.RemoveElementAt(index); + } + } + + const WorkerThreadFriendKey friendKey; + + if (!thread) { + thread = WorkerThread::Create(friendKey); + if (!thread) { + UnregisterWorker(aWorkerPrivate); + return false; + } + } + + int32_t priority = aWorkerPrivate->IsChromeWorker() ? + nsISupportsPriority::PRIORITY_NORMAL : + nsISupportsPriority::PRIORITY_LOW; + + if (NS_FAILED(thread->SetPriority(priority))) { + NS_WARNING("Could not set the thread's priority!"); + } + + JSContext* cx = CycleCollectedJSContext::Get()->Context(); + nsCOMPtr<nsIRunnable> runnable = + new WorkerThreadPrimaryRunnable(aWorkerPrivate, thread, + JS_GetParentContext(cx)); + if (NS_FAILED(thread->DispatchPrimaryRunnable(friendKey, runnable.forget()))) { + UnregisterWorker(aWorkerPrivate); + return false; + } + + return true; +} + +// static +void +RuntimeService::ShutdownIdleThreads(nsITimer* aTimer, void* /* aClosure */) +{ + AssertIsOnMainThread(); + + RuntimeService* runtime = RuntimeService::GetService(); + NS_ASSERTION(runtime, "This should never be null!"); + + NS_ASSERTION(aTimer == runtime->mIdleThreadTimer, "Wrong timer!"); + + // Cheat a little and grab all threads that expire within one second of now. + TimeStamp now = TimeStamp::NowLoRes() + TimeDuration::FromSeconds(1); + + TimeStamp nextExpiration; + + AutoTArray<RefPtr<WorkerThread>, 20> expiredThreads; + { + MutexAutoLock lock(runtime->mMutex); + + for (uint32_t index = 0; index < runtime->mIdleThreadArray.Length(); + index++) { + IdleThreadInfo& info = runtime->mIdleThreadArray[index]; + if (info.mExpirationTime > now) { + nextExpiration = info.mExpirationTime; + break; + } + + RefPtr<WorkerThread>* thread = expiredThreads.AppendElement(); + thread->swap(info.mThread); + } + + if (!expiredThreads.IsEmpty()) { + runtime->mIdleThreadArray.RemoveElementsAt(0, expiredThreads.Length()); + } + } + + if (!nextExpiration.IsNull()) { + TimeDuration delta = nextExpiration - TimeStamp::NowLoRes(); + uint32_t delay(delta > TimeDuration(0) ? delta.ToMilliseconds() : 0); + + // Reschedule the timer. + MOZ_ALWAYS_SUCCEEDS( + aTimer->InitWithFuncCallback(ShutdownIdleThreads, + nullptr, + delay, + nsITimer::TYPE_ONE_SHOT)); + } + + for (uint32_t index = 0; index < expiredThreads.Length(); index++) { + if (NS_FAILED(expiredThreads[index]->Shutdown())) { + NS_WARNING("Failed to shutdown thread!"); + } + } +} + +nsresult +RuntimeService::Init() +{ + AssertIsOnMainThread(); + + nsLayoutStatics::AddRef(); + + // Make sure PBackground actors are connected as soon as possible for the main + // thread in case workers clone remote blobs here. + if (!BackgroundChild::GetForCurrentThread()) { + RefPtr<BackgroundChildCallback> callback = new BackgroundChildCallback(); + if (!BackgroundChild::GetOrCreateForCurrentThread(callback)) { + MOZ_CRASH("Unable to connect PBackground actor for the main thread!"); + } + } + + // Initialize JSSettings. + if (!sDefaultJSSettings.gcSettings[0].IsSet()) { + sDefaultJSSettings.contextOptions = JS::ContextOptions(); + sDefaultJSSettings.chrome.maxScriptRuntime = -1; + sDefaultJSSettings.chrome.compartmentOptions.behaviors().setVersion(JSVERSION_LATEST); + sDefaultJSSettings.content.maxScriptRuntime = MAX_SCRIPT_RUN_TIME_SEC; +#ifdef JS_GC_ZEAL + sDefaultJSSettings.gcZealFrequency = JS_DEFAULT_ZEAL_FREQ; + sDefaultJSSettings.gcZeal = 0; +#endif + SetDefaultJSGCSettings(JSGC_MAX_BYTES, WORKER_DEFAULT_RUNTIME_HEAPSIZE); + SetDefaultJSGCSettings(JSGC_ALLOCATION_THRESHOLD, + WORKER_DEFAULT_ALLOCATION_THRESHOLD); + } + + mIdleThreadTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + NS_ENSURE_STATE(mIdleThreadTimer); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE); + + nsresult rv = + obs->AddObserver(this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, false); + NS_ENSURE_SUCCESS(rv, rv); + + rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + NS_ENSURE_SUCCESS(rv, rv); + + mObserved = true; + + if (NS_FAILED(obs->AddObserver(this, GC_REQUEST_OBSERVER_TOPIC, false))) { + NS_WARNING("Failed to register for GC request notifications!"); + } + + if (NS_FAILED(obs->AddObserver(this, CC_REQUEST_OBSERVER_TOPIC, false))) { + NS_WARNING("Failed to register for CC request notifications!"); + } + + if (NS_FAILED(obs->AddObserver(this, MEMORY_PRESSURE_OBSERVER_TOPIC, + false))) { + NS_WARNING("Failed to register for memory pressure notifications!"); + } + + if (NS_FAILED(obs->AddObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC, false))) { + NS_WARNING("Failed to register for offline notification event!"); + } + + MOZ_ASSERT(!gRuntimeServiceDuringInit, "This should be false!"); + gRuntimeServiceDuringInit = true; + + if (NS_FAILED(Preferences::RegisterCallback( + LoadJSGCMemoryOptions, + PREF_JS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX, + nullptr)) || + NS_FAILED(Preferences::RegisterCallbackAndCall( + LoadJSGCMemoryOptions, + PREF_WORKERS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX, + nullptr)) || +#ifdef JS_GC_ZEAL + NS_FAILED(Preferences::RegisterCallback( + LoadGCZealOptions, + PREF_JS_OPTIONS_PREFIX PREF_GCZEAL, + nullptr)) || +#endif + +#define WORKER_SIMPLE_PREF(name, getter, NAME) \ + NS_FAILED(Preferences::RegisterCallbackAndCall( \ + WorkerPrefChanged, \ + name, \ + reinterpret_cast<void*>(WORKERPREF_##NAME))) || +#define WORKER_PREF(name, callback) \ + NS_FAILED(Preferences::RegisterCallbackAndCall( \ + callback, \ + name, \ + nullptr)) || +#include "WorkerPrefs.h" +#undef WORKER_SIMPLE_PREF +#undef WORKER_PREF + + NS_FAILED(Preferences::RegisterCallbackAndCall( + LoadContextOptions, + PREF_WORKERS_OPTIONS_PREFIX, + nullptr)) || + NS_FAILED(Preferences::RegisterCallback(LoadContextOptions, + PREF_JS_OPTIONS_PREFIX, + nullptr))) { + NS_WARNING("Failed to register pref callbacks!"); + } + + MOZ_ASSERT(gRuntimeServiceDuringInit, "Should be true!"); + gRuntimeServiceDuringInit = false; + + // We assume atomic 32bit reads/writes. If this assumption doesn't hold on + // some wacky platform then the worst that could happen is that the close + // handler will run for a slightly different amount of time. + if (NS_FAILED(Preferences::AddIntVarCache( + &sDefaultJSSettings.content.maxScriptRuntime, + PREF_MAX_SCRIPT_RUN_TIME_CONTENT, + MAX_SCRIPT_RUN_TIME_SEC)) || + NS_FAILED(Preferences::AddIntVarCache( + &sDefaultJSSettings.chrome.maxScriptRuntime, + PREF_MAX_SCRIPT_RUN_TIME_CHROME, -1))) { + NS_WARNING("Failed to register timeout cache!"); + } + + int32_t maxPerDomain = Preferences::GetInt(PREF_WORKERS_MAX_PER_DOMAIN, + MAX_WORKERS_PER_DOMAIN); + gMaxWorkersPerDomain = std::max(0, maxPerDomain); + + int32_t maxHardwareConcurrency = + Preferences::GetInt(PREF_WORKERS_MAX_HARDWARE_CONCURRENCY, + MAX_HARDWARE_CONCURRENCY); + gMaxHardwareConcurrency = std::max(0, maxHardwareConcurrency); + + rv = InitOSFileConstants(); + if (NS_FAILED(rv)) { + return rv; + } + + if (NS_WARN_IF(!IndexedDatabaseManager::GetOrCreate())) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +void +RuntimeService::Shutdown() +{ + AssertIsOnMainThread(); + + MOZ_ASSERT(!mShuttingDown); + // That's it, no more workers. + mShuttingDown = true; + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + NS_WARNING_ASSERTION(obs, "Failed to get observer service?!"); + + // Tell anyone that cares that they're about to lose worker support. + if (obs && NS_FAILED(obs->NotifyObservers(nullptr, WORKERS_SHUTDOWN_TOPIC, + nullptr))) { + NS_WARNING("NotifyObservers failed!"); + } + + { + MutexAutoLock lock(mMutex); + + AutoTArray<WorkerPrivate*, 100> workers; + AddAllTopLevelWorkersToArray(workers); + + if (!workers.IsEmpty()) { + // Cancel all top-level workers. + { + MutexAutoUnlock unlock(mMutex); + + for (uint32_t index = 0; index < workers.Length(); index++) { + if (!workers[index]->Kill()) { + NS_WARNING("Failed to cancel worker!"); + } + } + } + } + } +} + +// This spins the event loop until all workers are finished and their threads +// have been joined. +void +RuntimeService::Cleanup() +{ + AssertIsOnMainThread(); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + NS_WARNING_ASSERTION(obs, "Failed to get observer service?!"); + + if (mIdleThreadTimer) { + if (NS_FAILED(mIdleThreadTimer->Cancel())) { + NS_WARNING("Failed to cancel idle timer!"); + } + mIdleThreadTimer = nullptr; + } + + { + MutexAutoLock lock(mMutex); + + AutoTArray<WorkerPrivate*, 100> workers; + AddAllTopLevelWorkersToArray(workers); + + if (!workers.IsEmpty()) { + nsIThread* currentThread = NS_GetCurrentThread(); + NS_ASSERTION(currentThread, "This should never be null!"); + + // Shut down any idle threads. + if (!mIdleThreadArray.IsEmpty()) { + AutoTArray<RefPtr<WorkerThread>, 20> idleThreads; + + uint32_t idleThreadCount = mIdleThreadArray.Length(); + idleThreads.SetLength(idleThreadCount); + + for (uint32_t index = 0; index < idleThreadCount; index++) { + NS_ASSERTION(mIdleThreadArray[index].mThread, "Null thread!"); + idleThreads[index].swap(mIdleThreadArray[index].mThread); + } + + mIdleThreadArray.Clear(); + + MutexAutoUnlock unlock(mMutex); + + for (uint32_t index = 0; index < idleThreadCount; index++) { + if (NS_FAILED(idleThreads[index]->Shutdown())) { + NS_WARNING("Failed to shutdown thread!"); + } + } + } + + // And make sure all their final messages have run and all their threads + // have joined. + while (mDomainMap.Count()) { + MutexAutoUnlock unlock(mMutex); + + if (!NS_ProcessNextEvent(currentThread)) { + NS_WARNING("Something bad happened!"); + break; + } + } + } + } + + NS_ASSERTION(!mWindowMap.Count(), "All windows should have been released!"); + + if (mObserved) { + if (NS_FAILED(Preferences::UnregisterCallback(LoadContextOptions, + PREF_JS_OPTIONS_PREFIX, + nullptr)) || + NS_FAILED(Preferences::UnregisterCallback(LoadContextOptions, + PREF_WORKERS_OPTIONS_PREFIX, + nullptr)) || + +#define WORKER_SIMPLE_PREF(name, getter, NAME) \ + NS_FAILED(Preferences::UnregisterCallback( \ + WorkerPrefChanged, \ + name, \ + reinterpret_cast<void*>(WORKERPREF_##NAME))) || +#define WORKER_PREF(name, callback) \ + NS_FAILED(Preferences::UnregisterCallback( \ + callback, \ + name, \ + nullptr)) || +#include "WorkerPrefs.h" +#undef WORKER_SIMPLE_PREF +#undef WORKER_PREF + +#ifdef JS_GC_ZEAL + NS_FAILED(Preferences::UnregisterCallback( + LoadGCZealOptions, + PREF_JS_OPTIONS_PREFIX PREF_GCZEAL, + nullptr)) || +#endif + NS_FAILED(Preferences::UnregisterCallback( + LoadJSGCMemoryOptions, + PREF_JS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX, + nullptr)) || + NS_FAILED(Preferences::UnregisterCallback( + LoadJSGCMemoryOptions, + PREF_WORKERS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX, + nullptr))) { + NS_WARNING("Failed to unregister pref callbacks!"); + } + + if (obs) { + if (NS_FAILED(obs->RemoveObserver(this, GC_REQUEST_OBSERVER_TOPIC))) { + NS_WARNING("Failed to unregister for GC request notifications!"); + } + + if (NS_FAILED(obs->RemoveObserver(this, CC_REQUEST_OBSERVER_TOPIC))) { + NS_WARNING("Failed to unregister for CC request notifications!"); + } + + if (NS_FAILED(obs->RemoveObserver(this, + MEMORY_PRESSURE_OBSERVER_TOPIC))) { + NS_WARNING("Failed to unregister for memory pressure notifications!"); + } + + if (NS_FAILED(obs->RemoveObserver(this, + NS_IOSERVICE_OFFLINE_STATUS_TOPIC))) { + NS_WARNING("Failed to unregister for offline notification event!"); + } + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID); + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + mObserved = false; + } + } + + CleanupOSFileConstants(); + nsLayoutStatics::Release(); +} + +void +RuntimeService::AddAllTopLevelWorkersToArray(nsTArray<WorkerPrivate*>& aWorkers) +{ + for (auto iter = mDomainMap.Iter(); !iter.Done(); iter.Next()) { + + WorkerDomainInfo* aData = iter.UserData(); + +#ifdef DEBUG + for (uint32_t index = 0; index < aData->mActiveWorkers.Length(); index++) { + MOZ_ASSERT(!aData->mActiveWorkers[index]->GetParent(), + "Shouldn't have a parent in this list!"); + } + for (uint32_t index = 0; index < aData->mActiveServiceWorkers.Length(); index++) { + MOZ_ASSERT(!aData->mActiveServiceWorkers[index]->GetParent(), + "Shouldn't have a parent in this list!"); + } +#endif + + aWorkers.AppendElements(aData->mActiveWorkers); + aWorkers.AppendElements(aData->mActiveServiceWorkers); + + // These might not be top-level workers... + for (uint32_t index = 0; index < aData->mQueuedWorkers.Length(); index++) { + WorkerPrivate* worker = aData->mQueuedWorkers[index]; + if (!worker->GetParent()) { + aWorkers.AppendElement(worker); + } + } + } +} + +void +RuntimeService::GetWorkersForWindow(nsPIDOMWindowInner* aWindow, + nsTArray<WorkerPrivate*>& aWorkers) +{ + AssertIsOnMainThread(); + + nsTArray<WorkerPrivate*>* workers; + if (mWindowMap.Get(aWindow, &workers)) { + NS_ASSERTION(!workers->IsEmpty(), "Should have been removed!"); + aWorkers.AppendElements(*workers); + } + else { + NS_ASSERTION(aWorkers.IsEmpty(), "Should be empty!"); + } +} + +void +RuntimeService::CancelWorkersForWindow(nsPIDOMWindowInner* aWindow) +{ + AssertIsOnMainThread(); + + nsTArray<WorkerPrivate*> workers; + GetWorkersForWindow(aWindow, workers); + + if (!workers.IsEmpty()) { + for (uint32_t index = 0; index < workers.Length(); index++) { + WorkerPrivate*& worker = workers[index]; + + if (worker->IsSharedWorker()) { + worker->CloseSharedWorkersForWindow(aWindow); + } else { + worker->Cancel(); + } + } + } +} + +void +RuntimeService::FreezeWorkersForWindow(nsPIDOMWindowInner* aWindow) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aWindow); + + nsTArray<WorkerPrivate*> workers; + GetWorkersForWindow(aWindow, workers); + + for (uint32_t index = 0; index < workers.Length(); index++) { + workers[index]->Freeze(aWindow); + } +} + +void +RuntimeService::ThawWorkersForWindow(nsPIDOMWindowInner* aWindow) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aWindow); + + nsTArray<WorkerPrivate*> workers; + GetWorkersForWindow(aWindow, workers); + + for (uint32_t index = 0; index < workers.Length(); index++) { + workers[index]->Thaw(aWindow); + } +} + +void +RuntimeService::SuspendWorkersForWindow(nsPIDOMWindowInner* aWindow) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aWindow); + + nsTArray<WorkerPrivate*> workers; + GetWorkersForWindow(aWindow, workers); + + for (uint32_t index = 0; index < workers.Length(); index++) { + workers[index]->ParentWindowPaused(); + } +} + +void +RuntimeService::ResumeWorkersForWindow(nsPIDOMWindowInner* aWindow) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aWindow); + + nsTArray<WorkerPrivate*> workers; + GetWorkersForWindow(aWindow, workers); + + for (uint32_t index = 0; index < workers.Length(); index++) { + workers[index]->ParentWindowResumed(); + } +} + +nsresult +RuntimeService::CreateSharedWorker(const GlobalObject& aGlobal, + const nsAString& aScriptURL, + const nsACString& aName, + SharedWorker** aSharedWorker) +{ + AssertIsOnMainThread(); + + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal.GetAsSupports()); + MOZ_ASSERT(window); + + JSContext* cx = aGlobal.Context(); + + WorkerLoadInfo loadInfo; + nsresult rv = WorkerPrivate::GetLoadInfo(cx, window, nullptr, aScriptURL, + false, + WorkerPrivate::OverrideLoadGroup, + WorkerTypeShared, &loadInfo); + NS_ENSURE_SUCCESS(rv, rv); + + return CreateSharedWorkerFromLoadInfo(cx, &loadInfo, aScriptURL, aName, + aSharedWorker); +} + +nsresult +RuntimeService::CreateSharedWorkerFromLoadInfo(JSContext* aCx, + WorkerLoadInfo* aLoadInfo, + const nsAString& aScriptURL, + const nsACString& aName, + SharedWorker** aSharedWorker) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aLoadInfo); + MOZ_ASSERT(aLoadInfo->mResolvedScriptURI); + + RefPtr<WorkerPrivate> workerPrivate; + { + MutexAutoLock lock(mMutex); + + WorkerDomainInfo* domainInfo; + SharedWorkerInfo* sharedWorkerInfo; + + nsCString scriptSpec; + nsresult rv = aLoadInfo->mResolvedScriptURI->GetSpec(scriptSpec); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_ASSERT(aLoadInfo->mPrincipal); + nsAutoCString key; + GenerateSharedWorkerKey(scriptSpec, aName, + BasePrincipal::Cast(aLoadInfo->mPrincipal)->OriginAttributesRef(), key); + + if (mDomainMap.Get(aLoadInfo->mDomain, &domainInfo) && + domainInfo->mSharedWorkerInfos.Get(key, &sharedWorkerInfo)) { + workerPrivate = sharedWorkerInfo->mWorkerPrivate; + } + } + + // Keep a reference to the window before spawning the worker. If the worker is + // a Shared/Service worker and the worker script loads and executes before + // the SharedWorker object itself is created before then WorkerScriptLoaded() + // will reset the loadInfo's window. + nsCOMPtr<nsPIDOMWindowInner> window = aLoadInfo->mWindow; + + // shouldAttachToWorkerPrivate tracks whether our SharedWorker should actually + // get attached to the WorkerPrivate we're using. It will become false if the + // WorkerPrivate already exists and its secure context state doesn't match + // what we want for the new SharedWorker. + bool shouldAttachToWorkerPrivate = true; + bool created = false; + ErrorResult rv; + if (!workerPrivate) { + workerPrivate = + WorkerPrivate::Constructor(aCx, aScriptURL, false, + WorkerTypeShared, aName, aLoadInfo, rv); + NS_ENSURE_TRUE(workerPrivate, rv.StealNSResult()); + + created = true; + } else { + // Check whether the secure context state matches. The current compartment + // of aCx is the compartment of the SharedWorker constructor that was + // invoked, which is the compartment of the document that will be hooked up + // to the worker, so that's what we want to check. + shouldAttachToWorkerPrivate = + workerPrivate->IsSecureContext() == + JS_GetIsSecureContext(js::GetContextCompartment(aCx)); + + // If we're attaching to an existing SharedWorker private, then we + // must update the overriden load group to account for our document's + // load group. + if (shouldAttachToWorkerPrivate) { + workerPrivate->UpdateOverridenLoadGroup(aLoadInfo->mLoadGroup); + } + } + + // We don't actually care about this MessageChannel, but we use it to 'steal' + // its 2 connected ports. + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(window); + RefPtr<MessageChannel> channel = MessageChannel::Constructor(global, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + RefPtr<SharedWorker> sharedWorker = new SharedWorker(window, workerPrivate, + channel->Port1()); + + if (!shouldAttachToWorkerPrivate) { + // We're done here. Just queue up our error event and return our + // dead-on-arrival SharedWorker. + RefPtr<AsyncEventDispatcher> errorEvent = + new AsyncEventDispatcher(sharedWorker, NS_LITERAL_STRING("error"), false); + errorEvent->PostDOMEvent(); + sharedWorker.forget(aSharedWorker); + return NS_OK; + } + + if (!workerPrivate->RegisterSharedWorker(sharedWorker, channel->Port2())) { + NS_WARNING("Worker is unreachable, this shouldn't happen!"); + sharedWorker->Close(); + return NS_ERROR_FAILURE; + } + + // This is normally handled in RegisterWorker, but that wasn't called if the + // worker already existed. + if (!created) { + nsTArray<WorkerPrivate*>* windowArray; + if (!mWindowMap.Get(window, &windowArray)) { + windowArray = new nsTArray<WorkerPrivate*>(1); + mWindowMap.Put(window, windowArray); + } + + if (!windowArray->Contains(workerPrivate)) { + windowArray->AppendElement(workerPrivate); + } + } + + sharedWorker.forget(aSharedWorker); + return NS_OK; +} + +void +RuntimeService::ForgetSharedWorker(WorkerPrivate* aWorkerPrivate) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aWorkerPrivate->IsSharedWorker()); + + MutexAutoLock lock(mMutex); + + WorkerDomainInfo* domainInfo; + if (mDomainMap.Get(aWorkerPrivate->Domain(), &domainInfo)) { + RemoveSharedWorker(domainInfo, aWorkerPrivate); + } +} + +void +RuntimeService::NoteIdleThread(WorkerThread* aThread) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aThread); + + bool shutdownThread = mShuttingDown; + bool scheduleTimer = false; + + if (!shutdownThread) { + static TimeDuration timeout = + TimeDuration::FromSeconds(IDLE_THREAD_TIMEOUT_SEC); + + TimeStamp expirationTime = TimeStamp::NowLoRes() + timeout; + + MutexAutoLock lock(mMutex); + + uint32_t previousIdleCount = mIdleThreadArray.Length(); + + if (previousIdleCount < MAX_IDLE_THREADS) { + IdleThreadInfo* info = mIdleThreadArray.AppendElement(); + info->mThread = aThread; + info->mExpirationTime = expirationTime; + + scheduleTimer = previousIdleCount == 0; + } else { + shutdownThread = true; + } + } + + MOZ_ASSERT_IF(shutdownThread, !scheduleTimer); + MOZ_ASSERT_IF(scheduleTimer, !shutdownThread); + + // Too many idle threads, just shut this one down. + if (shutdownThread) { + MOZ_ALWAYS_SUCCEEDS(aThread->Shutdown()); + } else if (scheduleTimer) { + MOZ_ALWAYS_SUCCEEDS( + mIdleThreadTimer->InitWithFuncCallback(ShutdownIdleThreads, + nullptr, + IDLE_THREAD_TIMEOUT_SEC * 1000, + nsITimer::TYPE_ONE_SHOT)); + } +} + +void +RuntimeService::UpdateAllWorkerContextOptions() +{ + BROADCAST_ALL_WORKERS(UpdateContextOptions, sDefaultJSSettings.contextOptions); +} + +void +RuntimeService::UpdateAppNameOverridePreference(const nsAString& aValue) +{ + AssertIsOnMainThread(); + mNavigatorProperties.mAppNameOverridden = aValue; +} + +void +RuntimeService::UpdateAppVersionOverridePreference(const nsAString& aValue) +{ + AssertIsOnMainThread(); + mNavigatorProperties.mAppVersionOverridden = aValue; +} + +void +RuntimeService::UpdatePlatformOverridePreference(const nsAString& aValue) +{ + AssertIsOnMainThread(); + mNavigatorProperties.mPlatformOverridden = aValue; +} + +void +RuntimeService::UpdateAllWorkerPreference(WorkerPreference aPref, bool aValue) +{ + BROADCAST_ALL_WORKERS(UpdatePreference, aPref, aValue); +} + +void +RuntimeService::UpdateAllWorkerLanguages(const nsTArray<nsString>& aLanguages) +{ + MOZ_ASSERT(NS_IsMainThread()); + + mNavigatorProperties.mLanguages = aLanguages; + BROADCAST_ALL_WORKERS(UpdateLanguages, aLanguages); +} + +void +RuntimeService::UpdateAllWorkerMemoryParameter(JSGCParamKey aKey, + uint32_t aValue) +{ + BROADCAST_ALL_WORKERS(UpdateJSWorkerMemoryParameter, aKey, aValue); +} + +#ifdef JS_GC_ZEAL +void +RuntimeService::UpdateAllWorkerGCZeal() +{ + BROADCAST_ALL_WORKERS(UpdateGCZeal, sDefaultJSSettings.gcZeal, + sDefaultJSSettings.gcZealFrequency); +} +#endif + +void +RuntimeService::GarbageCollectAllWorkers(bool aShrinking) +{ + BROADCAST_ALL_WORKERS(GarbageCollect, aShrinking); +} + +void +RuntimeService::CycleCollectAllWorkers() +{ + BROADCAST_ALL_WORKERS(CycleCollect, /* dummy = */ false); +} + +void +RuntimeService::SendOfflineStatusChangeEventToAllWorkers(bool aIsOffline) +{ + BROADCAST_ALL_WORKERS(OfflineStatusChangeEvent, aIsOffline); +} + +void +RuntimeService::MemoryPressureAllWorkers() +{ + BROADCAST_ALL_WORKERS(MemoryPressure, /* dummy = */ false); +} + +uint32_t +RuntimeService::ClampedHardwareConcurrency() const +{ + // This needs to be atomic, because multiple workers, and even mainthread, + // could race to initialize it at once. + static Atomic<uint32_t> clampedHardwareConcurrency; + + // No need to loop here: if compareExchange fails, that just means that some + // other worker has initialized numberOfProcessors, so we're good to go. + if (!clampedHardwareConcurrency) { + int32_t numberOfProcessors = PR_GetNumberOfProcessors(); + if (numberOfProcessors <= 0) { + numberOfProcessors = 1; // Must be one there somewhere + } + uint32_t clampedValue = std::min(uint32_t(numberOfProcessors), + gMaxHardwareConcurrency); + clampedHardwareConcurrency.compareExchange(0, clampedValue); + } + + return clampedHardwareConcurrency; +} + +// nsISupports +NS_IMPL_ISUPPORTS(RuntimeService, nsIObserver) + +// nsIObserver +NS_IMETHODIMP +RuntimeService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) +{ + AssertIsOnMainThread(); + + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + Shutdown(); + return NS_OK; + } + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID)) { + Cleanup(); + return NS_OK; + } + if (!strcmp(aTopic, GC_REQUEST_OBSERVER_TOPIC)) { + GarbageCollectAllWorkers(/* shrinking = */ false); + return NS_OK; + } + if (!strcmp(aTopic, CC_REQUEST_OBSERVER_TOPIC)) { + CycleCollectAllWorkers(); + return NS_OK; + } + if (!strcmp(aTopic, MEMORY_PRESSURE_OBSERVER_TOPIC)) { + GarbageCollectAllWorkers(/* shrinking = */ true); + CycleCollectAllWorkers(); + MemoryPressureAllWorkers(); + return NS_OK; + } + if (!strcmp(aTopic, NS_IOSERVICE_OFFLINE_STATUS_TOPIC)) { + SendOfflineStatusChangeEventToAllWorkers(NS_IsOffline()); + return NS_OK; + } + + NS_NOTREACHED("Unknown observer topic!"); + return NS_OK; +} + +/* static */ void +RuntimeService::WorkerPrefChanged(const char* aPrefName, void* aClosure) +{ + AssertIsOnMainThread(); + + const WorkerPreference key = + static_cast<WorkerPreference>(reinterpret_cast<uintptr_t>(aClosure)); + + switch (key) { +#define WORKER_SIMPLE_PREF(name, getter, NAME) case WORKERPREF_##NAME: +#define WORKER_PREF(name, callback) +#include "WorkerPrefs.h" +#undef WORKER_SIMPLE_PREF +#undef WORKER_PREF + sDefaultPreferences[key] = Preferences::GetBool(aPrefName, false); + break; + + default: + MOZ_ASSERT_UNREACHABLE("Invalid pref key"); + break; + } + + RuntimeService* rts = RuntimeService::GetService(); + if (rts) { + rts->UpdateAllWorkerPreference(key, sDefaultPreferences[key]); + } +} + +void +RuntimeService::JSVersionChanged(const char* /* aPrefName */, void* /* aClosure */) +{ + AssertIsOnMainThread(); + + bool useLatest = Preferences::GetBool("dom.workers.latestJSVersion", false); + JS::CompartmentOptions& options = sDefaultJSSettings.content.compartmentOptions; + options.behaviors().setVersion(useLatest ? JSVERSION_LATEST : JSVERSION_DEFAULT); +} + +bool +LogViolationDetailsRunnable::MainThreadRun() +{ + AssertIsOnMainThread(); + + nsIContentSecurityPolicy* csp = mWorkerPrivate->GetCSP(); + if (csp) { + NS_NAMED_LITERAL_STRING(scriptSample, + "Call to eval() or related function blocked by CSP."); + if (mWorkerPrivate->GetReportCSPViolations()) { + csp->LogViolationDetails(nsIContentSecurityPolicy::VIOLATION_TYPE_EVAL, + mFileName, scriptSample, mLineNum, + EmptyString(), EmptyString()); + } + } + + return true; +} + +NS_IMPL_ISUPPORTS_INHERITED0(WorkerThreadPrimaryRunnable, Runnable) + +NS_IMETHODIMP +WorkerThreadPrimaryRunnable::Run() +{ + using mozilla::ipc::BackgroundChild; + + char stackBaseGuess; + + PR_SetCurrentThreadName("DOM Worker"); + + nsAutoCString threadName; + threadName.AssignLiteral("DOM Worker '"); + threadName.Append(NS_LossyConvertUTF16toASCII(mWorkerPrivate->ScriptURL())); + threadName.Append('\''); + + profiler_register_thread(threadName.get(), &stackBaseGuess); + + // Note: SynchronouslyCreateForCurrentThread() must be called prior to + // mWorkerPrivate->SetThread() in order to avoid accidentally consuming + // worker messages here. + if (NS_WARN_IF(!BackgroundChild::SynchronouslyCreateForCurrentThread())) { + // XXX need to fire an error at parent. + // Failed in creating BackgroundChild: probably in shutdown. Continue to run + // without BackgroundChild created. + } + + class MOZ_STACK_CLASS SetThreadHelper final + { + // Raw pointer: this class is on the stack. + WorkerPrivate* mWorkerPrivate; + + public: + SetThreadHelper(WorkerPrivate* aWorkerPrivate, WorkerThread* aThread) + : mWorkerPrivate(aWorkerPrivate) + { + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aThread); + + mWorkerPrivate->SetThread(aThread); + } + + ~SetThreadHelper() + { + if (mWorkerPrivate) { + mWorkerPrivate->SetThread(nullptr); + } + } + + void Nullify() + { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->SetThread(nullptr); + mWorkerPrivate = nullptr; + } + }; + + SetThreadHelper threadHelper(mWorkerPrivate, mThread); + + mWorkerPrivate->AssertIsOnWorkerThread(); + + { + nsCycleCollector_startup(); + + WorkerJSContext context(mWorkerPrivate); + nsresult rv = context.Initialize(mParentContext); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + JSContext* cx = context.Context(); + + if (!InitJSContextForWorker(mWorkerPrivate, cx)) { + // XXX need to fire an error at parent. + NS_ERROR("Failed to create context!"); + return NS_ERROR_FAILURE; + } + + { +#ifdef MOZ_ENABLE_PROFILER_SPS + PseudoStack* stack = mozilla_get_pseudo_stack(); + if (stack) { + stack->sampleContext(cx); + } +#endif + + { + JSAutoRequest ar(cx); + + mWorkerPrivate->DoRunLoop(cx); + // The AutoJSAPI in DoRunLoop should have reported any exceptions left + // on cx. Note that we still need the JSAutoRequest above because + // AutoJSAPI on workers does NOT enter a request! + MOZ_ASSERT(!JS_IsExceptionPending(cx)); + } + + BackgroundChild::CloseForCurrentThread(); + +#ifdef MOZ_ENABLE_PROFILER_SPS + if (stack) { + stack->sampleContext(nullptr); + } +#endif + } + + // There may still be runnables on the debugger event queue that hold a + // strong reference to the debugger global scope. These runnables are not + // visible to the cycle collector, so we need to make sure to clear the + // debugger event queue before we try to destroy the context. If we don't, + // the garbage collector will crash. + mWorkerPrivate->ClearDebuggerEventQueue(); + + // Perform a full GC. This will collect the main worker global and CC, + // which should break all cycles that touch JS. + JS_GC(cx); + + // Before shutting down the cycle collector we need to do one more pass + // through the event loop to clean up any C++ objects that need deferred + // cleanup. + mWorkerPrivate->ClearMainEventQueue(WorkerPrivate::WorkerRan); + + // Now WorkerJSContext goes out of scope and its destructor will shut + // down the cycle collector. This breaks any remaining cycles and collects + // any remaining C++ objects. + } + + threadHelper.Nullify(); + + mWorkerPrivate->ScheduleDeletion(WorkerPrivate::WorkerRan); + + // It is no longer safe to touch mWorkerPrivate. + mWorkerPrivate = nullptr; + + // Now recycle this thread. + nsCOMPtr<nsIThread> mainThread = do_GetMainThread(); + MOZ_ASSERT(mainThread); + + RefPtr<FinishedRunnable> finishedRunnable = + new FinishedRunnable(mThread.forget()); + MOZ_ALWAYS_SUCCEEDS(mainThread->Dispatch(finishedRunnable, + NS_DISPATCH_NORMAL)); + + profiler_unregister_thread(); + return NS_OK; +} + +NS_IMPL_ISUPPORTS_INHERITED0(WorkerThreadPrimaryRunnable::FinishedRunnable, + Runnable) + +NS_IMETHODIMP +WorkerThreadPrimaryRunnable::FinishedRunnable::Run() +{ + AssertIsOnMainThread(); + + RefPtr<WorkerThread> thread; + mThread.swap(thread); + + RuntimeService* rts = RuntimeService::GetService(); + if (rts) { + rts->NoteIdleThread(thread); + } + else if (thread->ShutdownRequired()) { + MOZ_ALWAYS_SUCCEEDS(thread->Shutdown()); + } + + return NS_OK; +} diff --git a/dom/workers/RuntimeService.h b/dom/workers/RuntimeService.h new file mode 100644 index 000000000..2e5cc1dad --- /dev/null +++ b/dom/workers/RuntimeService.h @@ -0,0 +1,287 @@ +/* -*- 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_workers_runtimeservice_h__ +#define mozilla_dom_workers_runtimeservice_h__ + +#include "Workers.h" + +#include "nsIObserver.h" + +#include "mozilla/dom/BindingDeclarations.h" +#include "nsClassHashtable.h" +#include "nsHashKeys.h" +#include "nsTArray.h" + +class nsITimer; +class nsPIDOMWindowInner; + +BEGIN_WORKERS_NAMESPACE + +class SharedWorker; +class WorkerThread; + +class RuntimeService final : public nsIObserver +{ + struct SharedWorkerInfo + { + WorkerPrivate* mWorkerPrivate; + nsCString mScriptSpec; + nsCString mName; + + SharedWorkerInfo(WorkerPrivate* aWorkerPrivate, + const nsACString& aScriptSpec, + const nsACString& aName) + : mWorkerPrivate(aWorkerPrivate), mScriptSpec(aScriptSpec), mName(aName) + { } + }; + + struct WorkerDomainInfo + { + nsCString mDomain; + nsTArray<WorkerPrivate*> mActiveWorkers; + nsTArray<WorkerPrivate*> mActiveServiceWorkers; + nsTArray<WorkerPrivate*> mQueuedWorkers; + nsClassHashtable<nsCStringHashKey, SharedWorkerInfo> mSharedWorkerInfos; + uint32_t mChildWorkerCount; + + WorkerDomainInfo() + : mActiveWorkers(1), mChildWorkerCount(0) + { } + + uint32_t + ActiveWorkerCount() const + { + return mActiveWorkers.Length() + + mChildWorkerCount; + } + + uint32_t + ActiveServiceWorkerCount() const + { + return mActiveServiceWorkers.Length(); + } + + bool + HasNoWorkers() const + { + return ActiveWorkerCount() == 0 && + ActiveServiceWorkerCount() == 0; + } + }; + + struct IdleThreadInfo; + + mozilla::Mutex mMutex; + + // Protected by mMutex. + nsClassHashtable<nsCStringHashKey, WorkerDomainInfo> mDomainMap; + + // Protected by mMutex. + nsTArray<IdleThreadInfo> mIdleThreadArray; + + // *Not* protected by mMutex. + nsClassHashtable<nsPtrHashKey<nsPIDOMWindowInner>, + nsTArray<WorkerPrivate*> > mWindowMap; + + // Only used on the main thread. + nsCOMPtr<nsITimer> mIdleThreadTimer; + + static JSSettings sDefaultJSSettings; + static bool sDefaultPreferences[WORKERPREF_COUNT]; + +public: + struct NavigatorProperties + { + nsString mAppName; + nsString mAppNameOverridden; + nsString mAppVersion; + nsString mAppVersionOverridden; + nsString mPlatform; + nsString mPlatformOverridden; + nsTArray<nsString> mLanguages; + }; + +private: + NavigatorProperties mNavigatorProperties; + + // True when the observer service holds a reference to this object. + bool mObserved; + bool mShuttingDown; + bool mNavigatorPropertiesLoaded; + +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + static RuntimeService* + GetOrCreateService(); + + static RuntimeService* + GetService(); + + bool + RegisterWorker(WorkerPrivate* aWorkerPrivate); + + void + UnregisterWorker(WorkerPrivate* aWorkerPrivate); + + void + RemoveSharedWorker(WorkerDomainInfo* aDomainInfo, + WorkerPrivate* aWorkerPrivate); + + void + CancelWorkersForWindow(nsPIDOMWindowInner* aWindow); + + void + FreezeWorkersForWindow(nsPIDOMWindowInner* aWindow); + + void + ThawWorkersForWindow(nsPIDOMWindowInner* aWindow); + + void + SuspendWorkersForWindow(nsPIDOMWindowInner* aWindow); + + void + ResumeWorkersForWindow(nsPIDOMWindowInner* aWindow); + + nsresult + CreateSharedWorker(const GlobalObject& aGlobal, + const nsAString& aScriptURL, + const nsACString& aName, + SharedWorker** aSharedWorker); + + void + ForgetSharedWorker(WorkerPrivate* aWorkerPrivate); + + const NavigatorProperties& + GetNavigatorProperties() const + { + return mNavigatorProperties; + } + + void + NoteIdleThread(WorkerThread* aThread); + + static void + GetDefaultJSSettings(JSSettings& aSettings) + { + AssertIsOnMainThread(); + aSettings = sDefaultJSSettings; + } + + static void + GetDefaultPreferences(bool aPreferences[WORKERPREF_COUNT]) + { + AssertIsOnMainThread(); + memcpy(aPreferences, sDefaultPreferences, WORKERPREF_COUNT * sizeof(bool)); + } + + static void + SetDefaultContextOptions(const JS::ContextOptions& aContextOptions) + { + AssertIsOnMainThread(); + sDefaultJSSettings.contextOptions = aContextOptions; + } + + void + UpdateAppNameOverridePreference(const nsAString& aValue); + + void + UpdateAppVersionOverridePreference(const nsAString& aValue); + + void + UpdatePlatformOverridePreference(const nsAString& aValue); + + void + UpdateAllWorkerContextOptions(); + + void + UpdateAllWorkerLanguages(const nsTArray<nsString>& aLanguages); + + void + UpdateAllWorkerPreference(WorkerPreference aPref, bool aValue); + + static void + SetDefaultJSGCSettings(JSGCParamKey aKey, uint32_t aValue) + { + AssertIsOnMainThread(); + sDefaultJSSettings.ApplyGCSetting(aKey, aValue); + } + + void + UpdateAllWorkerMemoryParameter(JSGCParamKey aKey, uint32_t aValue); + +#ifdef JS_GC_ZEAL + static void + SetDefaultGCZeal(uint8_t aGCZeal, uint32_t aFrequency) + { + AssertIsOnMainThread(); + sDefaultJSSettings.gcZeal = aGCZeal; + sDefaultJSSettings.gcZealFrequency = aFrequency; + } + + void + UpdateAllWorkerGCZeal(); +#endif + + void + GarbageCollectAllWorkers(bool aShrinking); + + void + CycleCollectAllWorkers(); + + void + SendOfflineStatusChangeEventToAllWorkers(bool aIsOffline); + + void + MemoryPressureAllWorkers(); + + uint32_t ClampedHardwareConcurrency() const; + +private: + RuntimeService(); + ~RuntimeService(); + + nsresult + Init(); + + void + Shutdown(); + + void + Cleanup(); + + void + AddAllTopLevelWorkersToArray(nsTArray<WorkerPrivate*>& aWorkers); + + void + GetWorkersForWindow(nsPIDOMWindowInner* aWindow, + nsTArray<WorkerPrivate*>& aWorkers); + + bool + ScheduleWorker(WorkerPrivate* aWorkerPrivate); + + static void + ShutdownIdleThreads(nsITimer* aTimer, void* aClosure); + + static void + WorkerPrefChanged(const char* aPrefName, void* aClosure); + + static void + JSVersionChanged(const char* aPrefName, void* aClosure); + + nsresult + CreateSharedWorkerFromLoadInfo(JSContext* aCx, + WorkerLoadInfo* aLoadInfo, + const nsAString& aScriptURL, + const nsACString& aName, + SharedWorker** aSharedWorker); +}; + +END_WORKERS_NAMESPACE + +#endif /* mozilla_dom_workers_runtimeservice_h__ */ diff --git a/dom/workers/ScriptLoader.cpp b/dom/workers/ScriptLoader.cpp new file mode 100644 index 000000000..46545e737 --- /dev/null +++ b/dom/workers/ScriptLoader.cpp @@ -0,0 +1,2290 @@ +/* -*- 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 "nsIChannel.h" +#include "nsIContentPolicy.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIDocShell.h" +#include "nsIDOMDocument.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIInputStreamPump.h" +#include "nsIIOService.h" +#include "nsIProtocolHandler.h" +#include "nsIScriptError.h" +#include "nsIScriptSecurityManager.h" +#include "nsIStreamLoader.h" +#include "nsIStreamListenerTee.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsIURI.h" + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "nsError.h" +#include "nsContentPolicyUtils.h" +#include "nsContentUtils.h" +#include "nsDocShellCID.h" +#include "nsISupportsPrimitives.h" +#include "nsNetUtil.h" +#include "nsIPipe.h" +#include "nsIOutputStream.h" +#include "nsPrintfCString.h" +#include "nsScriptLoader.h" +#include "nsString.h" +#include "nsStreamUtils.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "nsXPCOM.h" +#include "xpcpublic.h" + +#include "mozilla/Assertions.h" +#include "mozilla/LoadContext.h" +#include "mozilla/Maybe.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/dom/CacheBinding.h" +#include "mozilla/dom/cache/CacheTypes.h" +#include "mozilla/dom/cache/Cache.h" +#include "mozilla/dom/cache/CacheStorage.h" +#include "mozilla/dom/ChannelInfo.h" +#include "mozilla/dom/Exceptions.h" +#include "mozilla/dom/InternalResponse.h" +#include "mozilla/dom/nsCSPService.h" +#include "mozilla/dom/nsCSPUtils.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/SRILogHelper.h" +#include "mozilla/UniquePtr.h" +#include "Principal.h" +#include "WorkerHolder.h" +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" +#include "WorkerScope.h" + +#define MAX_CONCURRENT_SCRIPTS 1000 + +USING_WORKERS_NAMESPACE + +using namespace mozilla; +using namespace mozilla::dom; +using mozilla::dom::cache::Cache; +using mozilla::dom::cache::CacheStorage; +using mozilla::ipc::PrincipalInfo; + +namespace { + +nsIURI* +GetBaseURI(bool aIsMainScript, WorkerPrivate* aWorkerPrivate) +{ + MOZ_ASSERT(aWorkerPrivate); + nsIURI* baseURI; + WorkerPrivate* parentWorker = aWorkerPrivate->GetParent(); + if (aIsMainScript) { + if (parentWorker) { + baseURI = parentWorker->GetBaseURI(); + NS_ASSERTION(baseURI, "Should have been set already!"); + } + else { + // May be null. + baseURI = aWorkerPrivate->GetBaseURI(); + } + } + else { + baseURI = aWorkerPrivate->GetBaseURI(); + NS_ASSERTION(baseURI, "Should have been set already!"); + } + + return baseURI; +} + +nsresult +ChannelFromScriptURL(nsIPrincipal* principal, + nsIURI* baseURI, + nsIDocument* parentDoc, + nsILoadGroup* loadGroup, + nsIIOService* ios, + nsIScriptSecurityManager* secMan, + const nsAString& aScriptURL, + bool aIsMainScript, + WorkerScriptType aWorkerScriptType, + nsContentPolicyType aContentPolicyType, + nsLoadFlags aLoadFlags, + bool aDefaultURIEncoding, + nsIChannel** aChannel) +{ + AssertIsOnMainThread(); + + nsresult rv; + nsCOMPtr<nsIURI> uri; + + if (aDefaultURIEncoding) { + rv = NS_NewURI(getter_AddRefs(uri), aScriptURL, nullptr, baseURI); + } else { + rv = nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(uri), + aScriptURL, parentDoc, + baseURI); + } + + if (NS_FAILED(rv)) { + return NS_ERROR_DOM_SYNTAX_ERR; + } + + // If we have the document, use it. Unfortunately, for dedicated workers + // 'parentDoc' ends up being the parent document, which is not the document + // that we want to use. So make sure to avoid using 'parentDoc' in that + // situation. + if (parentDoc && parentDoc->NodePrincipal() != principal) { + parentDoc = nullptr; + } + + aLoadFlags |= nsIChannel::LOAD_CLASSIFY_URI; + uint32_t secFlags = aIsMainScript ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED + : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS; + + if (aWorkerScriptType == DebuggerScript) { + // A DebuggerScript needs to be a local resource like chrome: or resource: + bool isUIResource = false; + rv = NS_URIChainHasFlags(uri, nsIProtocolHandler::URI_IS_UI_RESOURCE, + &isUIResource); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!isUIResource) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + secFlags |= nsILoadInfo::SEC_ALLOW_CHROME; + } + + // Note: this is for backwards compatibility and goes against spec. + // We should find a better solution. + bool isData = false; + if (aIsMainScript && NS_SUCCEEDED(uri->SchemeIs("data", &isData)) && isData) { + secFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL; + } + + nsCOMPtr<nsIChannel> channel; + // If we have the document, use it. Unfortunately, for dedicated workers + // 'parentDoc' ends up being the parent document, which is not the document + // that we want to use. So make sure to avoid using 'parentDoc' in that + // situation. + if (parentDoc && parentDoc->NodePrincipal() == principal) { + rv = NS_NewChannel(getter_AddRefs(channel), + uri, + parentDoc, + secFlags, + aContentPolicyType, + loadGroup, + nullptr, // aCallbacks + aLoadFlags, + ios); + } else { + // We must have a loadGroup with a load context for the principal to + // traverse the channel correctly. + MOZ_ASSERT(loadGroup); + MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(loadGroup, principal)); + + rv = NS_NewChannel(getter_AddRefs(channel), + uri, + principal, + secFlags, + aContentPolicyType, + loadGroup, + nullptr, // aCallbacks + aLoadFlags, + ios); + } + + NS_ENSURE_SUCCESS(rv, rv); + + if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel)) { + rv = nsContentUtils::SetFetchReferrerURIWithPolicy(principal, parentDoc, + httpChannel, mozilla::net::RP_Default); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + channel.forget(aChannel); + return rv; +} + +struct ScriptLoadInfo +{ + ScriptLoadInfo() + : mScriptTextBuf(nullptr) + , mScriptTextLength(0) + , mLoadResult(NS_ERROR_NOT_INITIALIZED) + , mLoadingFinished(false) + , mExecutionScheduled(false) + , mExecutionResult(false) + , mCacheStatus(Uncached) + { } + + ~ScriptLoadInfo() + { + if (mScriptTextBuf) { + js_free(mScriptTextBuf); + } + } + + bool + ReadyToExecute() + { + return !mChannel && NS_SUCCEEDED(mLoadResult) && !mExecutionScheduled; + } + + nsString mURL; + + // This full URL string is populated only if this object is used in a + // ServiceWorker. + nsString mFullURL; + + // This promise is set only when the script is for a ServiceWorker but + // it's not in the cache yet. The promise is resolved when the full body is + // stored into the cache. mCachePromise will be set to nullptr after + // resolution. + RefPtr<Promise> mCachePromise; + + // The reader stream the cache entry should be filled from, for those cases + // when we're going to have an mCachePromise. + nsCOMPtr<nsIInputStream> mCacheReadStream; + + nsCOMPtr<nsIChannel> mChannel; + char16_t* mScriptTextBuf; + size_t mScriptTextLength; + + nsresult mLoadResult; + bool mLoadingFinished; + bool mExecutionScheduled; + bool mExecutionResult; + + enum CacheStatus { + // By default a normal script is just loaded from the network. But for + // ServiceWorkers, we have to check if the cache contains the script and + // load it from the cache. + Uncached, + + WritingToCache, + + ReadingFromCache, + + // This script has been loaded from the ServiceWorker cache. + Cached, + + // This script must be stored in the ServiceWorker cache. + ToBeCached, + + // Something went wrong or the worker went away. + Cancel + }; + + CacheStatus mCacheStatus; + + Maybe<bool> mMutedErrorFlag; + + bool Finished() const + { + return mLoadingFinished && !mCachePromise && !mChannel; + } +}; + +class ScriptLoaderRunnable; + +class ScriptExecutorRunnable final : public MainThreadWorkerSyncRunnable +{ + ScriptLoaderRunnable& mScriptLoader; + bool mIsWorkerScript; + uint32_t mFirstIndex; + uint32_t mLastIndex; + +public: + ScriptExecutorRunnable(ScriptLoaderRunnable& aScriptLoader, + nsIEventTarget* aSyncLoopTarget, + bool aIsWorkerScript, + uint32_t aFirstIndex, + uint32_t aLastIndex); + +private: + ~ScriptExecutorRunnable() + { } + + virtual bool + IsDebuggerRunnable() const override; + + virtual bool + PreRun(WorkerPrivate* aWorkerPrivate) override; + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override; + + virtual void + PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aRunResult) + override; + + nsresult + Cancel() override; + + void + ShutdownScriptLoader(JSContext* aCx, + WorkerPrivate* aWorkerPrivate, + bool aResult, + bool aMutedError); + + void LogExceptionToConsole(JSContext* aCx, + WorkerPrivate* WorkerPrivate); +}; + +class CacheScriptLoader; + +class CacheCreator final : public PromiseNativeHandler +{ +public: + NS_DECL_ISUPPORTS + + explicit CacheCreator(WorkerPrivate* aWorkerPrivate) + : mCacheName(aWorkerPrivate->ServiceWorkerCacheName()) + , mOriginAttributes(aWorkerPrivate->GetOriginAttributes()) + { + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + MOZ_ASSERT(aWorkerPrivate->LoadScriptAsPartOfLoadingServiceWorkerScript()); + AssertIsOnMainThread(); + } + + void + AddLoader(CacheScriptLoader* aLoader) + { + AssertIsOnMainThread(); + MOZ_ASSERT(!mCacheStorage); + mLoaders.AppendElement(aLoader); + } + + virtual void + ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; + + virtual void + RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; + + // Try to load from cache with aPrincipal used for cache access. + nsresult + Load(nsIPrincipal* aPrincipal); + + Cache* + Cache_() const + { + AssertIsOnMainThread(); + MOZ_ASSERT(mCache); + return mCache; + } + + nsIGlobalObject* + Global() const + { + AssertIsOnMainThread(); + MOZ_ASSERT(mSandboxGlobalObject); + return mSandboxGlobalObject; + } + + void + DeleteCache(); + +private: + ~CacheCreator() + { + } + + nsresult + CreateCacheStorage(nsIPrincipal* aPrincipal); + + void + FailLoaders(nsresult aRv); + + RefPtr<Cache> mCache; + RefPtr<CacheStorage> mCacheStorage; + nsCOMPtr<nsIGlobalObject> mSandboxGlobalObject; + nsTArray<RefPtr<CacheScriptLoader>> mLoaders; + + nsString mCacheName; + PrincipalOriginAttributes mOriginAttributes; +}; + +NS_IMPL_ISUPPORTS0(CacheCreator) + +class CacheScriptLoader final : public PromiseNativeHandler + , public nsIStreamLoaderObserver +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLOADEROBSERVER + + CacheScriptLoader(WorkerPrivate* aWorkerPrivate, ScriptLoadInfo& aLoadInfo, + uint32_t aIndex, bool aIsWorkerScript, + ScriptLoaderRunnable* aRunnable) + : mLoadInfo(aLoadInfo) + , mIndex(aIndex) + , mRunnable(aRunnable) + , mIsWorkerScript(aIsWorkerScript) + , mFailed(false) + { + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + mBaseURI = GetBaseURI(mIsWorkerScript, aWorkerPrivate); + AssertIsOnMainThread(); + } + + void + Fail(nsresult aRv); + + void + Load(Cache* aCache); + + virtual void + ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; + + virtual void + RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; + +private: + ~CacheScriptLoader() + { + AssertIsOnMainThread(); + } + + ScriptLoadInfo& mLoadInfo; + uint32_t mIndex; + RefPtr<ScriptLoaderRunnable> mRunnable; + bool mIsWorkerScript; + bool mFailed; + nsCOMPtr<nsIInputStreamPump> mPump; + nsCOMPtr<nsIURI> mBaseURI; + mozilla::dom::ChannelInfo mChannelInfo; + UniquePtr<PrincipalInfo> mPrincipalInfo; +}; + +NS_IMPL_ISUPPORTS(CacheScriptLoader, nsIStreamLoaderObserver) + +class CachePromiseHandler final : public PromiseNativeHandler +{ +public: + NS_DECL_ISUPPORTS + + CachePromiseHandler(ScriptLoaderRunnable* aRunnable, + ScriptLoadInfo& aLoadInfo, + uint32_t aIndex) + : mRunnable(aRunnable) + , mLoadInfo(aLoadInfo) + , mIndex(aIndex) + { + AssertIsOnMainThread(); + MOZ_ASSERT(mRunnable); + } + + virtual void + ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; + + virtual void + RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; + +private: + ~CachePromiseHandler() + { + AssertIsOnMainThread(); + } + + RefPtr<ScriptLoaderRunnable> mRunnable; + ScriptLoadInfo& mLoadInfo; + uint32_t mIndex; +}; + +NS_IMPL_ISUPPORTS0(CachePromiseHandler) + +class LoaderListener final : public nsIStreamLoaderObserver + , public nsIRequestObserver +{ +public: + NS_DECL_ISUPPORTS + + LoaderListener(ScriptLoaderRunnable* aRunnable, uint32_t aIndex) + : mRunnable(aRunnable) + , mIndex(aIndex) + { + MOZ_ASSERT(mRunnable); + } + + NS_IMETHOD + OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, + nsresult aStatus, uint32_t aStringLen, + const uint8_t* aString) override; + + NS_IMETHOD + OnStartRequest(nsIRequest* aRequest, nsISupports* aContext) override; + + NS_IMETHOD + OnStopRequest(nsIRequest* aRequest, nsISupports* aContext, + nsresult aStatusCode) override + { + // Nothing to do here! + return NS_OK; + } + +private: + ~LoaderListener() {} + + RefPtr<ScriptLoaderRunnable> mRunnable; + uint32_t mIndex; +}; + +NS_IMPL_ISUPPORTS(LoaderListener, nsIStreamLoaderObserver, nsIRequestObserver) + +class ScriptLoaderHolder; + +class ScriptLoaderRunnable final : public nsIRunnable +{ + friend class ScriptExecutorRunnable; + friend class ScriptLoaderHolder; + friend class CachePromiseHandler; + friend class CacheScriptLoader; + friend class LoaderListener; + + WorkerPrivate* mWorkerPrivate; + nsCOMPtr<nsIEventTarget> mSyncLoopTarget; + nsTArray<ScriptLoadInfo> mLoadInfos; + RefPtr<CacheCreator> mCacheCreator; + bool mIsMainScript; + WorkerScriptType mWorkerScriptType; + bool mCanceled; + bool mCanceledMainThread; + ErrorResult& mRv; + +public: + NS_DECL_THREADSAFE_ISUPPORTS + + ScriptLoaderRunnable(WorkerPrivate* aWorkerPrivate, + nsIEventTarget* aSyncLoopTarget, + nsTArray<ScriptLoadInfo>& aLoadInfos, + bool aIsMainScript, + WorkerScriptType aWorkerScriptType, + ErrorResult& aRv) + : mWorkerPrivate(aWorkerPrivate), mSyncLoopTarget(aSyncLoopTarget), + mIsMainScript(aIsMainScript), mWorkerScriptType(aWorkerScriptType), + mCanceled(false), mCanceledMainThread(false), mRv(aRv) + { + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aSyncLoopTarget); + MOZ_ASSERT_IF(aIsMainScript, aLoadInfos.Length() == 1); + + mLoadInfos.SwapElements(aLoadInfos); + } + +private: + ~ScriptLoaderRunnable() + { } + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + + nsresult rv = RunInternal(); + if (NS_WARN_IF(NS_FAILED(rv))) { + CancelMainThread(rv); + } + + return NS_OK; + } + + void + LoadingFinished(uint32_t aIndex, nsresult aRv) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aIndex < mLoadInfos.Length()); + ScriptLoadInfo& loadInfo = mLoadInfos[aIndex]; + + loadInfo.mLoadResult = aRv; + + MOZ_ASSERT(!loadInfo.mLoadingFinished); + loadInfo.mLoadingFinished = true; + + MaybeExecuteFinishedScripts(aIndex); + } + + void + MaybeExecuteFinishedScripts(uint32_t aIndex) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aIndex < mLoadInfos.Length()); + ScriptLoadInfo& loadInfo = mLoadInfos[aIndex]; + + // We execute the last step if we don't have a pending operation with the + // cache and the loading is completed. + if (loadInfo.Finished()) { + ExecuteFinishedScripts(); + } + } + + nsresult + OnStreamComplete(nsIStreamLoader* aLoader, uint32_t aIndex, + nsresult aStatus, uint32_t aStringLen, + const uint8_t* aString) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aIndex < mLoadInfos.Length()); + + nsresult rv = OnStreamCompleteInternal(aLoader, aStatus, aStringLen, + aString, mLoadInfos[aIndex]); + LoadingFinished(aIndex, rv); + return NS_OK; + } + + nsresult + OnStartRequest(nsIRequest* aRequest, uint32_t aIndex) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aIndex < mLoadInfos.Length()); + + // If one load info cancels or hits an error, it can race with the start + // callback coming from another load info. + if (mCanceledMainThread || !mCacheCreator) { + aRequest->Cancel(NS_ERROR_FAILURE); + return NS_ERROR_FAILURE; + } + + ScriptLoadInfo& loadInfo = mLoadInfos[aIndex]; + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + MOZ_ASSERT(channel == loadInfo.mChannel); + + // We synthesize the result code, but its never exposed to content. + RefPtr<mozilla::dom::InternalResponse> ir = + new mozilla::dom::InternalResponse(200, NS_LITERAL_CSTRING("OK")); + ir->SetBody(loadInfo.mCacheReadStream, InternalResponse::UNKNOWN_BODY_SIZE); + // Drop our reference to the stream now that we've passed it along, so it + // doesn't hang around once the cache is done with it and keep data alive. + loadInfo.mCacheReadStream = nullptr; + + // Set the channel info of the channel on the response so that it's + // saved in the cache. + ir->InitChannelInfo(channel); + + // Save the principal of the channel since its URI encodes the script URI + // rather than the ServiceWorkerRegistrationInfo URI. + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + NS_ASSERTION(ssm, "Should never be null!"); + + nsCOMPtr<nsIPrincipal> channelPrincipal; + nsresult rv = ssm->GetChannelResultPrincipal(channel, getter_AddRefs(channelPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + channel->Cancel(rv); + return rv; + } + + UniquePtr<PrincipalInfo> principalInfo(new PrincipalInfo()); + rv = PrincipalToPrincipalInfo(channelPrincipal, principalInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + channel->Cancel(rv); + return rv; + } + + ir->SetPrincipalInfo(Move(principalInfo)); + + RefPtr<mozilla::dom::Response> response = + new mozilla::dom::Response(mCacheCreator->Global(), ir); + + mozilla::dom::RequestOrUSVString request; + + MOZ_ASSERT(!loadInfo.mFullURL.IsEmpty()); + request.SetAsUSVString().Rebind(loadInfo.mFullURL.Data(), + loadInfo.mFullURL.Length()); + + ErrorResult error; + RefPtr<Promise> cachePromise = + mCacheCreator->Cache_()->Put(request, *response, error); + if (NS_WARN_IF(error.Failed())) { + nsresult rv = error.StealNSResult(); + channel->Cancel(rv); + return rv; + } + + RefPtr<CachePromiseHandler> promiseHandler = + new CachePromiseHandler(this, loadInfo, aIndex); + cachePromise->AppendNativeHandler(promiseHandler); + + loadInfo.mCachePromise.swap(cachePromise); + loadInfo.mCacheStatus = ScriptLoadInfo::WritingToCache; + + return NS_OK; + } + + bool + Notify(Status aStatus) + { + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (aStatus >= Terminating && !mCanceled) { + mCanceled = true; + + MOZ_ALWAYS_SUCCEEDS( + NS_DispatchToMainThread(NewRunnableMethod(this, + &ScriptLoaderRunnable::CancelMainThreadWithBindingAborted))); + } + + return true; + } + + bool + IsMainWorkerScript() const + { + return mIsMainScript && mWorkerScriptType == WorkerScript; + } + + void + CancelMainThreadWithBindingAborted() + { + CancelMainThread(NS_BINDING_ABORTED); + } + + void + CancelMainThread(nsresult aCancelResult) + { + AssertIsOnMainThread(); + + if (mCanceledMainThread) { + return; + } + + mCanceledMainThread = true; + + if (mCacheCreator) { + MOZ_ASSERT(mWorkerPrivate->IsServiceWorker()); + DeleteCache(); + } + + // Cancel all the channels that were already opened. + for (uint32_t index = 0; index < mLoadInfos.Length(); index++) { + ScriptLoadInfo& loadInfo = mLoadInfos[index]; + + // If promise or channel is non-null, their failures will lead to + // LoadingFinished being called. + bool callLoadingFinished = true; + + if (loadInfo.mCachePromise) { + MOZ_ASSERT(mWorkerPrivate->IsServiceWorker()); + loadInfo.mCachePromise->MaybeReject(aCancelResult); + loadInfo.mCachePromise = nullptr; + callLoadingFinished = false; + } + + if (loadInfo.mChannel) { + if (NS_SUCCEEDED(loadInfo.mChannel->Cancel(aCancelResult))) { + callLoadingFinished = false; + } else { + NS_WARNING("Failed to cancel channel!"); + } + } + + if (callLoadingFinished && !loadInfo.Finished()) { + LoadingFinished(index, aCancelResult); + } + } + + ExecuteFinishedScripts(); + } + + void + DeleteCache() + { + AssertIsOnMainThread(); + + if (!mCacheCreator) { + return; + } + + mCacheCreator->DeleteCache(); + mCacheCreator = nullptr; + } + + nsresult + RunInternal() + { + AssertIsOnMainThread(); + + if (IsMainWorkerScript() && mWorkerPrivate->IsServiceWorker()) { + mWorkerPrivate->SetLoadingWorkerScript(true); + } + + if (!mWorkerPrivate->IsServiceWorker() || + !mWorkerPrivate->LoadScriptAsPartOfLoadingServiceWorkerScript()) { + for (uint32_t index = 0, len = mLoadInfos.Length(); index < len; + ++index) { + nsresult rv = LoadScript(index); + if (NS_WARN_IF(NS_FAILED(rv))) { + LoadingFinished(index, rv); + return rv; + } + } + + return NS_OK; + } + + MOZ_ASSERT(!mCacheCreator); + mCacheCreator = new CacheCreator(mWorkerPrivate); + + for (uint32_t index = 0, len = mLoadInfos.Length(); index < len; ++index) { + RefPtr<CacheScriptLoader> loader = + new CacheScriptLoader(mWorkerPrivate, mLoadInfos[index], index, + IsMainWorkerScript(), this); + mCacheCreator->AddLoader(loader); + } + + // The worker may have a null principal on first load, but in that case its + // parent definitely will have one. + nsIPrincipal* principal = mWorkerPrivate->GetPrincipal(); + if (!principal) { + WorkerPrivate* parentWorker = mWorkerPrivate->GetParent(); + MOZ_ASSERT(parentWorker, "Must have a parent!"); + principal = parentWorker->GetPrincipal(); + } + + nsresult rv = mCacheCreator->Load(principal); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + nsresult + LoadScript(uint32_t aIndex) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aIndex < mLoadInfos.Length()); + + WorkerPrivate* parentWorker = mWorkerPrivate->GetParent(); + + // Figure out which principal to use. + nsIPrincipal* principal = mWorkerPrivate->GetPrincipal(); + nsCOMPtr<nsILoadGroup> loadGroup = mWorkerPrivate->GetLoadGroup(); + if (!principal) { + NS_ASSERTION(parentWorker, "Must have a principal!"); + NS_ASSERTION(mIsMainScript, "Must have a principal for importScripts!"); + + principal = parentWorker->GetPrincipal(); + loadGroup = parentWorker->GetLoadGroup(); + } + NS_ASSERTION(principal, "This should never be null here!"); + MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(loadGroup, principal)); + + // Figure out our base URI. + nsCOMPtr<nsIURI> baseURI = GetBaseURI(mIsMainScript, mWorkerPrivate); + + // May be null. + nsCOMPtr<nsIDocument> parentDoc = mWorkerPrivate->GetDocument(); + + nsCOMPtr<nsIChannel> channel; + if (IsMainWorkerScript()) { + // May be null. + channel = mWorkerPrivate->ForgetWorkerChannel(); + } + + nsCOMPtr<nsIIOService> ios(do_GetIOService()); + + nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager(); + NS_ASSERTION(secMan, "This should never be null!"); + + ScriptLoadInfo& loadInfo = mLoadInfos[aIndex]; + nsresult& rv = loadInfo.mLoadResult; + + nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL; + + // Get the top-level worker. + WorkerPrivate* topWorkerPrivate = mWorkerPrivate; + WorkerPrivate* parent = topWorkerPrivate->GetParent(); + while (parent) { + topWorkerPrivate = parent; + parent = topWorkerPrivate->GetParent(); + } + + // If the top-level worker is a dedicated worker and has a window, and the + // window has a docshell, the caching behavior of this worker should match + // that of that docshell. + if (topWorkerPrivate->IsDedicatedWorker()) { + nsCOMPtr<nsPIDOMWindowInner> window = topWorkerPrivate->GetWindow(); + if (window) { + nsCOMPtr<nsIDocShell> docShell = window->GetDocShell(); + if (docShell) { + nsresult rv = docShell->GetDefaultLoadFlags(&loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + + // If we are loading a script for a ServiceWorker then we must not + // try to intercept it. If the interception matches the current + // ServiceWorker's scope then we could deadlock the load. + if (mWorkerPrivate->IsServiceWorker()) { + loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER; + } + + if (!channel) { + // Only top level workers' main script use the document charset for the + // script uri encoding. Otherwise, default encoding (UTF-8) is applied. + bool useDefaultEncoding = !(!parentWorker && IsMainWorkerScript()); + rv = ChannelFromScriptURL(principal, baseURI, parentDoc, loadGroup, ios, + secMan, loadInfo.mURL, IsMainWorkerScript(), + mWorkerScriptType, + mWorkerPrivate->ContentPolicyType(), loadFlags, + useDefaultEncoding, + getter_AddRefs(channel)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // We need to know which index we're on in OnStreamComplete so we know + // where to put the result. + RefPtr<LoaderListener> listener = new LoaderListener(this, aIndex); + + // We don't care about progress so just use the simple stream loader for + // OnStreamComplete notification only. + nsCOMPtr<nsIStreamLoader> loader; + rv = NS_NewStreamLoader(getter_AddRefs(loader), listener); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (loadInfo.mCacheStatus != ScriptLoadInfo::ToBeCached) { + rv = channel->AsyncOpen2(loader); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + nsCOMPtr<nsIOutputStream> writer; + + // In case we return early. + loadInfo.mCacheStatus = ScriptLoadInfo::Cancel; + + rv = NS_NewPipe(getter_AddRefs(loadInfo.mCacheReadStream), + getter_AddRefs(writer), 0, + UINT32_MAX, // unlimited size to avoid writer WOULD_BLOCK case + true, false); // non-blocking reader, blocking writer + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIStreamListenerTee> tee = + do_CreateInstance(NS_STREAMLISTENERTEE_CONTRACTID); + rv = tee->Init(loader, writer, listener); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsresult rv = channel->AsyncOpen2(tee); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + loadInfo.mChannel.swap(channel); + + return NS_OK; + } + + nsresult + OnStreamCompleteInternal(nsIStreamLoader* aLoader, nsresult aStatus, + uint32_t aStringLen, const uint8_t* aString, + ScriptLoadInfo& aLoadInfo) + { + AssertIsOnMainThread(); + + if (!aLoadInfo.mChannel) { + return NS_BINDING_ABORTED; + } + + aLoadInfo.mChannel = nullptr; + + if (NS_FAILED(aStatus)) { + return aStatus; + } + + NS_ASSERTION(aString, "This should never be null!"); + + nsCOMPtr<nsIRequest> request; + nsresult rv = aLoader->GetRequest(getter_AddRefs(request)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request); + MOZ_ASSERT(channel); + + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + NS_ASSERTION(ssm, "Should never be null!"); + + nsCOMPtr<nsIPrincipal> channelPrincipal; + rv = ssm->GetChannelResultPrincipal(channel, getter_AddRefs(channelPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsIPrincipal* principal = mWorkerPrivate->GetPrincipal(); + if (!principal) { + WorkerPrivate* parentWorker = mWorkerPrivate->GetParent(); + MOZ_ASSERT(parentWorker, "Must have a parent!"); + principal = parentWorker->GetPrincipal(); + } + + // We don't mute the main worker script becase we've already done + // same-origin checks on them so we should be able to see their errors. + // Note that for data: url, where we allow it through the same-origin check + // but then give it a different origin. + aLoadInfo.mMutedErrorFlag.emplace(IsMainWorkerScript() + ? false + : !principal->Subsumes(channelPrincipal)); + + // Make sure we're not seeing the result of a 404 or something by checking + // the 'requestSucceeded' attribute on the http channel. + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(request); + nsAutoCString tCspHeaderValue, tCspROHeaderValue, tRPHeaderCValue; + + if (httpChannel) { + bool requestSucceeded; + rv = httpChannel->GetRequestSucceeded(&requestSucceeded); + NS_ENSURE_SUCCESS(rv, rv); + + if (!requestSucceeded) { + return NS_ERROR_NOT_AVAILABLE; + } + + httpChannel->GetResponseHeader( + NS_LITERAL_CSTRING("content-security-policy"), + tCspHeaderValue); + + httpChannel->GetResponseHeader( + NS_LITERAL_CSTRING("content-security-policy-report-only"), + tCspROHeaderValue); + + httpChannel->GetResponseHeader( + NS_LITERAL_CSTRING("referrer-policy"), + tRPHeaderCValue); + } + + // May be null. + nsIDocument* parentDoc = mWorkerPrivate->GetDocument(); + + // Use the regular nsScriptLoader for this grunt work! Should be just fine + // because we're running on the main thread. + // Unlike <script> tags, Worker scripts are always decoded as UTF-8, + // per spec. So we explicitly pass in the charset hint. + rv = nsScriptLoader::ConvertToUTF16(aLoadInfo.mChannel, aString, aStringLen, + NS_LITERAL_STRING("UTF-8"), parentDoc, + aLoadInfo.mScriptTextBuf, + aLoadInfo.mScriptTextLength); + if (NS_FAILED(rv)) { + return rv; + } + + if (!aLoadInfo.mScriptTextLength && !aLoadInfo.mScriptTextBuf) { + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("DOM"), parentDoc, + nsContentUtils::eDOM_PROPERTIES, + "EmptyWorkerSourceWarning"); + } else if (!aLoadInfo.mScriptTextBuf) { + return NS_ERROR_FAILURE; + } + + // Figure out what we actually loaded. + nsCOMPtr<nsIURI> finalURI; + rv = NS_GetFinalChannelURI(channel, getter_AddRefs(finalURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString filename; + rv = finalURI->GetSpec(filename); + NS_ENSURE_SUCCESS(rv, rv); + + if (!filename.IsEmpty()) { + // This will help callers figure out what their script url resolved to in + // case of errors. + aLoadInfo.mURL.Assign(NS_ConvertUTF8toUTF16(filename)); + } + + nsCOMPtr<nsILoadInfo> chanLoadInfo = channel->GetLoadInfo(); + if (chanLoadInfo && chanLoadInfo->GetEnforceSRI()) { + // importScripts() and the Worker constructor do not support integrity metadata + // (or any fetch options). Until then, we can just block. + // If we ever have those data in the future, we'll have to the check to + // by using the SRICheck module + MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug, + ("Scriptloader::Load, SRI required but not supported in workers")); + nsCOMPtr<nsIContentSecurityPolicy> wcsp; + chanLoadInfo->LoadingPrincipal()->GetCsp(getter_AddRefs(wcsp)); + MOZ_ASSERT(wcsp, "We sould have a CSP for the worker here"); + if (wcsp) { + wcsp->LogViolationDetails( + nsIContentSecurityPolicy::VIOLATION_TYPE_REQUIRE_SRI_FOR_SCRIPT, + aLoadInfo.mURL, EmptyString(), 0, EmptyString(), EmptyString()); + } + return NS_ERROR_SRI_CORRUPT; + } + + // Update the principal of the worker and its base URI if we just loaded the + // worker's primary script. + if (IsMainWorkerScript()) { + // Take care of the base URI first. + mWorkerPrivate->SetBaseURI(finalURI); + + // Store the channel info if needed. + mWorkerPrivate->InitChannelInfo(channel); + + // Now to figure out which principal to give this worker. + WorkerPrivate* parent = mWorkerPrivate->GetParent(); + + NS_ASSERTION(mWorkerPrivate->GetPrincipal() || parent, + "Must have one of these!"); + + nsCOMPtr<nsIPrincipal> loadPrincipal = mWorkerPrivate->GetPrincipal() ? + mWorkerPrivate->GetPrincipal() : + parent->GetPrincipal(); + + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + NS_ASSERTION(ssm, "Should never be null!"); + + nsCOMPtr<nsIPrincipal> channelPrincipal; + rv = ssm->GetChannelResultPrincipal(channel, getter_AddRefs(channelPrincipal)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsILoadGroup> channelLoadGroup; + rv = channel->GetLoadGroup(getter_AddRefs(channelLoadGroup)); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(channelLoadGroup); + + // If the load principal is the system principal then the channel + // principal must also be the system principal (we do not allow chrome + // code to create workers with non-chrome scripts, and if we ever decide + // to change this we need to make sure we don't always set + // mPrincipalIsSystem to true in WorkerPrivate::GetLoadInfo()). Otherwise + // this channel principal must be same origin with the load principal (we + // check again here in case redirects changed the location of the script). + if (nsContentUtils::IsSystemPrincipal(loadPrincipal)) { + if (!nsContentUtils::IsSystemPrincipal(channelPrincipal)) { + // See if this is a resource URI. Since JSMs usually come from + // resource:// URIs we're currently considering all URIs with the + // URI_IS_UI_RESOURCE flag as valid for creating privileged workers. + bool isResource; + rv = NS_URIChainHasFlags(finalURI, + nsIProtocolHandler::URI_IS_UI_RESOURCE, + &isResource); + NS_ENSURE_SUCCESS(rv, rv); + + if (isResource) { + // Assign the system principal to the resource:// worker only if it + // was loaded from code using the system principal. + channelPrincipal = loadPrincipal; + } else { + return NS_ERROR_DOM_BAD_URI; + } + } + } + + // The principal can change, but it should still match the original + // load group's appId and browser element flag. + MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(channelLoadGroup, channelPrincipal)); + + mWorkerPrivate->SetPrincipal(channelPrincipal, channelLoadGroup); + + // We did inherit CSP in bug 1223647. If we do not already have a CSP, we + // should get it from the HTTP headers on the worker script. + if (!mWorkerPrivate->GetCSP() && CSPService::sCSPEnabled) { + NS_ConvertASCIItoUTF16 cspHeaderValue(tCspHeaderValue); + NS_ConvertASCIItoUTF16 cspROHeaderValue(tCspROHeaderValue); + + nsIPrincipal* principal = mWorkerPrivate->GetPrincipal(); + MOZ_ASSERT(principal, "Should not be null"); + + nsCOMPtr<nsIContentSecurityPolicy> csp; + rv = principal->EnsureCSP(nullptr, getter_AddRefs(csp)); + + if (csp) { + // If there's a CSP header, apply it. + if (!cspHeaderValue.IsEmpty()) { + rv = CSP_AppendCSPFromHeader(csp, cspHeaderValue, false); + NS_ENSURE_SUCCESS(rv, rv); + } + // If there's a report-only CSP header, apply it. + if (!cspROHeaderValue.IsEmpty()) { + rv = CSP_AppendCSPFromHeader(csp, cspROHeaderValue, true); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Set evalAllowed, default value is set in GetAllowsEval + bool evalAllowed = false; + bool reportEvalViolations = false; + rv = csp->GetAllowsEval(&reportEvalViolations, &evalAllowed); + NS_ENSURE_SUCCESS(rv, rv); + + mWorkerPrivate->SetCSP(csp); + mWorkerPrivate->SetEvalAllowed(evalAllowed); + mWorkerPrivate->SetReportCSPViolations(reportEvalViolations); + + // Set ReferrerPolicy, default value is set in GetReferrerPolicy + bool hasReferrerPolicy = false; + uint32_t rp = mozilla::net::RP_Default; + rv = csp->GetReferrerPolicy(&rp, &hasReferrerPolicy); + NS_ENSURE_SUCCESS(rv, rv); + + + if (hasReferrerPolicy) { //FIXME bug 1307366: move RP out of CSP code + mWorkerPrivate->SetReferrerPolicy(static_cast<net::ReferrerPolicy>(rp)); + } + } + } + if (parent) { + // XHR Params Allowed + mWorkerPrivate->SetXHRParamsAllowed(parent->XHRParamsAllowed()); + } + } + + NS_ConvertUTF8toUTF16 tRPHeaderValue(tRPHeaderCValue); + // If there's a Referrer-Policy header, apply it. + if (!tRPHeaderValue.IsEmpty()) { + net::ReferrerPolicy policy = + nsContentUtils::GetReferrerPolicyFromHeader(tRPHeaderValue); + if (policy != net::RP_Unset) { + mWorkerPrivate->SetReferrerPolicy(policy); + } + } + + return NS_OK; + } + + void + DataReceivedFromCache(uint32_t aIndex, const uint8_t* aString, + uint32_t aStringLen, + const mozilla::dom::ChannelInfo& aChannelInfo, + UniquePtr<PrincipalInfo> aPrincipalInfo) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aIndex < mLoadInfos.Length()); + ScriptLoadInfo& loadInfo = mLoadInfos[aIndex]; + MOZ_ASSERT(loadInfo.mCacheStatus == ScriptLoadInfo::Cached); + + nsCOMPtr<nsIPrincipal> responsePrincipal = + PrincipalInfoToPrincipal(*aPrincipalInfo); + + nsIPrincipal* principal = mWorkerPrivate->GetPrincipal(); + if (!principal) { + WorkerPrivate* parentWorker = mWorkerPrivate->GetParent(); + MOZ_ASSERT(parentWorker, "Must have a parent!"); + principal = parentWorker->GetPrincipal(); + } + + loadInfo.mMutedErrorFlag.emplace(!principal->Subsumes(responsePrincipal)); + + // May be null. + nsIDocument* parentDoc = mWorkerPrivate->GetDocument(); + + MOZ_ASSERT(!loadInfo.mScriptTextBuf); + + nsresult rv = + nsScriptLoader::ConvertToUTF16(nullptr, aString, aStringLen, + NS_LITERAL_STRING("UTF-8"), parentDoc, + loadInfo.mScriptTextBuf, + loadInfo.mScriptTextLength); + if (NS_SUCCEEDED(rv) && IsMainWorkerScript()) { + nsCOMPtr<nsIURI> finalURI; + rv = NS_NewURI(getter_AddRefs(finalURI), loadInfo.mFullURL, nullptr, nullptr); + if (NS_SUCCEEDED(rv)) { + mWorkerPrivate->SetBaseURI(finalURI); + } + + mozilla::DebugOnly<nsIPrincipal*> principal = mWorkerPrivate->GetPrincipal(); + MOZ_ASSERT(principal); + nsILoadGroup* loadGroup = mWorkerPrivate->GetLoadGroup(); + MOZ_ASSERT(loadGroup); + + mozilla::DebugOnly<bool> equal = false; + MOZ_ASSERT(responsePrincipal && NS_SUCCEEDED(responsePrincipal->Equals(principal, &equal))); + MOZ_ASSERT(equal); + + mWorkerPrivate->InitChannelInfo(aChannelInfo); + mWorkerPrivate->SetPrincipal(responsePrincipal, loadGroup); + } + + if (NS_SUCCEEDED(rv)) { + DataReceived(); + } + + LoadingFinished(aIndex, rv); + } + + void + DataReceived() + { + if (IsMainWorkerScript()) { + WorkerPrivate* parent = mWorkerPrivate->GetParent(); + + if (parent) { + // XHR Params Allowed + mWorkerPrivate->SetXHRParamsAllowed(parent->XHRParamsAllowed()); + + // Set Eval and ContentSecurityPolicy + mWorkerPrivate->SetCSP(parent->GetCSP()); + mWorkerPrivate->SetEvalAllowed(parent->IsEvalAllowed()); + } + } + } + + void + ExecuteFinishedScripts() + { + AssertIsOnMainThread(); + + if (IsMainWorkerScript()) { + mWorkerPrivate->WorkerScriptLoaded(); + } + + uint32_t firstIndex = UINT32_MAX; + uint32_t lastIndex = UINT32_MAX; + + // Find firstIndex based on whether mExecutionScheduled is unset. + for (uint32_t index = 0; index < mLoadInfos.Length(); index++) { + if (!mLoadInfos[index].mExecutionScheduled) { + firstIndex = index; + break; + } + } + + // Find lastIndex based on whether mChannel is set, and update + // mExecutionScheduled on the ones we're about to schedule. + if (firstIndex != UINT32_MAX) { + for (uint32_t index = firstIndex; index < mLoadInfos.Length(); index++) { + ScriptLoadInfo& loadInfo = mLoadInfos[index]; + + if (!loadInfo.Finished()) { + break; + } + + // We can execute this one. + loadInfo.mExecutionScheduled = true; + + lastIndex = index; + } + } + + // This is the last index, we can unused things before the exection of the + // script and the stopping of the sync loop. + if (lastIndex == mLoadInfos.Length() - 1) { + mCacheCreator = nullptr; + } + + if (firstIndex != UINT32_MAX && lastIndex != UINT32_MAX) { + RefPtr<ScriptExecutorRunnable> runnable = + new ScriptExecutorRunnable(*this, mSyncLoopTarget, IsMainWorkerScript(), + firstIndex, lastIndex); + if (!runnable->Dispatch()) { + MOZ_ASSERT(false, "This should never fail!"); + } + } + } +}; + +NS_IMPL_ISUPPORTS(ScriptLoaderRunnable, nsIRunnable) + +class MOZ_STACK_CLASS ScriptLoaderHolder final : public WorkerHolder +{ + // Raw pointer because this holder object follows the mRunnable life-time. + ScriptLoaderRunnable* mRunnable; + +public: + explicit ScriptLoaderHolder(ScriptLoaderRunnable* aRunnable) + : mRunnable(aRunnable) + { + MOZ_ASSERT(aRunnable); + } + + virtual bool + Notify(Status aStatus) override + { + mRunnable->Notify(aStatus); + return true; + } +}; + +NS_IMETHODIMP +LoaderListener::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, + nsresult aStatus, uint32_t aStringLen, + const uint8_t* aString) +{ + return mRunnable->OnStreamComplete(aLoader, mIndex, aStatus, aStringLen, aString); +} + +NS_IMETHODIMP +LoaderListener::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext) +{ + return mRunnable->OnStartRequest(aRequest, mIndex); +} + +void +CachePromiseHandler::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + AssertIsOnMainThread(); + // May already have been canceled by CacheScriptLoader::Fail from + // CancelMainThread. + MOZ_ASSERT(mLoadInfo.mCacheStatus == ScriptLoadInfo::WritingToCache || + mLoadInfo.mCacheStatus == ScriptLoadInfo::Cancel); + MOZ_ASSERT_IF(mLoadInfo.mCacheStatus == ScriptLoadInfo::Cancel, !mLoadInfo.mCachePromise); + + if (mLoadInfo.mCachePromise) { + mLoadInfo.mCacheStatus = ScriptLoadInfo::Cached; + mLoadInfo.mCachePromise = nullptr; + mRunnable->MaybeExecuteFinishedScripts(mIndex); + } +} + +void +CachePromiseHandler::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + AssertIsOnMainThread(); + // May already have been canceled by CacheScriptLoader::Fail from + // CancelMainThread. + MOZ_ASSERT(mLoadInfo.mCacheStatus == ScriptLoadInfo::WritingToCache || + mLoadInfo.mCacheStatus == ScriptLoadInfo::Cancel); + mLoadInfo.mCacheStatus = ScriptLoadInfo::Cancel; + + mLoadInfo.mCachePromise = nullptr; + + // This will delete the cache object and will call LoadingFinished() with an + // error for each ongoing operation. + mRunnable->DeleteCache(); +} + +nsresult +CacheCreator::CreateCacheStorage(nsIPrincipal* aPrincipal) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(!mCacheStorage); + MOZ_ASSERT(aPrincipal); + + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + + mozilla::AutoSafeJSContext cx; + JS::Rooted<JSObject*> sandbox(cx); + nsresult rv = xpc->CreateSandbox(cx, aPrincipal, sandbox.address()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mSandboxGlobalObject = xpc::NativeGlobal(sandbox); + if (NS_WARN_IF(!mSandboxGlobalObject)) { + return NS_ERROR_FAILURE; + } + + // If we're in private browsing mode, don't even try to create the + // CacheStorage. Instead, just fail immediately to terminate the + // ServiceWorker load. + if (NS_WARN_IF(mOriginAttributes.mPrivateBrowsingId > 0)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + // Create a CacheStorage bypassing its trusted origin checks. The + // ServiceWorker has already performed its own checks before getting + // to this point. + ErrorResult error; + mCacheStorage = + CacheStorage::CreateOnMainThread(mozilla::dom::cache::CHROME_ONLY_NAMESPACE, + mSandboxGlobalObject, + aPrincipal, + false, /* privateBrowsing can't be true here */ + true /* force trusted origin */, + error); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + + return NS_OK; +} + +nsresult +CacheCreator::Load(nsIPrincipal* aPrincipal) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(!mLoaders.IsEmpty()); + + nsresult rv = CreateCacheStorage(aPrincipal); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + ErrorResult error; + MOZ_ASSERT(!mCacheName.IsEmpty()); + RefPtr<Promise> promise = mCacheStorage->Open(mCacheName, error); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + + promise->AppendNativeHandler(this); + return NS_OK; +} + +void +CacheCreator::FailLoaders(nsresult aRv) +{ + AssertIsOnMainThread(); + + // Fail() can call LoadingFinished() which may call ExecuteFinishedScripts() + // which sets mCacheCreator to null, so hold a ref. + RefPtr<CacheCreator> kungfuDeathGrip = this; + + for (uint32_t i = 0, len = mLoaders.Length(); i < len; ++i) { + mLoaders[i]->Fail(aRv); + } + + mLoaders.Clear(); +} + +void +CacheCreator::RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) +{ + AssertIsOnMainThread(); + FailLoaders(NS_ERROR_FAILURE); +} + +void +CacheCreator::ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) +{ + AssertIsOnMainThread(); + + if (!aValue.isObject()) { + FailLoaders(NS_ERROR_FAILURE); + 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))) { + FailLoaders(NS_ERROR_FAILURE); + return; + } + + mCache = cache; + MOZ_DIAGNOSTIC_ASSERT(mCache); + + // If the worker is canceled, CancelMainThread() will have cleared the + // loaders via DeleteCache(). + for (uint32_t i = 0, len = mLoaders.Length(); i < len; ++i) { + MOZ_DIAGNOSTIC_ASSERT(mLoaders[i]); + mLoaders[i]->Load(cache); + } +} + +void +CacheCreator::DeleteCache() +{ + AssertIsOnMainThread(); + + // This is called when the load is canceled which can occur before + // mCacheStorage is initialized. + if (mCacheStorage) { + // It's safe to do this while Cache::Match() and Cache::Put() calls are + // running. + IgnoredErrorResult rv; + RefPtr<Promise> promise = mCacheStorage->Delete(mCacheName, rv); + + // We don't care to know the result of the promise object. + } + + // Always call this here to ensure the loaders array is cleared. + FailLoaders(NS_ERROR_FAILURE); +} + +void +CacheScriptLoader::Fail(nsresult aRv) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(NS_FAILED(aRv)); + + if (mFailed) { + return; + } + + mFailed = true; + + if (mPump) { + MOZ_ASSERT(mLoadInfo.mCacheStatus == ScriptLoadInfo::ReadingFromCache); + mPump->Cancel(aRv); + mPump = nullptr; + } + + mLoadInfo.mCacheStatus = ScriptLoadInfo::Cancel; + + // Stop if the load was aborted on the main thread. + // Can't use Finished() because mCachePromise may still be true. + if (mLoadInfo.mLoadingFinished) { + MOZ_ASSERT(!mLoadInfo.mChannel); + MOZ_ASSERT_IF(mLoadInfo.mCachePromise, + mLoadInfo.mCacheStatus == ScriptLoadInfo::WritingToCache || + mLoadInfo.mCacheStatus == ScriptLoadInfo::Cancel); + return; + } + + mRunnable->LoadingFinished(mIndex, aRv); +} + +void +CacheScriptLoader::Load(Cache* aCache) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aCache); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), mLoadInfo.mURL, nullptr, + mBaseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(rv); + return; + } + + nsAutoCString spec; + rv = uri->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(rv); + return; + } + + MOZ_ASSERT(mLoadInfo.mFullURL.IsEmpty()); + CopyUTF8toUTF16(spec, mLoadInfo.mFullURL); + + mozilla::dom::RequestOrUSVString request; + request.SetAsUSVString().Rebind(mLoadInfo.mFullURL.Data(), + mLoadInfo.mFullURL.Length()); + + mozilla::dom::CacheQueryOptions params; + + ErrorResult error; + RefPtr<Promise> promise = aCache->Match(request, params, error); + if (NS_WARN_IF(error.Failed())) { + Fail(error.StealNSResult()); + return; + } + + promise->AppendNativeHandler(this); +} + +void +CacheScriptLoader::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(mLoadInfo.mCacheStatus == ScriptLoadInfo::Uncached); + Fail(NS_ERROR_FAILURE); +} + +void +CacheScriptLoader::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue) +{ + AssertIsOnMainThread(); + // If we have already called 'Fail', we should not proceed. + if (mFailed) { + return; + } + + MOZ_ASSERT(mLoadInfo.mCacheStatus == ScriptLoadInfo::Uncached); + + nsresult rv; + + if (aValue.isUndefined()) { + mLoadInfo.mCacheStatus = ScriptLoadInfo::ToBeCached; + rv = mRunnable->LoadScript(mIndex); + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(rv); + } + return; + } + + MOZ_ASSERT(aValue.isObject()); + + JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); + mozilla::dom::Response* response = nullptr; + rv = UNWRAP_OBJECT(Response, &obj, response); + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(rv); + return; + } + + nsCOMPtr<nsIInputStream> inputStream; + response->GetBody(getter_AddRefs(inputStream)); + mChannelInfo = response->GetChannelInfo(); + const UniquePtr<PrincipalInfo>& pInfo = response->GetPrincipalInfo(); + if (pInfo) { + mPrincipalInfo = mozilla::MakeUnique<PrincipalInfo>(*pInfo); + } + + if (!inputStream) { + mLoadInfo.mCacheStatus = ScriptLoadInfo::Cached; + mRunnable->DataReceivedFromCache(mIndex, (uint8_t*)"", 0, mChannelInfo, + Move(mPrincipalInfo)); + return; + } + + MOZ_ASSERT(!mPump); + rv = NS_NewInputStreamPump(getter_AddRefs(mPump), inputStream); + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(rv); + return; + } + + nsCOMPtr<nsIStreamLoader> loader; + rv = NS_NewStreamLoader(getter_AddRefs(loader), this); + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(rv); + return; + } + + rv = mPump->AsyncRead(loader, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPump = nullptr; + Fail(rv); + return; + } + + + nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(mPump); + if (rr) { + nsCOMPtr<nsIEventTarget> sts = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + rv = rr->RetargetDeliveryTo(sts); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch the nsIInputStreamPump to a IO thread."); + } + } + + mLoadInfo.mCacheStatus = ScriptLoadInfo::ReadingFromCache; +} + +NS_IMETHODIMP +CacheScriptLoader::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, + nsresult aStatus, uint32_t aStringLen, + const uint8_t* aString) +{ + AssertIsOnMainThread(); + + mPump = nullptr; + + if (NS_FAILED(aStatus)) { + MOZ_ASSERT(mLoadInfo.mCacheStatus == ScriptLoadInfo::ReadingFromCache || + mLoadInfo.mCacheStatus == ScriptLoadInfo::Cancel); + Fail(aStatus); + return NS_OK; + } + + MOZ_ASSERT(mLoadInfo.mCacheStatus == ScriptLoadInfo::ReadingFromCache); + mLoadInfo.mCacheStatus = ScriptLoadInfo::Cached; + + MOZ_ASSERT(mPrincipalInfo); + mRunnable->DataReceivedFromCache(mIndex, aString, aStringLen, mChannelInfo, + Move(mPrincipalInfo)); + return NS_OK; +} + +class ChannelGetterRunnable final : public WorkerMainThreadRunnable +{ + const nsAString& mScriptURL; + nsIChannel** mChannel; + nsresult mResult; + +public: + ChannelGetterRunnable(WorkerPrivate* aParentWorker, + const nsAString& aScriptURL, + nsIChannel** aChannel) + : WorkerMainThreadRunnable(aParentWorker, + NS_LITERAL_CSTRING("ScriptLoader :: ChannelGetter")) + , mScriptURL(aScriptURL) + , mChannel(aChannel) + , mResult(NS_ERROR_FAILURE) + { + MOZ_ASSERT(aParentWorker); + aParentWorker->AssertIsOnWorkerThread(); + } + + virtual bool + MainThreadRun() override + { + AssertIsOnMainThread(); + + nsIPrincipal* principal = mWorkerPrivate->GetPrincipal(); + MOZ_ASSERT(principal); + + // Figure out our base URI. + nsCOMPtr<nsIURI> baseURI = mWorkerPrivate->GetBaseURI(); + MOZ_ASSERT(baseURI); + + // May be null. + nsCOMPtr<nsIDocument> parentDoc = mWorkerPrivate->GetDocument(); + + nsCOMPtr<nsILoadGroup> loadGroup = mWorkerPrivate->GetLoadGroup(); + + nsCOMPtr<nsIChannel> channel; + mResult = + scriptloader::ChannelFromScriptURLMainThread(principal, baseURI, + parentDoc, loadGroup, + mScriptURL, + // Nested workers are always dedicated. + nsIContentPolicy::TYPE_INTERNAL_WORKER, + // Nested workers use default uri encoding. + true, + getter_AddRefs(channel)); + if (NS_SUCCEEDED(mResult)) { + channel.forget(mChannel); + } + + return true; + } + + nsresult + GetResult() const + { + return mResult; + } + +private: + virtual ~ChannelGetterRunnable() + { } +}; + +ScriptExecutorRunnable::ScriptExecutorRunnable( + ScriptLoaderRunnable& aScriptLoader, + nsIEventTarget* aSyncLoopTarget, + bool aIsWorkerScript, + uint32_t aFirstIndex, + uint32_t aLastIndex) +: MainThreadWorkerSyncRunnable(aScriptLoader.mWorkerPrivate, aSyncLoopTarget), + mScriptLoader(aScriptLoader), mIsWorkerScript(aIsWorkerScript), + mFirstIndex(aFirstIndex), mLastIndex(aLastIndex) +{ + MOZ_ASSERT(aFirstIndex <= aLastIndex); + MOZ_ASSERT(aLastIndex < aScriptLoader.mLoadInfos.Length()); +} + +bool +ScriptExecutorRunnable::IsDebuggerRunnable() const +{ + // ScriptExecutorRunnable is used to execute both worker and debugger scripts. + // In the latter case, the runnable needs to be dispatched to the debugger + // queue. + return mScriptLoader.mWorkerScriptType == DebuggerScript; +} + +bool +ScriptExecutorRunnable::PreRun(WorkerPrivate* aWorkerPrivate) +{ + aWorkerPrivate->AssertIsOnWorkerThread(); + + if (!mIsWorkerScript) { + return true; + } + + if (!aWorkerPrivate->GetJSContext()) { + return false; + } + + MOZ_ASSERT(mFirstIndex == 0); + MOZ_ASSERT(!mScriptLoader.mRv.Failed()); + + AutoJSAPI jsapi; + jsapi.Init(); + + WorkerGlobalScope* globalScope = + aWorkerPrivate->GetOrCreateGlobalScope(jsapi.cx()); + if (NS_WARN_IF(!globalScope)) { + NS_WARNING("Failed to make global!"); + // There's no way to report the exception on jsapi right now, because there + // is no way to even enter a compartment on this thread anymore. Just clear + // the exception. We'll report some sort of error to our caller in + // ShutdownScriptLoader, but it will get squelched for the same reason we're + // squelching here: all the error reporting machinery relies on being able + // to enter a compartment to report the error. + jsapi.ClearException(); + return false; + } + + return true; +} + +bool +ScriptExecutorRunnable::WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) +{ + aWorkerPrivate->AssertIsOnWorkerThread(); + + nsTArray<ScriptLoadInfo>& loadInfos = mScriptLoader.mLoadInfos; + + // Don't run if something else has already failed. + for (uint32_t index = 0; index < mFirstIndex; index++) { + ScriptLoadInfo& loadInfo = loadInfos.ElementAt(index); + + NS_ASSERTION(!loadInfo.mChannel, "Should no longer have a channel!"); + NS_ASSERTION(loadInfo.mExecutionScheduled, "Should be scheduled!"); + + if (!loadInfo.mExecutionResult) { + return true; + } + } + + // If nothing else has failed, our ErrorResult better not be a failure either. + MOZ_ASSERT(!mScriptLoader.mRv.Failed(), "Who failed it and why?"); + + // Slightly icky action at a distance, but there's no better place to stash + // this value, really. + JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx)); + MOZ_ASSERT(global); + + for (uint32_t index = mFirstIndex; index <= mLastIndex; index++) { + ScriptLoadInfo& loadInfo = loadInfos.ElementAt(index); + + NS_ASSERTION(!loadInfo.mChannel, "Should no longer have a channel!"); + NS_ASSERTION(loadInfo.mExecutionScheduled, "Should be scheduled!"); + NS_ASSERTION(!loadInfo.mExecutionResult, "Should not have executed yet!"); + + MOZ_ASSERT(!mScriptLoader.mRv.Failed(), "Who failed it and why?"); + mScriptLoader.mRv.MightThrowJSException(); + if (NS_FAILED(loadInfo.mLoadResult)) { + scriptloader::ReportLoadError(mScriptLoader.mRv, + loadInfo.mLoadResult, loadInfo.mURL); + // Top level scripts only! + if (mIsWorkerScript) { + aWorkerPrivate->MaybeDispatchLoadFailedRunnable(); + } + return true; + } + + NS_ConvertUTF16toUTF8 filename(loadInfo.mURL); + + JS::CompileOptions options(aCx); + options.setFileAndLine(filename.get(), 1) + .setNoScriptRval(true); + + if (mScriptLoader.mWorkerScriptType == DebuggerScript) { + options.setVersion(JSVERSION_LATEST); + } + + MOZ_ASSERT(loadInfo.mMutedErrorFlag.isSome()); + options.setMutedErrors(loadInfo.mMutedErrorFlag.valueOr(true)); + + JS::SourceBufferHolder srcBuf(loadInfo.mScriptTextBuf, + loadInfo.mScriptTextLength, + JS::SourceBufferHolder::GiveOwnership); + loadInfo.mScriptTextBuf = nullptr; + loadInfo.mScriptTextLength = 0; + + // Our ErrorResult still shouldn't be a failure. + MOZ_ASSERT(!mScriptLoader.mRv.Failed(), "Who failed it and why?"); + JS::Rooted<JS::Value> unused(aCx); + if (!JS::Evaluate(aCx, options, srcBuf, &unused)) { + mScriptLoader.mRv.StealExceptionFromJSContext(aCx); + return true; + } + + loadInfo.mExecutionResult = true; + } + + return true; +} + +void +ScriptExecutorRunnable::PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + bool aRunResult) +{ + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(!JS_IsExceptionPending(aCx), "Who left an exception on there?"); + + nsTArray<ScriptLoadInfo>& loadInfos = mScriptLoader.mLoadInfos; + + if (mLastIndex == loadInfos.Length() - 1) { + // All done. If anything failed then return false. + bool result = true; + bool mutedError = false; + for (uint32_t index = 0; index < loadInfos.Length(); index++) { + if (!loadInfos[index].mExecutionResult) { + mutedError = loadInfos[index].mMutedErrorFlag.valueOr(true); + result = false; + break; + } + } + + // The only way we can get here with "result" false but without + // mScriptLoader.mRv being a failure is if we're loading the main worker + // script and GetOrCreateGlobalScope() fails. In that case we would have + // returned false from WorkerRun, so assert that. + MOZ_ASSERT_IF(!result && !mScriptLoader.mRv.Failed(), + !aRunResult); + ShutdownScriptLoader(aCx, aWorkerPrivate, result, mutedError); + } +} + +nsresult +ScriptExecutorRunnable::Cancel() +{ + if (mLastIndex == mScriptLoader.mLoadInfos.Length() - 1) { + ShutdownScriptLoader(mWorkerPrivate->GetJSContext(), mWorkerPrivate, + false, false); + } + return MainThreadWorkerSyncRunnable::Cancel(); +} + +void +ScriptExecutorRunnable::ShutdownScriptLoader(JSContext* aCx, + WorkerPrivate* aWorkerPrivate, + bool aResult, + bool aMutedError) +{ + aWorkerPrivate->AssertIsOnWorkerThread(); + + MOZ_ASSERT(mLastIndex == mScriptLoader.mLoadInfos.Length() - 1); + + if (mIsWorkerScript && aWorkerPrivate->IsServiceWorker()) { + aWorkerPrivate->SetLoadingWorkerScript(false); + } + + if (!aResult) { + // At this point there are two possibilities: + // + // 1) mScriptLoader.mRv.Failed(). In that case we just want to leave it + // as-is, except if it has a JS exception and we need to mute JS + // exceptions. In that case, we log the exception without firing any + // events and then replace it on the ErrorResult with a NetworkError, + // per spec. + // + // 2) mScriptLoader.mRv succeeded. As far as I can tell, this can only + // happen when loading the main worker script and + // GetOrCreateGlobalScope() fails or if ScriptExecutorRunnable::Cancel + // got called. Does it matter what we throw in this case? I'm not + // sure... + if (mScriptLoader.mRv.Failed()) { + if (aMutedError && mScriptLoader.mRv.IsJSException()) { + LogExceptionToConsole(aCx, aWorkerPrivate); + mScriptLoader.mRv.Throw(NS_ERROR_DOM_NETWORK_ERR); + } + } else { + mScriptLoader.mRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + } + } + + aWorkerPrivate->StopSyncLoop(mSyncLoopTarget, aResult); +} + +void +ScriptExecutorRunnable::LogExceptionToConsole(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) +{ + aWorkerPrivate->AssertIsOnWorkerThread(); + + MOZ_ASSERT(mScriptLoader.mRv.IsJSException()); + + JS::Rooted<JS::Value> exn(aCx); + if (!ToJSValue(aCx, mScriptLoader.mRv, &exn)) { + return; + } + + // Now the exception state should all be in exn. + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + MOZ_ASSERT(!mScriptLoader.mRv.Failed()); + + js::ErrorReport report(aCx); + if (!report.init(aCx, exn, js::ErrorReport::WithSideEffects)) { + JS_ClearPendingException(aCx); + return; + } + + RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport(); + xpcReport->Init(report.report(), report.toStringResult().c_str(), + aWorkerPrivate->IsChromeWorker(), aWorkerPrivate->WindowID()); + + RefPtr<AsyncErrorReporter> r = new AsyncErrorReporter(xpcReport); + NS_DispatchToMainThread(r); +} + +void +LoadAllScripts(WorkerPrivate* aWorkerPrivate, + nsTArray<ScriptLoadInfo>& aLoadInfos, bool aIsMainScript, + WorkerScriptType aWorkerScriptType, ErrorResult& aRv) +{ + aWorkerPrivate->AssertIsOnWorkerThread(); + NS_ASSERTION(!aLoadInfos.IsEmpty(), "Bad arguments!"); + + AutoSyncLoopHolder syncLoop(aWorkerPrivate); + + RefPtr<ScriptLoaderRunnable> loader = + new ScriptLoaderRunnable(aWorkerPrivate, syncLoop.EventTarget(), + aLoadInfos, aIsMainScript, aWorkerScriptType, + aRv); + + NS_ASSERTION(aLoadInfos.IsEmpty(), "Should have swapped!"); + + ScriptLoaderHolder workerHolder(loader); + + if (NS_WARN_IF(!workerHolder.HoldWorker(aWorkerPrivate, Terminating))) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + if (NS_FAILED(NS_DispatchToMainThread(loader))) { + NS_ERROR("Failed to dispatch!"); + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + syncLoop.Run(); +} + +} /* anonymous namespace */ + +BEGIN_WORKERS_NAMESPACE + +namespace scriptloader { + +nsresult +ChannelFromScriptURLMainThread(nsIPrincipal* aPrincipal, + nsIURI* aBaseURI, + nsIDocument* aParentDoc, + nsILoadGroup* aLoadGroup, + const nsAString& aScriptURL, + nsContentPolicyType aContentPolicyType, + bool aDefaultURIEncoding, + nsIChannel** aChannel) +{ + AssertIsOnMainThread(); + + nsCOMPtr<nsIIOService> ios(do_GetIOService()); + + nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager(); + NS_ASSERTION(secMan, "This should never be null!"); + + return ChannelFromScriptURL(aPrincipal, aBaseURI, aParentDoc, aLoadGroup, + ios, secMan, aScriptURL, true, WorkerScript, + aContentPolicyType, nsIRequest::LOAD_NORMAL, + aDefaultURIEncoding, aChannel); +} + +nsresult +ChannelFromScriptURLWorkerThread(JSContext* aCx, + WorkerPrivate* aParent, + const nsAString& aScriptURL, + nsIChannel** aChannel) +{ + aParent->AssertIsOnWorkerThread(); + + RefPtr<ChannelGetterRunnable> getter = + new ChannelGetterRunnable(aParent, aScriptURL, aChannel); + + ErrorResult rv; + getter->Dispatch(rv); + if (rv.Failed()) { + NS_ERROR("Failed to dispatch!"); + return rv.StealNSResult(); + } + + return getter->GetResult(); +} + +void ReportLoadError(ErrorResult& aRv, nsresult aLoadResult, + const nsAString& aScriptURL) +{ + MOZ_ASSERT(!aRv.Failed()); + + switch (aLoadResult) { + case NS_ERROR_FILE_NOT_FOUND: + case NS_ERROR_NOT_AVAILABLE: + aLoadResult = NS_ERROR_DOM_NETWORK_ERR; + break; + + case NS_ERROR_MALFORMED_URI: + aLoadResult = NS_ERROR_DOM_SYNTAX_ERR; + break; + + case NS_BINDING_ABORTED: + // Note: we used to pretend like we didn't set an exception for + // NS_BINDING_ABORTED, but then ShutdownScriptLoader did it anyway. The + // other callsite, in WorkerPrivate::Constructor, never passed in + // NS_BINDING_ABORTED. So just throw it directly here. Consumers will + // deal as needed. But note that we do NOT want to ThrowDOMException() + // for this case, because that will make it impossible for consumers to + // realize that our error was NS_BINDING_ABORTED. + aRv.Throw(aLoadResult); + return; + + case NS_ERROR_DOM_SECURITY_ERR: + case NS_ERROR_DOM_SYNTAX_ERR: + break; + + case NS_ERROR_DOM_BAD_URI: + // This is actually a security error. + aLoadResult = NS_ERROR_DOM_SECURITY_ERR; + break; + + default: + // For lack of anything better, go ahead and throw a NetworkError here. + // We don't want to throw a JS exception, because for toplevel script + // loads that would get squelched. + aRv.ThrowDOMException(NS_ERROR_DOM_NETWORK_ERR, + nsPrintfCString("Failed to load worker script at %s (nsresult = 0x%x)", + NS_ConvertUTF16toUTF8(aScriptURL).get(), + aLoadResult)); + return; + } + + aRv.ThrowDOMException(aLoadResult, + NS_LITERAL_CSTRING("Failed to load worker script at \"") + + NS_ConvertUTF16toUTF8(aScriptURL) + + NS_LITERAL_CSTRING("\"")); +} + +void +LoadMainScript(WorkerPrivate* aWorkerPrivate, + const nsAString& aScriptURL, + WorkerScriptType aWorkerScriptType, + ErrorResult& aRv) +{ + nsTArray<ScriptLoadInfo> loadInfos; + + ScriptLoadInfo* info = loadInfos.AppendElement(); + info->mURL = aScriptURL; + + LoadAllScripts(aWorkerPrivate, loadInfos, true, aWorkerScriptType, aRv); +} + +void +Load(WorkerPrivate* aWorkerPrivate, + const nsTArray<nsString>& aScriptURLs, WorkerScriptType aWorkerScriptType, + ErrorResult& aRv) +{ + const uint32_t urlCount = aScriptURLs.Length(); + + if (!urlCount) { + return; + } + + if (urlCount > MAX_CONCURRENT_SCRIPTS) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + nsTArray<ScriptLoadInfo> loadInfos; + loadInfos.SetLength(urlCount); + + for (uint32_t index = 0; index < urlCount; index++) { + loadInfos[index].mURL = aScriptURLs[index]; + } + + LoadAllScripts(aWorkerPrivate, loadInfos, false, aWorkerScriptType, aRv); +} + +} // namespace scriptloader + +END_WORKERS_NAMESPACE diff --git a/dom/workers/ScriptLoader.h b/dom/workers/ScriptLoader.h new file mode 100644 index 000000000..c92c369ad --- /dev/null +++ b/dom/workers/ScriptLoader.h @@ -0,0 +1,68 @@ +/* -*- 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_workers_scriptloader_h__ +#define mozilla_dom_workers_scriptloader_h__ + +#include "Workers.h" +#include "nsIContentPolicyBase.h" + +class nsIPrincipal; +class nsIURI; +class nsIDocument; +class nsILoadGroup; +class nsString; +class nsIChannel; + +namespace mozilla { + +class ErrorResult; + +} // namespace mozilla + +BEGIN_WORKERS_NAMESPACE + +enum WorkerScriptType { + WorkerScript, + DebuggerScript +}; + +namespace scriptloader { + +nsresult +ChannelFromScriptURLMainThread(nsIPrincipal* aPrincipal, + nsIURI* aBaseURI, + nsIDocument* aParentDoc, + nsILoadGroup* aLoadGroup, + const nsAString& aScriptURL, + nsContentPolicyType aContentPolicyType, + bool aDefaultURIEncoding, + nsIChannel** aChannel); + +nsresult +ChannelFromScriptURLWorkerThread(JSContext* aCx, + WorkerPrivate* aParent, + const nsAString& aScriptURL, + nsIChannel** aChannel); + +void ReportLoadError(ErrorResult& aRv, nsresult aLoadResult, + const nsAString& aScriptURL); + +void LoadMainScript(WorkerPrivate* aWorkerPrivate, + const nsAString& aScriptURL, + WorkerScriptType aWorkerScriptType, + ErrorResult& aRv); + +void Load(WorkerPrivate* aWorkerPrivate, + const nsTArray<nsString>& aScriptURLs, + WorkerScriptType aWorkerScriptType, + mozilla::ErrorResult& aRv); + +} // namespace scriptloader + +END_WORKERS_NAMESPACE + +#endif /* mozilla_dom_workers_scriptloader_h__ */ diff --git a/dom/workers/ServiceWorker.cpp b/dom/workers/ServiceWorker.cpp new file mode 100644 index 000000000..6a6995d59 --- /dev/null +++ b/dom/workers/ServiceWorker.cpp @@ -0,0 +1,103 @@ +/* -*- 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 "ServiceWorker.h" + +#include "nsIDocument.h" +#include "nsPIDOMWindow.h" +#include "ServiceWorkerClient.h" +#include "ServiceWorkerManager.h" +#include "ServiceWorkerPrivate.h" +#include "WorkerPrivate.h" + +#include "mozilla/Preferences.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/ServiceWorkerGlobalScopeBinding.h" + +#ifdef XP_WIN +#undef PostMessage +#endif + +using mozilla::ErrorResult; +using namespace mozilla::dom; + +namespace mozilla { +namespace dom { +namespace workers { + +bool +ServiceWorkerVisible(JSContext* aCx, JSObject* aObj) +{ + if (NS_IsMainThread()) { + return Preferences::GetBool("dom.serviceWorkers.enabled", false); + } + + return IS_INSTANCE_OF(ServiceWorkerGlobalScope, aObj); +} + +ServiceWorker::ServiceWorker(nsPIDOMWindowInner* aWindow, + ServiceWorkerInfo* aInfo) + : DOMEventTargetHelper(aWindow), + mInfo(aInfo) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aInfo); + + // This will update our state too. + mInfo->AppendWorker(this); +} + +ServiceWorker::~ServiceWorker() +{ + AssertIsOnMainThread(); + mInfo->RemoveWorker(this); +} + +NS_IMPL_ADDREF_INHERITED(ServiceWorker, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(ServiceWorker, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(ServiceWorker) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +JSObject* +ServiceWorker::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + AssertIsOnMainThread(); + + return ServiceWorkerBinding::Wrap(aCx, this, aGivenProto); +} + +void +ServiceWorker::GetScriptURL(nsString& aURL) const +{ + CopyUTF8toUTF16(mInfo->ScriptSpec(), aURL); +} + +void +ServiceWorker::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + ErrorResult& aRv) +{ + if (State() == ServiceWorkerState::Redundant) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(GetParentObject()); + if (!window || !window->GetExtantDoc()) { + NS_WARNING("Trying to call post message from an invalid dom object."); + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + UniquePtr<ServiceWorkerClientInfo> clientInfo(new ServiceWorkerClientInfo(window->GetExtantDoc())); + ServiceWorkerPrivate* workerPrivate = mInfo->WorkerPrivate(); + aRv = workerPrivate->SendMessageEvent(aCx, aMessage, aTransferable, Move(clientInfo)); +} + +} // namespace workers +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/ServiceWorker.h b/dom/workers/ServiceWorker.h new file mode 100644 index 000000000..e49f334e6 --- /dev/null +++ b/dom/workers/ServiceWorker.h @@ -0,0 +1,85 @@ +/* -*- 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_workers_serviceworker_h__ +#define mozilla_dom_workers_serviceworker_h__ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/ServiceWorkerBinding.h" // For ServiceWorkerState. + +class nsPIDOMWindowInner; + +namespace mozilla { +namespace dom { + +namespace workers { + +class ServiceWorkerInfo; +class ServiceWorkerManager; +class SharedWorker; + +bool +ServiceWorkerVisible(JSContext* aCx, JSObject* aObj); + +class ServiceWorker final : public DOMEventTargetHelper +{ + friend class ServiceWorkerInfo; +public: + NS_DECL_ISUPPORTS_INHERITED + + IMPL_EVENT_HANDLER(statechange) + IMPL_EVENT_HANDLER(error) + + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + ServiceWorkerState + State() const + { + return mState; + } + + void + SetState(ServiceWorkerState aState) + { + mState = aState; + } + + void + GetScriptURL(nsString& aURL) const; + + void + DispatchStateChange(ServiceWorkerState aState) + { + DOMEventTargetHelper::DispatchTrustedEvent(NS_LITERAL_STRING("statechange")); + } + +#ifdef XP_WIN +#undef PostMessage +#endif + + void + PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + ErrorResult& aRv); + +private: + // This class can only be created from ServiceWorkerInfo::GetOrCreateInstance(). + ServiceWorker(nsPIDOMWindowInner* aWindow, ServiceWorkerInfo* aInfo); + + // This class is reference-counted and will be destroyed from Release(). + ~ServiceWorker(); + + ServiceWorkerState mState; + const RefPtr<ServiceWorkerInfo> mInfo; +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_serviceworker_h__ diff --git a/dom/workers/ServiceWorkerClient.cpp b/dom/workers/ServiceWorkerClient.cpp new file mode 100644 index 000000000..660512a5f --- /dev/null +++ b/dom/workers/ServiceWorkerClient.cpp @@ -0,0 +1,232 @@ +/* -*- 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 "ServiceWorkerClient.h" +#include "ServiceWorkerContainer.h" + +#include "mozilla/dom/MessageEvent.h" +#include "mozilla/dom/Navigator.h" +#include "mozilla/dom/ServiceWorkerMessageEvent.h" +#include "mozilla/dom/ServiceWorkerMessageEventBinding.h" +#include "nsGlobalWindow.h" +#include "nsIBrowserDOMWindow.h" +#include "nsIDocument.h" +#include "ServiceWorker.h" +#include "ServiceWorkerPrivate.h" +#include "WorkerPrivate.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::dom::workers; + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ServiceWorkerClient, mOwner) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ServiceWorkerClient) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ServiceWorkerClient) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ServiceWorkerClient) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +ServiceWorkerClientInfo::ServiceWorkerClientInfo(nsIDocument* aDoc) + : mWindowId(0) + , mFrameType(FrameType::None) +{ + MOZ_ASSERT(aDoc); + nsresult rv = aDoc->GetOrCreateId(mClientId); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to get the UUID of the document."); + } + + RefPtr<nsGlobalWindow> innerWindow = nsGlobalWindow::Cast(aDoc->GetInnerWindow()); + if (innerWindow) { + // XXXcatalinb: The inner window can be null if the document is navigating + // and was detached. + mWindowId = innerWindow->WindowID(); + } + + nsCOMPtr<nsIURI> originalURI = aDoc->GetOriginalURI(); + if (originalURI) { + nsAutoCString spec; + originalURI->GetSpec(spec); + CopyUTF8toUTF16(spec, mUrl); + } + mVisibilityState = aDoc->VisibilityState(); + + ErrorResult result; + mFocused = aDoc->HasFocus(result); + if (result.Failed()) { + NS_WARNING("Failed to get focus information."); + } + + RefPtr<nsGlobalWindow> outerWindow = nsGlobalWindow::Cast(aDoc->GetWindow()); + if (!outerWindow) { + MOZ_ASSERT(mFrameType == FrameType::None); + } else if (!outerWindow->IsTopLevelWindow()) { + mFrameType = FrameType::Nested; + } else if (outerWindow->HadOriginalOpener()) { + mFrameType = FrameType::Auxiliary; + } else { + mFrameType = FrameType::Top_level; + } +} + +JSObject* +ServiceWorkerClient::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return ClientBinding::Wrap(aCx, this, aGivenProto); +} + +namespace { + +class ServiceWorkerClientPostMessageRunnable final + : public Runnable + , public StructuredCloneHolder +{ + uint64_t mWindowId; + +public: + explicit ServiceWorkerClientPostMessageRunnable(uint64_t aWindowId) + : StructuredCloneHolder(CloningSupported, TransferringSupported, + StructuredCloneScope::SameProcessDifferentThread) + , mWindowId(aWindowId) + {} + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + nsGlobalWindow* window = nsGlobalWindow::GetInnerWindowWithId(mWindowId); + if (!window) { + return NS_ERROR_FAILURE; + } + + ErrorResult result; + dom::Navigator* navigator = window->GetNavigator(result); + if (NS_WARN_IF(result.Failed())) { + return result.StealNSResult(); + } + + RefPtr<ServiceWorkerContainer> container = navigator->ServiceWorker(); + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(window))) { + return NS_ERROR_FAILURE; + } + JSContext* cx = jsapi.cx(); + + return DispatchDOMEvent(cx, container); + } + +private: + NS_IMETHOD + DispatchDOMEvent(JSContext* aCx, ServiceWorkerContainer* aTargetContainer) + { + AssertIsOnMainThread(); + + MOZ_ASSERT(aTargetContainer->GetParentObject(), + "How come we don't have a window here?!"); + + JS::Rooted<JS::Value> messageData(aCx); + ErrorResult rv; + Read(aTargetContainer->GetParentObject(), aCx, &messageData, rv); + if (NS_WARN_IF(rv.Failed())) { + xpc::Throw(aCx, rv.StealNSResult()); + return NS_ERROR_FAILURE; + } + + RootedDictionary<ServiceWorkerMessageEventInit> init(aCx); + + nsCOMPtr<nsIPrincipal> principal = aTargetContainer->GetParentObject()->PrincipalOrNull(); + NS_WARNING_ASSERTION(principal, "Why is the principal null here?"); + + bool isNullPrincipal = false; + bool isSystemPrincipal = false; + if (principal) { + isNullPrincipal = principal->GetIsNullPrincipal(); + MOZ_ASSERT(!isNullPrincipal); + isSystemPrincipal = principal->GetIsSystemPrincipal(); + MOZ_ASSERT(!isSystemPrincipal); + } + + init.mData = messageData; + nsAutoCString origin; + if (principal && !isNullPrincipal && !isSystemPrincipal) { + principal->GetOrigin(origin); + } + init.mOrigin = NS_ConvertUTF8toUTF16(origin); + + RefPtr<ServiceWorker> serviceWorker = aTargetContainer->GetController(); + if (serviceWorker) { + init.mSource.SetValue().SetAsServiceWorker() = serviceWorker; + } + + if (!TakeTransferredPortsAsSequence(init.mPorts)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + RefPtr<ServiceWorkerMessageEvent> event = + ServiceWorkerMessageEvent::Constructor(aTargetContainer, + NS_LITERAL_STRING("message"), + init); + + event->SetTrusted(true); + bool status = false; + aTargetContainer->DispatchEvent(static_cast<dom::Event*>(event.get()), + &status); + + if (!status) { + return NS_ERROR_FAILURE; + } + + return NS_OK; + } +}; + +} // namespace + +void +ServiceWorkerClient::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + ErrorResult& aRv) +{ + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + workerPrivate->AssertIsOnWorkerThread(); + + JS::Rooted<JS::Value> transferable(aCx, JS::UndefinedValue()); + if (aTransferable.WasPassed()) { + const Sequence<JS::Value>& realTransferable = aTransferable.Value(); + + JS::HandleValueArray elements = + JS::HandleValueArray::fromMarkedLocation(realTransferable.Length(), + realTransferable.Elements()); + + JSObject* array = JS_NewArrayObject(aCx, elements); + if (!array) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + transferable.setObject(*array); + } + + RefPtr<ServiceWorkerClientPostMessageRunnable> runnable = + new ServiceWorkerClientPostMessageRunnable(mWindowId); + + runnable->Write(aCx, aMessage, transferable, JS::CloneDataPolicy().denySharedArrayBuffer(), + aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + aRv = workerPrivate->DispatchToMainThread(runnable.forget()); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} + diff --git a/dom/workers/ServiceWorkerClient.h b/dom/workers/ServiceWorkerClient.h new file mode 100644 index 000000000..36a9cc168 --- /dev/null +++ b/dom/workers/ServiceWorkerClient.h @@ -0,0 +1,118 @@ +/* -*- 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_workers_serviceworkerclient_h +#define mozilla_dom_workers_serviceworkerclient_h + +#include "nsCOMPtr.h" +#include "nsWrapperCache.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/ClientBinding.h" + +class nsIDocument; + +namespace mozilla { +namespace dom { +namespace workers { + +class ServiceWorkerClient; +class ServiceWorkerWindowClient; + +// Used as a container object for information needed to create +// client objects. +class ServiceWorkerClientInfo final +{ + friend class ServiceWorkerClient; + friend class ServiceWorkerWindowClient; + +public: + explicit ServiceWorkerClientInfo(nsIDocument* aDoc); + + const nsString& ClientId() const + { + return mClientId; + } + +private: + nsString mClientId; + uint64_t mWindowId; + nsString mUrl; + + // Window Clients + VisibilityState mVisibilityState; + bool mFocused; + FrameType mFrameType; +}; + +class ServiceWorkerClient : public nsISupports, + public nsWrapperCache +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ServiceWorkerClient) + + ServiceWorkerClient(nsISupports* aOwner, + const ServiceWorkerClientInfo& aClientInfo) + : mOwner(aOwner) + , mId(aClientInfo.mClientId) + , mUrl(aClientInfo.mUrl) + , mWindowId(aClientInfo.mWindowId) + , mFrameType(aClientInfo.mFrameType) + { + MOZ_ASSERT(aOwner); + } + + nsISupports* + GetParentObject() const + { + return mOwner; + } + + void GetId(nsString& aRetval) const + { + aRetval = mId; + } + + void + GetUrl(nsAString& aUrl) const + { + aUrl.Assign(mUrl); + } + + mozilla::dom::FrameType + FrameType() const + { + return mFrameType; + } + + void + PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + ErrorResult& aRv); + + JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + +protected: + virtual ~ServiceWorkerClient() + { } + +private: + nsCOMPtr<nsISupports> mOwner; + nsString mId; + nsString mUrl; + +protected: + uint64_t mWindowId; + mozilla::dom::FrameType mFrameType; +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_serviceworkerclient_h diff --git a/dom/workers/ServiceWorkerClients.cpp b/dom/workers/ServiceWorkerClients.cpp new file mode 100644 index 000000000..11f864443 --- /dev/null +++ b/dom/workers/ServiceWorkerClients.cpp @@ -0,0 +1,864 @@ +/* -*- 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 "ServiceWorkerClients.h" + +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseWorkerProxy.h" + +#include "ServiceWorkerClient.h" +#include "ServiceWorkerManager.h" +#include "ServiceWorkerPrivate.h" +#include "ServiceWorkerWindowClient.h" + +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" +#include "WorkerScope.h" + +#include "nsContentUtils.h" +#include "nsIBrowserDOMWindow.h" +#include "nsIDocShell.h" +#include "nsIDOMChromeWindow.h" +#include "nsIDOMWindow.h" +#include "nsIWebNavigation.h" +#include "nsIWebProgress.h" +#include "nsIWebProgressListener.h" +#include "nsIWindowMediator.h" +#include "nsIWindowWatcher.h" +#include "nsNetUtil.h" +#include "nsPIWindowWatcher.h" +#include "nsWindowWatcher.h" +#include "nsWeakReference.h" + +#ifdef MOZ_WIDGET_ANDROID +#include "AndroidBridge.h" +#endif + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::dom::workers; + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ServiceWorkerClients) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ServiceWorkerClients) +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ServiceWorkerClients, mWorkerScope) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ServiceWorkerClients) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +ServiceWorkerClients::ServiceWorkerClients(ServiceWorkerGlobalScope* aWorkerScope) + : mWorkerScope(aWorkerScope) +{ + MOZ_ASSERT(mWorkerScope); +} + +JSObject* +ServiceWorkerClients::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return ClientsBinding::Wrap(aCx, this, aGivenProto); +} + +namespace { + +class GetRunnable final : public Runnable +{ + class ResolvePromiseWorkerRunnable final : public WorkerRunnable + { + RefPtr<PromiseWorkerProxy> mPromiseProxy; + UniquePtr<ServiceWorkerClientInfo> mValue; + nsresult mRv; + + public: + ResolvePromiseWorkerRunnable(WorkerPrivate* aWorkerPrivate, + PromiseWorkerProxy* aPromiseProxy, + UniquePtr<ServiceWorkerClientInfo>&& aValue, + nsresult aRv) + : WorkerRunnable(aWorkerPrivate), + mPromiseProxy(aPromiseProxy), + mValue(Move(aValue)), + mRv(Move(aRv)) + { + AssertIsOnMainThread(); + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + Promise* promise = mPromiseProxy->WorkerPromise(); + MOZ_ASSERT(promise); + + if (NS_FAILED(mRv)) { + promise->MaybeReject(mRv); + } else if (mValue) { + RefPtr<ServiceWorkerWindowClient> windowClient = + new ServiceWorkerWindowClient(promise->GetParentObject(), *mValue); + promise->MaybeResolve(windowClient.get()); + } else { + promise->MaybeResolveWithUndefined(); + } + mPromiseProxy->CleanUp(); + return true; + } + }; + + RefPtr<PromiseWorkerProxy> mPromiseProxy; + nsString mClientId; +public: + GetRunnable(PromiseWorkerProxy* aPromiseProxy, + const nsAString& aClientId) + : mPromiseProxy(aPromiseProxy), + mClientId(aClientId) + { + } + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + + MutexAutoLock lock(mPromiseProxy->Lock()); + if (mPromiseProxy->CleanedUp()) { + return NS_OK; + } + + WorkerPrivate* workerPrivate = mPromiseProxy->GetWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + UniquePtr<ServiceWorkerClientInfo> result; + ErrorResult rv; + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + rv = NS_ERROR_FAILURE; + } else { + result = swm->GetClient(workerPrivate->GetPrincipal(), mClientId, rv); + } + + RefPtr<ResolvePromiseWorkerRunnable> r = + new ResolvePromiseWorkerRunnable(mPromiseProxy->GetWorkerPrivate(), + mPromiseProxy, Move(result), + rv.StealNSResult()); + rv.SuppressException(); + + r->Dispatch(); + return NS_OK; + } +}; + +class MatchAllRunnable final : public Runnable +{ + class ResolvePromiseWorkerRunnable final : public WorkerRunnable + { + RefPtr<PromiseWorkerProxy> mPromiseProxy; + nsTArray<ServiceWorkerClientInfo> mValue; + + public: + ResolvePromiseWorkerRunnable(WorkerPrivate* aWorkerPrivate, + PromiseWorkerProxy* aPromiseProxy, + nsTArray<ServiceWorkerClientInfo>& aValue) + : WorkerRunnable(aWorkerPrivate), + mPromiseProxy(aPromiseProxy) + { + AssertIsOnMainThread(); + mValue.SwapElements(aValue); + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + Promise* promise = mPromiseProxy->WorkerPromise(); + MOZ_ASSERT(promise); + + nsTArray<RefPtr<ServiceWorkerClient>> ret; + for (size_t i = 0; i < mValue.Length(); i++) { + ret.AppendElement(RefPtr<ServiceWorkerClient>( + new ServiceWorkerWindowClient(promise->GetParentObject(), + mValue.ElementAt(i)))); + } + + promise->MaybeResolve(ret); + mPromiseProxy->CleanUp(); + return true; + } + }; + + RefPtr<PromiseWorkerProxy> mPromiseProxy; + nsCString mScope; + bool mIncludeUncontrolled; +public: + MatchAllRunnable(PromiseWorkerProxy* aPromiseProxy, + const nsCString& aScope, + bool aIncludeUncontrolled) + : mPromiseProxy(aPromiseProxy), + mScope(aScope), + mIncludeUncontrolled(aIncludeUncontrolled) + { + MOZ_ASSERT(mPromiseProxy); + } + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + + MutexAutoLock lock(mPromiseProxy->Lock()); + if (mPromiseProxy->CleanedUp()) { + return NS_OK; + } + + nsTArray<ServiceWorkerClientInfo> result; + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->GetAllClients(mPromiseProxy->GetWorkerPrivate()->GetPrincipal(), + mScope, mIncludeUncontrolled, result); + } + RefPtr<ResolvePromiseWorkerRunnable> r = + new ResolvePromiseWorkerRunnable(mPromiseProxy->GetWorkerPrivate(), + mPromiseProxy, result); + + r->Dispatch(); + return NS_OK; + } +}; + +class ResolveClaimRunnable final : public WorkerRunnable +{ + RefPtr<PromiseWorkerProxy> mPromiseProxy; + nsresult mResult; + +public: + ResolveClaimRunnable(WorkerPrivate* aWorkerPrivate, + PromiseWorkerProxy* aPromiseProxy, + nsresult aResult) + : WorkerRunnable(aWorkerPrivate) + , mPromiseProxy(aPromiseProxy) + , mResult(aResult) + { + AssertIsOnMainThread(); + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<Promise> promise = mPromiseProxy->WorkerPromise(); + MOZ_ASSERT(promise); + + if (NS_SUCCEEDED(mResult)) { + promise->MaybeResolveWithUndefined(); + } else { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + } + + mPromiseProxy->CleanUp(); + return true; + } +}; + +class ClaimRunnable final : public Runnable +{ + RefPtr<PromiseWorkerProxy> mPromiseProxy; + nsCString mScope; + uint64_t mServiceWorkerID; + +public: + ClaimRunnable(PromiseWorkerProxy* aPromiseProxy, const nsCString& aScope) + : mPromiseProxy(aPromiseProxy) + , mScope(aScope) + // Safe to call GetWorkerPrivate() since we are being called on the worker + // thread via script (so no clean up has occured yet). + , mServiceWorkerID(aPromiseProxy->GetWorkerPrivate()->ServiceWorkerID()) + { + MOZ_ASSERT(aPromiseProxy); + } + + NS_IMETHOD + Run() override + { + MutexAutoLock lock(mPromiseProxy->Lock()); + if (mPromiseProxy->CleanedUp()) { + return NS_OK; + } + + WorkerPrivate* workerPrivate = mPromiseProxy->GetWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + nsresult rv = NS_OK; + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown + rv = NS_ERROR_FAILURE; + } else { + rv = swm->ClaimClients(workerPrivate->GetPrincipal(), mScope, + mServiceWorkerID); + } + + RefPtr<ResolveClaimRunnable> r = + new ResolveClaimRunnable(workerPrivate, mPromiseProxy, rv); + + r->Dispatch(); + return NS_OK; + } +}; + +class ResolveOpenWindowRunnable final : public WorkerRunnable +{ +public: + ResolveOpenWindowRunnable(PromiseWorkerProxy* aPromiseProxy, + UniquePtr<ServiceWorkerClientInfo>&& aClientInfo, + const nsresult aStatus) + : WorkerRunnable(aPromiseProxy->GetWorkerPrivate()) + , mPromiseProxy(aPromiseProxy) + , mClientInfo(Move(aClientInfo)) + , mStatus(aStatus) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aPromiseProxy); + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + Promise* promise = mPromiseProxy->WorkerPromise(); + if (NS_WARN_IF(NS_FAILED(mStatus))) { + promise->MaybeReject(mStatus); + } else if (mClientInfo) { + RefPtr<ServiceWorkerWindowClient> client = + new ServiceWorkerWindowClient(promise->GetParentObject(), + *mClientInfo); + promise->MaybeResolve(client); + } else { + promise->MaybeResolve(JS::NullHandleValue); + } + + mPromiseProxy->CleanUp(); + return true; + } + +private: + RefPtr<PromiseWorkerProxy> mPromiseProxy; + UniquePtr<ServiceWorkerClientInfo> mClientInfo; + const nsresult mStatus; +}; + +class WebProgressListener final : public nsIWebProgressListener, + public nsSupportsWeakReference +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(WebProgressListener, nsIWebProgressListener) + + WebProgressListener(PromiseWorkerProxy* aPromiseProxy, + ServiceWorkerPrivate* aServiceWorkerPrivate, + nsPIDOMWindowOuter* aWindow, + nsIURI* aBaseURI) + : mPromiseProxy(aPromiseProxy) + , mServiceWorkerPrivate(aServiceWorkerPrivate) + , mWindow(aWindow) + , mBaseURI(aBaseURI) + { + MOZ_ASSERT(aPromiseProxy); + MOZ_ASSERT(aServiceWorkerPrivate); + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->IsOuterWindow()); + MOZ_ASSERT(aBaseURI); + AssertIsOnMainThread(); + + mServiceWorkerPrivate->StoreISupports(static_cast<nsIWebProgressListener*>(this)); + } + + NS_IMETHOD + OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aStateFlags, nsresult aStatus) override + { + if (!(aStateFlags & STATE_IS_DOCUMENT) || + !(aStateFlags & (STATE_STOP | STATE_TRANSFERRING))) { + return NS_OK; + } + + // Our caller keeps a strong reference, so it is safe to remove the listener + // from ServiceWorkerPrivate. + mServiceWorkerPrivate->RemoveISupports(static_cast<nsIWebProgressListener*>(this)); + aWebProgress->RemoveProgressListener(this); + + MutexAutoLock lock(mPromiseProxy->Lock()); + if (mPromiseProxy->CleanedUp()) { + return NS_OK; + } + + nsCOMPtr<nsIDocument> doc = mWindow->GetExtantDoc(); + UniquePtr<ServiceWorkerClientInfo> clientInfo; + if (doc) { + // Check same origin. + nsCOMPtr<nsIScriptSecurityManager> securityManager = + nsContentUtils::GetSecurityManager(); + nsresult rv = securityManager->CheckSameOriginURI(doc->GetOriginalURI(), + mBaseURI, false); + if (NS_SUCCEEDED(rv)) { + clientInfo.reset(new ServiceWorkerClientInfo(doc)); + } + } + + RefPtr<ResolveOpenWindowRunnable> r = + new ResolveOpenWindowRunnable(mPromiseProxy, + Move(clientInfo), + NS_OK); + r->Dispatch(); + + return NS_OK; + } + + NS_IMETHOD + OnProgressChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) override + { + MOZ_ASSERT(false, "Unexpected notification."); + return NS_OK; + } + + NS_IMETHOD + OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsIURI* aLocation, + uint32_t aFlags) override + { + MOZ_ASSERT(false, "Unexpected notification."); + return NS_OK; + } + + NS_IMETHOD + OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsresult aStatus, const char16_t* aMessage) override + { + MOZ_ASSERT(false, "Unexpected notification."); + return NS_OK; + } + + NS_IMETHOD + OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aState) override + { + MOZ_ASSERT(false, "Unexpected notification."); + return NS_OK; + } + +private: + ~WebProgressListener() + { } + + RefPtr<PromiseWorkerProxy> mPromiseProxy; + RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate; + nsCOMPtr<nsPIDOMWindowOuter> mWindow; + nsCOMPtr<nsIURI> mBaseURI; +}; + +NS_IMPL_CYCLE_COLLECTING_ADDREF(WebProgressListener) +NS_IMPL_CYCLE_COLLECTING_RELEASE(WebProgressListener) +NS_IMPL_CYCLE_COLLECTION(WebProgressListener, mPromiseProxy, + mServiceWorkerPrivate, mWindow) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebProgressListener) + NS_INTERFACE_MAP_ENTRY(nsIWebProgressListener) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END + +class OpenWindowRunnable final : public Runnable +{ + RefPtr<PromiseWorkerProxy> mPromiseProxy; + nsString mUrl; + nsString mScope; + +public: + OpenWindowRunnable(PromiseWorkerProxy* aPromiseProxy, + const nsAString& aUrl, + const nsAString& aScope) + : mPromiseProxy(aPromiseProxy) + , mUrl(aUrl) + , mScope(aScope) + { + MOZ_ASSERT(aPromiseProxy); + MOZ_ASSERT(aPromiseProxy->GetWorkerPrivate()); + aPromiseProxy->GetWorkerPrivate()->AssertIsOnWorkerThread(); + } + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + + MutexAutoLock lock(mPromiseProxy->Lock()); + if (mPromiseProxy->CleanedUp()) { + return NS_OK; + } + +#ifdef MOZ_WIDGET_ANDROID + // This fires an intent that will start launching Fennec and foreground it, + // if necessary. + java::GeckoAppShell::OpenWindowForNotification(); +#endif + + nsCOMPtr<nsPIDOMWindowOuter> window; + nsresult rv = OpenWindow(getter_AddRefs(window)); + if (NS_SUCCEEDED(rv)) { + MOZ_ASSERT(window); + + rv = nsContentUtils::DispatchFocusChromeEvent(window); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + WorkerPrivate* workerPrivate = mPromiseProxy->GetWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + WorkerPrivate::LocationInfo& info = workerPrivate->GetLocationInfo(); + nsCOMPtr<nsIURI> baseURI; + nsresult rv = NS_NewURI(getter_AddRefs(baseURI), info.mHref); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIDocShell> docShell = window->GetDocShell(); + nsCOMPtr<nsIWebProgress> webProgress = do_GetInterface(docShell); + + if (!webProgress) { + return NS_ERROR_FAILURE; + } + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIPrincipal> principal = workerPrivate->GetPrincipal(); + MOZ_ASSERT(principal); + RefPtr<ServiceWorkerRegistrationInfo> registration = + swm->GetRegistration(principal, NS_ConvertUTF16toUTF8(mScope)); + if (NS_WARN_IF(!registration)) { + return NS_ERROR_FAILURE; + } + RefPtr<ServiceWorkerInfo> serviceWorkerInfo = + registration->GetServiceWorkerInfoById(workerPrivate->ServiceWorkerID()); + if (NS_WARN_IF(!serviceWorkerInfo)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIWebProgressListener> listener = + new WebProgressListener(mPromiseProxy, serviceWorkerInfo->WorkerPrivate(), + window, baseURI); + + rv = webProgress->AddProgressListener(listener, + nsIWebProgress::NOTIFY_STATE_DOCUMENT); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + return NS_OK; + } +#ifdef MOZ_WIDGET_ANDROID + else if (rv == NS_ERROR_NOT_AVAILABLE) { + // We couldn't get a browser window, so Fennec must not be running. + // Send an Intent to launch Fennec and wait for "BrowserChrome:Ready" + // to try opening a window again. + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + NS_ENSURE_STATE(os); + + WorkerPrivate* workerPrivate = mPromiseProxy->GetWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIPrincipal> principal = workerPrivate->GetPrincipal(); + MOZ_ASSERT(principal); + + RefPtr<ServiceWorkerRegistrationInfo> registration = + swm->GetRegistration(principal, NS_ConvertUTF16toUTF8(mScope)); + if (NS_WARN_IF(!registration)) { + return NS_ERROR_FAILURE; + } + + RefPtr<ServiceWorkerInfo> serviceWorkerInfo = + registration->GetServiceWorkerInfoById(workerPrivate->ServiceWorkerID()); + if (NS_WARN_IF(!serviceWorkerInfo)) { + return NS_ERROR_FAILURE; + } + + os->AddObserver(static_cast<nsIObserver*>(serviceWorkerInfo->WorkerPrivate()), + "BrowserChrome:Ready", true); + serviceWorkerInfo->WorkerPrivate()->AddPendingWindow(this); + return NS_OK; + } +#endif + + RefPtr<ResolveOpenWindowRunnable> resolveRunnable = + new ResolveOpenWindowRunnable(mPromiseProxy, nullptr, rv); + + Unused << NS_WARN_IF(!resolveRunnable->Dispatch()); + + return NS_OK; + } + +private: + nsresult + OpenWindow(nsPIDOMWindowOuter** aWindow) + { + MOZ_DIAGNOSTIC_ASSERT(aWindow); + WorkerPrivate* workerPrivate = mPromiseProxy->GetWorkerPrivate(); + + // [[1. Let url be the result of parsing url with entry settings object's API + // base URL.]] + nsCOMPtr<nsIURI> uri; + WorkerPrivate::LocationInfo& info = workerPrivate->GetLocationInfo(); + + nsCOMPtr<nsIURI> baseURI; + nsresult rv = NS_NewURI(getter_AddRefs(baseURI), info.mHref); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_TYPE_ERR; + } + + rv = NS_NewURI(getter_AddRefs(uri), mUrl, nullptr, baseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_TYPE_ERR; + } + + // [[6.1 Open Window]] + nsCOMPtr<nsIWindowMediator> wm = do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, + &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (XRE_IsContentProcess()) { + // ContentProcess + nsCOMPtr<nsIWindowWatcher> wwatch = + do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + nsCOMPtr<nsPIWindowWatcher> pwwatch(do_QueryInterface(wwatch)); + NS_ENSURE_STATE(pwwatch); + + nsCString spec; + rv = uri->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<mozIDOMWindowProxy> newWindow; + rv = pwwatch->OpenWindow2(nullptr, + spec.get(), + nullptr, + nullptr, + false, false, true, nullptr, + // Not a spammy popup; we got permission, we swear! + /* aIsPopupSpam = */ false, + // Don't force noopener. We're not passing in an + // opener anyway, and we _do_ want the returned + // window. + /* aForceNoOpener = */ false, + /* aLoadInfp = */ nullptr, + getter_AddRefs(newWindow)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + nsCOMPtr<nsPIDOMWindowOuter> pwindow = nsPIDOMWindowOuter::From(newWindow); + pwindow.forget(aWindow); + MOZ_DIAGNOSTIC_ASSERT(*aWindow); + return NS_OK; + } + + // Find the most recent browser window and open a new tab in it. + nsCOMPtr<nsPIDOMWindowOuter> browserWindow = + nsContentUtils::GetMostRecentNonPBWindow(); + if (!browserWindow) { + // It is possible to be running without a browser window on Mac OS, so + // we need to open a new chrome window. + // TODO(catalinb): open new chrome window. Bug 1218080 + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr<nsIDOMChromeWindow> chromeWin = do_QueryInterface(browserWindow); + if (NS_WARN_IF(!chromeWin)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIBrowserDOMWindow> bwin; + chromeWin->GetBrowserDOMWindow(getter_AddRefs(bwin)); + + if (NS_WARN_IF(!bwin)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<mozIDOMWindowProxy> win; + rv = bwin->OpenURI(uri, nullptr, + nsIBrowserDOMWindow::OPEN_DEFAULTWINDOW, + nsIBrowserDOMWindow::OPEN_NEW, + getter_AddRefs(win)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + NS_ENSURE_STATE(win); + + nsCOMPtr<nsPIDOMWindowOuter> pWin = nsPIDOMWindowOuter::From(win); + pWin.forget(aWindow); + MOZ_DIAGNOSTIC_ASSERT(*aWindow); + + return NS_OK; + } +}; + +} // namespace + +already_AddRefed<Promise> +ServiceWorkerClients::Get(const nsAString& aClientId, ErrorResult& aRv) +{ + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + workerPrivate->AssertIsOnWorkerThread(); + + RefPtr<Promise> promise = Promise::Create(mWorkerScope, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + RefPtr<PromiseWorkerProxy> promiseProxy = + PromiseWorkerProxy::Create(workerPrivate, promise); + if (!promiseProxy) { + promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + return promise.forget(); + } + + RefPtr<GetRunnable> r = + new GetRunnable(promiseProxy, aClientId); + MOZ_ALWAYS_SUCCEEDS(workerPrivate->DispatchToMainThread(r.forget())); + return promise.forget(); +} + +already_AddRefed<Promise> +ServiceWorkerClients::MatchAll(const ClientQueryOptions& aOptions, + ErrorResult& aRv) +{ + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + workerPrivate->AssertIsOnWorkerThread(); + + nsString scope; + mWorkerScope->GetScope(scope); + + if (aOptions.mType != ClientType::Window) { + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(mWorkerScope, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + RefPtr<PromiseWorkerProxy> promiseProxy = + PromiseWorkerProxy::Create(workerPrivate, promise); + if (!promiseProxy) { + promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + return promise.forget(); + } + + RefPtr<MatchAllRunnable> r = + new MatchAllRunnable(promiseProxy, + NS_ConvertUTF16toUTF8(scope), + aOptions.mIncludeUncontrolled); + MOZ_ALWAYS_SUCCEEDS(workerPrivate->DispatchToMainThread(r.forget())); + return promise.forget(); +} + +already_AddRefed<Promise> +ServiceWorkerClients::OpenWindow(const nsAString& aUrl, + ErrorResult& aRv) +{ + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + RefPtr<Promise> promise = Promise::Create(mWorkerScope, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (aUrl.EqualsLiteral("about:blank")) { + promise->MaybeReject(NS_ERROR_TYPE_ERR); + return promise.forget(); + } + + // [[4. If this algorithm is not allowed to show a popup ..]] + // In Gecko the service worker is allowed to show a popup only if the user + // just clicked on a notification. + if (!workerPrivate->GlobalScope()->WindowInteractionAllowed()) { + promise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return promise.forget(); + } + + RefPtr<PromiseWorkerProxy> promiseProxy = + PromiseWorkerProxy::Create(workerPrivate, promise); + + if (!promiseProxy) { + return nullptr; + } + + nsString scope; + mWorkerScope->GetScope(scope); + + RefPtr<OpenWindowRunnable> r = new OpenWindowRunnable(promiseProxy, + aUrl, scope); + MOZ_ALWAYS_SUCCEEDS(workerPrivate->DispatchToMainThread(r.forget())); + + return promise.forget(); +} + +already_AddRefed<Promise> +ServiceWorkerClients::Claim(ErrorResult& aRv) +{ + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + RefPtr<Promise> promise = Promise::Create(mWorkerScope, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + RefPtr<PromiseWorkerProxy> promiseProxy = + PromiseWorkerProxy::Create(workerPrivate, promise); + if (!promiseProxy) { + promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + return promise.forget(); + } + + nsString scope; + mWorkerScope->GetScope(scope); + + RefPtr<ClaimRunnable> runnable = + new ClaimRunnable(promiseProxy, NS_ConvertUTF16toUTF8(scope)); + + MOZ_ALWAYS_SUCCEEDS(workerPrivate->DispatchToMainThread(runnable.forget())); + return promise.forget(); +} diff --git a/dom/workers/ServiceWorkerClients.h b/dom/workers/ServiceWorkerClients.h new file mode 100644 index 000000000..3c507516f --- /dev/null +++ b/dom/workers/ServiceWorkerClients.h @@ -0,0 +1,64 @@ +/* -*- 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_workers_serviceworkerclients_h +#define mozilla_dom_workers_serviceworkerclients_h + +#include "nsWrapperCache.h" + +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/ClientsBinding.h" +#include "mozilla/ErrorResult.h" + +namespace mozilla { +namespace dom { +namespace workers { + +class ServiceWorkerClients final : public nsISupports, + public nsWrapperCache +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(ServiceWorkerClients) + + explicit ServiceWorkerClients(ServiceWorkerGlobalScope* aWorkerScope); + + already_AddRefed<Promise> + Get(const nsAString& aClientId, ErrorResult& aRv); + + already_AddRefed<Promise> + MatchAll(const ClientQueryOptions& aOptions, ErrorResult& aRv); + + already_AddRefed<Promise> + OpenWindow(const nsAString& aUrl, ErrorResult& aRv); + + already_AddRefed<Promise> + Claim(ErrorResult& aRv); + + JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + ServiceWorkerGlobalScope* + GetParentObject() const + { + return mWorkerScope; + } + +private: + ~ServiceWorkerClients() + { + } + + RefPtr<ServiceWorkerGlobalScope> mWorkerScope; +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_serviceworkerclients_h diff --git a/dom/workers/ServiceWorkerCommon.h b/dom/workers/ServiceWorkerCommon.h new file mode 100644 index 000000000..1c272c125 --- /dev/null +++ b/dom/workers/ServiceWorkerCommon.h @@ -0,0 +1,25 @@ +/* -*- 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_ServiceWorkerCommon_h +#define mozilla_dom_ServiceWorkerCommon_h + +namespace mozilla { +namespace dom { + +// Use multiples of 2 since they can be bitwise ORed when calling +// InvalidateServiceWorkerRegistrationWorker. +enum class WhichServiceWorker { + INSTALLING_WORKER = 1, + WAITING_WORKER = 2, + ACTIVE_WORKER = 4, +}; +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(WhichServiceWorker) + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ServiceWorkerCommon_h diff --git a/dom/workers/ServiceWorkerContainer.cpp b/dom/workers/ServiceWorkerContainer.cpp new file mode 100644 index 000000000..274d72d50 --- /dev/null +++ b/dom/workers/ServiceWorkerContainer.cpp @@ -0,0 +1,341 @@ +/* -*- 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 "ServiceWorkerContainer.h" + +#include "nsContentUtils.h" +#include "nsIDocument.h" +#include "nsIServiceWorkerManager.h" +#include "nsIURL.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" + +#include "nsCycleCollectionParticipant.h" +#include "nsServiceManagerUtils.h" + +#include "mozilla/dom/Navigator.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/ServiceWorkerContainerBinding.h" +#include "mozilla/dom/workers/bindings/ServiceWorker.h" + +#include "ServiceWorker.h" + +namespace mozilla { +namespace dom { + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(ServiceWorkerContainer) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper, + mControllerWorker, mReadyPromise) + +/* static */ bool +ServiceWorkerContainer::IsEnabled(JSContext* aCx, JSObject* aGlobal) +{ + MOZ_ASSERT(NS_IsMainThread()); + + JS::Rooted<JSObject*> global(aCx, aGlobal); + nsCOMPtr<nsPIDOMWindowInner> window = Navigator::GetWindowFromGlobal(global); + if (!window) { + return false; + } + + nsIDocument* doc = window->GetExtantDoc(); + if (!doc || nsContentUtils::IsInPrivateBrowsing(doc)) { + return false; + } + + return Preferences::GetBool("dom.serviceWorkers.enabled", false); +} + +ServiceWorkerContainer::ServiceWorkerContainer(nsPIDOMWindowInner* aWindow) + : DOMEventTargetHelper(aWindow) +{ +} + +ServiceWorkerContainer::~ServiceWorkerContainer() +{ + RemoveReadyPromise(); +} + +void +ServiceWorkerContainer::DisconnectFromOwner() +{ + mControllerWorker = nullptr; + RemoveReadyPromise(); + DOMEventTargetHelper::DisconnectFromOwner(); +} + +void +ServiceWorkerContainer::ControllerChanged(ErrorResult& aRv) +{ + mControllerWorker = nullptr; + aRv = DispatchTrustedEvent(NS_LITERAL_STRING("controllerchange")); +} + +void +ServiceWorkerContainer::RemoveReadyPromise() +{ + if (nsCOMPtr<nsPIDOMWindowInner> window = GetOwner()) { + nsCOMPtr<nsIServiceWorkerManager> swm = + mozilla::services::GetServiceWorkerManager(); + if (!swm) { + // If the browser is shutting down, we don't need to remove the promise. + return; + } + + swm->RemoveReadyPromise(window); + } +} + +JSObject* +ServiceWorkerContainer::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return ServiceWorkerContainerBinding::Wrap(aCx, this, aGivenProto); +} + +static nsresult +CheckForSlashEscapedCharsInPath(nsIURI* aURI) +{ + MOZ_ASSERT(aURI); + + // A URL that can't be downcast to a standard URL is an invalid URL and should + // be treated as such and fail with SecurityError. + nsCOMPtr<nsIURL> url(do_QueryInterface(aURI)); + if (NS_WARN_IF(!url)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + nsAutoCString path; + nsresult rv = url->GetFilePath(path); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + ToLowerCase(path); + if (path.Find("%2f") != kNotFound || + path.Find("%5c") != kNotFound) { + return NS_ERROR_DOM_TYPE_ERR; + } + + return NS_OK; +} + +already_AddRefed<Promise> +ServiceWorkerContainer::Register(const nsAString& aScriptURL, + const RegistrationOptions& aOptions, + ErrorResult& aRv) +{ + nsCOMPtr<nsISupports> promise; + + nsCOMPtr<nsIServiceWorkerManager> swm = mozilla::services::GetServiceWorkerManager(); + if (!swm) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsCOMPtr<nsIURI> baseURI; + + nsIDocument* doc = GetEntryDocument(); + if (doc) { + baseURI = doc->GetBaseURI(); + } else { + // XXXnsm. One of our devtools browser test calls register() from a content + // script where there is no valid entry document. Use the window to resolve + // the uri in that case. + nsCOMPtr<nsPIDOMWindowInner> window = GetOwner(); + nsCOMPtr<nsPIDOMWindowOuter> outerWindow; + if (window && (outerWindow = window->GetOuterWindow()) && + outerWindow->GetServiceWorkersTestingEnabled()) { + baseURI = window->GetDocBaseURI(); + } + } + + nsresult rv; + nsCOMPtr<nsIURI> scriptURI; + rv = NS_NewURI(getter_AddRefs(scriptURI), aScriptURL, nullptr, baseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.ThrowTypeError<MSG_INVALID_URL>(aScriptURL); + return nullptr; + } + + aRv = CheckForSlashEscapedCharsInPath(scriptURI); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + // In ServiceWorkerContainer.register() the scope argument is parsed against + // different base URLs depending on whether it was passed or not. + nsCOMPtr<nsIURI> scopeURI; + + // Step 4. If none passed, parse against script's URL + if (!aOptions.mScope.WasPassed()) { + NS_NAMED_LITERAL_STRING(defaultScope, "./"); + rv = NS_NewURI(getter_AddRefs(scopeURI), defaultScope, + nullptr, scriptURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + nsAutoCString spec; + scriptURI->GetSpec(spec); + NS_ConvertUTF8toUTF16 wSpec(spec); + aRv.ThrowTypeError<MSG_INVALID_SCOPE>(defaultScope, wSpec); + return nullptr; + } + } else { + // Step 5. Parse against entry settings object's base URL. + rv = NS_NewURI(getter_AddRefs(scopeURI), aOptions.mScope.Value(), + nullptr, baseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + nsIURI* uri = baseURI ? baseURI : scriptURI; + nsAutoCString spec; + uri->GetSpec(spec); + NS_ConvertUTF8toUTF16 wSpec(spec); + aRv.ThrowTypeError<MSG_INVALID_SCOPE>(aOptions.mScope.Value(), wSpec); + return nullptr; + } + + aRv = CheckForSlashEscapedCharsInPath(scopeURI); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + + // The spec says that the "client" passed to Register() must be the global + // where the ServiceWorkerContainer was retrieved from. + aRv = swm->Register(GetOwner(), scopeURI, scriptURI, getter_AddRefs(promise)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + RefPtr<Promise> ret = static_cast<Promise*>(promise.get()); + MOZ_ASSERT(ret); + return ret.forget(); +} + +already_AddRefed<workers::ServiceWorker> +ServiceWorkerContainer::GetController() +{ + if (!mControllerWorker) { + nsCOMPtr<nsIServiceWorkerManager> swm = mozilla::services::GetServiceWorkerManager(); + if (!swm) { + return nullptr; + } + + // TODO: What should we do here if the ServiceWorker script fails to load? + // In theory the DOM ServiceWorker object can exist without the worker + // thread running, but it seems our design does not expect that. + nsCOMPtr<nsISupports> serviceWorker; + nsresult rv = swm->GetDocumentController(GetOwner(), + getter_AddRefs(serviceWorker)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + mControllerWorker = + static_cast<workers::ServiceWorker*>(serviceWorker.get()); + } + + RefPtr<workers::ServiceWorker> ref = mControllerWorker; + return ref.forget(); +} + +already_AddRefed<Promise> +ServiceWorkerContainer::GetRegistrations(ErrorResult& aRv) +{ + nsresult rv; + nsCOMPtr<nsIServiceWorkerManager> swm = do_GetService(SERVICEWORKERMANAGER_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return nullptr; + } + + nsCOMPtr<nsISupports> promise; + aRv = swm->GetRegistrations(GetOwner(), getter_AddRefs(promise)); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<Promise> ret = static_cast<Promise*>(promise.get()); + MOZ_ASSERT(ret); + return ret.forget(); +} + +already_AddRefed<Promise> +ServiceWorkerContainer::GetRegistration(const nsAString& aDocumentURL, + ErrorResult& aRv) +{ + nsresult rv; + nsCOMPtr<nsIServiceWorkerManager> swm = do_GetService(SERVICEWORKERMANAGER_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return nullptr; + } + + nsCOMPtr<nsISupports> promise; + aRv = swm->GetRegistration(GetOwner(), aDocumentURL, getter_AddRefs(promise)); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<Promise> ret = static_cast<Promise*>(promise.get()); + MOZ_ASSERT(ret); + return ret.forget(); +} + +Promise* +ServiceWorkerContainer::GetReady(ErrorResult& aRv) +{ + if (mReadyPromise) { + return mReadyPromise; + } + + nsCOMPtr<nsIServiceWorkerManager> swm = mozilla::services::GetServiceWorkerManager(); + if (!swm) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsCOMPtr<nsISupports> promise; + aRv = swm->GetReadyPromise(GetOwner(), getter_AddRefs(promise)); + + mReadyPromise = static_cast<Promise*>(promise.get()); + return mReadyPromise; +} + +// Testing only. +void +ServiceWorkerContainer::GetScopeForUrl(const nsAString& aUrl, + nsString& aScope, + ErrorResult& aRv) +{ + nsCOMPtr<nsIServiceWorkerManager> swm = mozilla::services::GetServiceWorkerManager(); + if (!swm) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + nsCOMPtr<nsPIDOMWindowInner> window = GetOwner(); + if (NS_WARN_IF(!window)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + nsCOMPtr<nsIDocument> doc = window->GetExtantDoc(); + if (NS_WARN_IF(!doc)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + aRv = swm->GetScopeForUrl(doc->NodePrincipal(), + aUrl, aScope); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/ServiceWorkerContainer.h b/dom/workers/ServiceWorkerContainer.h new file mode 100644 index 000000000..efd70a601 --- /dev/null +++ b/dom/workers/ServiceWorkerContainer.h @@ -0,0 +1,87 @@ +/* -*- 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_serviceworkercontainer_h__ +#define mozilla_dom_serviceworkercontainer_h__ + +#include "mozilla/DOMEventTargetHelper.h" + +class nsPIDOMWindowInner; + +namespace mozilla { +namespace dom { + +class Promise; +struct RegistrationOptions; + +namespace workers { +class ServiceWorker; +} // namespace workers + +// Lightweight serviceWorker APIs collection. +class ServiceWorkerContainer final : public DOMEventTargetHelper +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ServiceWorkerContainer, DOMEventTargetHelper) + + IMPL_EVENT_HANDLER(controllerchange) + IMPL_EVENT_HANDLER(error) + IMPL_EVENT_HANDLER(message) + + static bool IsEnabled(JSContext* aCx, JSObject* aGlobal); + + explicit ServiceWorkerContainer(nsPIDOMWindowInner* aWindow); + + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + already_AddRefed<Promise> + Register(const nsAString& aScriptURL, + const RegistrationOptions& aOptions, + ErrorResult& aRv); + + already_AddRefed<workers::ServiceWorker> + GetController(); + + already_AddRefed<Promise> + GetRegistration(const nsAString& aDocumentURL, + ErrorResult& aRv); + + already_AddRefed<Promise> + GetRegistrations(ErrorResult& aRv); + + Promise* + GetReady(ErrorResult& aRv); + + // Testing only. + void + GetScopeForUrl(const nsAString& aUrl, nsString& aScope, ErrorResult& aRv); + + // DOMEventTargetHelper + void DisconnectFromOwner() override; + + // Invalidates |mControllerWorker| and dispatches a "controllerchange" + // event. + void + ControllerChanged(ErrorResult& aRv); + +private: + ~ServiceWorkerContainer(); + + void RemoveReadyPromise(); + + // This only changes when a worker hijacks everything in its scope by calling + // claim. + RefPtr<workers::ServiceWorker> mControllerWorker; + + RefPtr<Promise> mReadyPromise; +}; + +} // namespace dom +} // namespace mozilla + +#endif /* mozilla_dom_workers_serviceworkercontainer_h__ */ diff --git a/dom/workers/ServiceWorkerEvents.cpp b/dom/workers/ServiceWorkerEvents.cpp new file mode 100644 index 000000000..09b09a24b --- /dev/null +++ b/dom/workers/ServiceWorkerEvents.cpp @@ -0,0 +1,1280 @@ +/* -*- 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 "ServiceWorkerEvents.h" + +#include "nsAutoPtr.h" +#include "nsIConsoleReportCollector.h" +#include "nsIHttpChannelInternal.h" +#include "nsINetworkInterceptController.h" +#include "nsIOutputStream.h" +#include "nsIScriptError.h" +#include "nsIUnicodeDecoder.h" +#include "nsIUnicodeEncoder.h" +#include "nsContentPolicyUtils.h" +#include "nsContentUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsStreamUtils.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsSerializationHelper.h" +#include "nsQueryObject.h" +#include "ServiceWorkerClient.h" +#include "ServiceWorkerManager.h" + +#include "mozilla/ErrorResult.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/BodyUtil.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/DOMExceptionBinding.h" +#include "mozilla/dom/EncodingUtils.h" +#include "mozilla/dom/FetchEventBinding.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/PushEventBinding.h" +#include "mozilla/dom/PushMessageDataBinding.h" +#include "mozilla/dom/PushUtil.h" +#include "mozilla/dom/Request.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/dom/workers/bindings/ServiceWorker.h" + +#include "js/Conversions.h" +#include "js/TypeDecls.h" +#include "WorkerPrivate.h" +#include "xpcpublic.h" + +using namespace mozilla::dom; +using namespace mozilla::dom::workers; + +namespace { + +void +AsyncLog(nsIInterceptedChannel *aInterceptedChannel, + const nsACString& aRespondWithScriptSpec, + uint32_t aRespondWithLineNumber, uint32_t aRespondWithColumnNumber, + const nsACString& aMessageName, const nsTArray<nsString>& aParams) +{ + MOZ_ASSERT(aInterceptedChannel); + nsCOMPtr<nsIConsoleReportCollector> reporter = + aInterceptedChannel->GetConsoleReportCollector(); + if (reporter) { + reporter->AddConsoleReport(nsIScriptError::errorFlag, + NS_LITERAL_CSTRING("Service Worker Interception"), + nsContentUtils::eDOM_PROPERTIES, + aRespondWithScriptSpec, + aRespondWithLineNumber, + aRespondWithColumnNumber, + aMessageName, aParams); + } +} + +template<typename... Params> +void +AsyncLog(nsIInterceptedChannel* aInterceptedChannel, + const nsACString& aRespondWithScriptSpec, + uint32_t aRespondWithLineNumber, uint32_t aRespondWithColumnNumber, + // We have to list one explicit string so that calls with an + // nsTArray of params won't end up in here. + const nsACString& aMessageName, const nsAString& aFirstParam, + Params&&... aParams) +{ + nsTArray<nsString> paramsList(sizeof...(Params) + 1); + StringArrayAppender::Append(paramsList, sizeof...(Params) + 1, + aFirstParam, Forward<Params>(aParams)...); + AsyncLog(aInterceptedChannel, aRespondWithScriptSpec, aRespondWithLineNumber, + aRespondWithColumnNumber, aMessageName, paramsList); +} + +} // anonymous namespace + +BEGIN_WORKERS_NAMESPACE + +CancelChannelRunnable::CancelChannelRunnable(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel, + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration, + nsresult aStatus) + : mChannel(aChannel) + , mRegistration(aRegistration) + , mStatus(aStatus) +{ +} + +NS_IMETHODIMP +CancelChannelRunnable::Run() +{ + MOZ_ASSERT(NS_IsMainThread()); + mChannel->Cancel(mStatus); + mRegistration->MaybeScheduleUpdate(); + return NS_OK; +} + +FetchEvent::FetchEvent(EventTarget* aOwner) + : ExtendableEvent(aOwner) + , mPreventDefaultLineNumber(0) + , mPreventDefaultColumnNumber(0) + , mIsReload(false) + , mWaitToRespond(false) +{ +} + +FetchEvent::~FetchEvent() +{ +} + +void +FetchEvent::PostInit(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel, + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration, + const nsACString& aScriptSpec) +{ + mChannel = aChannel; + mRegistration = aRegistration; + mScriptSpec.Assign(aScriptSpec); +} + +/*static*/ already_AddRefed<FetchEvent> +FetchEvent::Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const FetchEventInit& aOptions, + ErrorResult& aRv) +{ + RefPtr<EventTarget> owner = do_QueryObject(aGlobal.GetAsSupports()); + MOZ_ASSERT(owner); + RefPtr<FetchEvent> e = new FetchEvent(owner); + bool trusted = e->Init(owner); + e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable); + e->SetTrusted(trusted); + e->SetComposed(aOptions.mComposed); + e->mRequest = aOptions.mRequest; + e->mClientId = aOptions.mClientId; + e->mIsReload = aOptions.mIsReload; + return e.forget(); +} + +namespace { + +class FinishResponse final : public Runnable +{ + nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel; + RefPtr<InternalResponse> mInternalResponse; + ChannelInfo mWorkerChannelInfo; + const nsCString mScriptSpec; + const nsCString mResponseURLSpec; + +public: + FinishResponse(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel, + InternalResponse* aInternalResponse, + const ChannelInfo& aWorkerChannelInfo, + const nsACString& aScriptSpec, + const nsACString& aResponseURLSpec) + : mChannel(aChannel) + , mInternalResponse(aInternalResponse) + , mWorkerChannelInfo(aWorkerChannelInfo) + , mScriptSpec(aScriptSpec) + , mResponseURLSpec(aResponseURLSpec) + { + } + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + + nsCOMPtr<nsIChannel> underlyingChannel; + nsresult rv = mChannel->GetChannel(getter_AddRefs(underlyingChannel)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(underlyingChannel, NS_ERROR_UNEXPECTED); + nsCOMPtr<nsILoadInfo> loadInfo = underlyingChannel->GetLoadInfo(); + + if (!CSPPermitsResponse(loadInfo)) { + mChannel->Cancel(NS_ERROR_CONTENT_BLOCKED); + return NS_OK; + } + + ChannelInfo channelInfo; + if (mInternalResponse->GetChannelInfo().IsInitialized()) { + channelInfo = mInternalResponse->GetChannelInfo(); + } else { + // We are dealing with a synthesized response here, so fall back to the + // channel info for the worker script. + channelInfo = mWorkerChannelInfo; + } + rv = mChannel->SetChannelInfo(&channelInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + mChannel->Cancel(NS_ERROR_INTERCEPTION_FAILED); + return NS_OK; + } + + rv = mChannel->SynthesizeStatus(mInternalResponse->GetUnfilteredStatus(), + mInternalResponse->GetUnfilteredStatusText()); + if (NS_WARN_IF(NS_FAILED(rv))) { + mChannel->Cancel(NS_ERROR_INTERCEPTION_FAILED); + return NS_OK; + } + + AutoTArray<InternalHeaders::Entry, 5> entries; + mInternalResponse->UnfilteredHeaders()->GetEntries(entries); + for (uint32_t i = 0; i < entries.Length(); ++i) { + mChannel->SynthesizeHeader(entries[i].mName, entries[i].mValue); + } + + loadInfo->MaybeIncreaseTainting(mInternalResponse->GetTainting()); + + rv = mChannel->FinishSynthesizedResponse(mResponseURLSpec); + if (NS_WARN_IF(NS_FAILED(rv))) { + mChannel->Cancel(NS_ERROR_INTERCEPTION_FAILED); + return NS_OK; + } + + nsCOMPtr<nsIObserverService> obsService = services::GetObserverService(); + if (obsService) { + obsService->NotifyObservers(underlyingChannel, "service-worker-synthesized-response", nullptr); + } + + return rv; + } + bool CSPPermitsResponse(nsILoadInfo* aLoadInfo) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aLoadInfo); + nsresult rv; + nsCOMPtr<nsIURI> uri; + nsCString url = mInternalResponse->GetUnfilteredURL(); + if (url.IsEmpty()) { + // Synthetic response. The buck stops at the worker script. + url = mScriptSpec; + } + rv = NS_NewURI(getter_AddRefs(uri), url, nullptr, nullptr); + NS_ENSURE_SUCCESS(rv, false); + int16_t decision = nsIContentPolicy::ACCEPT; + rv = NS_CheckContentLoadPolicy(aLoadInfo->InternalContentPolicyType(), uri, + aLoadInfo->LoadingPrincipal(), + aLoadInfo->LoadingNode(), EmptyCString(), + nullptr, &decision); + NS_ENSURE_SUCCESS(rv, false); + return decision == nsIContentPolicy::ACCEPT; + } +}; + +class RespondWithHandler final : public PromiseNativeHandler +{ + nsMainThreadPtrHandle<nsIInterceptedChannel> mInterceptedChannel; + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration; + const RequestMode mRequestMode; + const RequestRedirect mRequestRedirectMode; +#ifdef DEBUG + const bool mIsClientRequest; +#endif + const nsCString mScriptSpec; + const nsString mRequestURL; + const nsCString mRespondWithScriptSpec; + const uint32_t mRespondWithLineNumber; + const uint32_t mRespondWithColumnNumber; + bool mRequestWasHandled; +public: + NS_DECL_ISUPPORTS + + RespondWithHandler(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel, + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration, + RequestMode aRequestMode, bool aIsClientRequest, + RequestRedirect aRedirectMode, + const nsACString& aScriptSpec, + const nsAString& aRequestURL, + const nsACString& aRespondWithScriptSpec, + uint32_t aRespondWithLineNumber, + uint32_t aRespondWithColumnNumber) + : mInterceptedChannel(aChannel) + , mRegistration(aRegistration) + , mRequestMode(aRequestMode) + , mRequestRedirectMode(aRedirectMode) +#ifdef DEBUG + , mIsClientRequest(aIsClientRequest) +#endif + , mScriptSpec(aScriptSpec) + , mRequestURL(aRequestURL) + , mRespondWithScriptSpec(aRespondWithScriptSpec) + , mRespondWithLineNumber(aRespondWithLineNumber) + , mRespondWithColumnNumber(aRespondWithColumnNumber) + , mRequestWasHandled(false) + { + } + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; + + void CancelRequest(nsresult aStatus); + + void AsyncLog(const nsACString& aMessageName, const nsTArray<nsString>& aParams) + { + ::AsyncLog(mInterceptedChannel, mRespondWithScriptSpec, mRespondWithLineNumber, + mRespondWithColumnNumber, aMessageName, aParams); + } + + void AsyncLog(const nsACString& aSourceSpec, uint32_t aLine, uint32_t aColumn, + const nsACString& aMessageName, const nsTArray<nsString>& aParams) + { + ::AsyncLog(mInterceptedChannel, aSourceSpec, aLine, aColumn, aMessageName, + aParams); + } + +private: + ~RespondWithHandler() + { + if (!mRequestWasHandled) { + ::AsyncLog(mInterceptedChannel, mRespondWithScriptSpec, + mRespondWithLineNumber, mRespondWithColumnNumber, + NS_LITERAL_CSTRING("InterceptionFailedWithURL"), mRequestURL); + CancelRequest(NS_ERROR_INTERCEPTION_FAILED); + } + } +}; + +struct RespondWithClosure +{ + nsMainThreadPtrHandle<nsIInterceptedChannel> mInterceptedChannel; + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration; + RefPtr<InternalResponse> mInternalResponse; + ChannelInfo mWorkerChannelInfo; + const nsCString mScriptSpec; + const nsCString mResponseURLSpec; + const nsString mRequestURL; + const nsCString mRespondWithScriptSpec; + const uint32_t mRespondWithLineNumber; + const uint32_t mRespondWithColumnNumber; + + RespondWithClosure(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel, + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration, + InternalResponse* aInternalResponse, + const ChannelInfo& aWorkerChannelInfo, + const nsCString& aScriptSpec, + const nsACString& aResponseURLSpec, + const nsAString& aRequestURL, + const nsACString& aRespondWithScriptSpec, + uint32_t aRespondWithLineNumber, + uint32_t aRespondWithColumnNumber) + : mInterceptedChannel(aChannel) + , mRegistration(aRegistration) + , mInternalResponse(aInternalResponse) + , mWorkerChannelInfo(aWorkerChannelInfo) + , mScriptSpec(aScriptSpec) + , mResponseURLSpec(aResponseURLSpec) + , mRequestURL(aRequestURL) + , mRespondWithScriptSpec(aRespondWithScriptSpec) + , mRespondWithLineNumber(aRespondWithLineNumber) + , mRespondWithColumnNumber(aRespondWithColumnNumber) + { + } +}; + +void RespondWithCopyComplete(void* aClosure, nsresult aStatus) +{ + nsAutoPtr<RespondWithClosure> data(static_cast<RespondWithClosure*>(aClosure)); + nsCOMPtr<nsIRunnable> event; + if (NS_WARN_IF(NS_FAILED(aStatus))) { + AsyncLog(data->mInterceptedChannel, data->mRespondWithScriptSpec, + data->mRespondWithLineNumber, data->mRespondWithColumnNumber, + NS_LITERAL_CSTRING("InterceptionFailedWithURL"), + data->mRequestURL); + event = new CancelChannelRunnable(data->mInterceptedChannel, + data->mRegistration, + NS_ERROR_INTERCEPTION_FAILED); + } else { + event = new FinishResponse(data->mInterceptedChannel, + data->mInternalResponse, + data->mWorkerChannelInfo, + data->mScriptSpec, + data->mResponseURLSpec); + } + // In theory this can happen after the worker thread is terminated. + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + if (worker) { + MOZ_ALWAYS_SUCCEEDS(worker->DispatchToMainThread(event.forget())); + } else { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(event.forget())); + } +} + +namespace { + +void +ExtractErrorValues(JSContext* aCx, JS::Handle<JS::Value> aValue, + nsACString& aSourceSpecOut, uint32_t *aLineOut, + uint32_t *aColumnOut, nsString& aMessageOut) +{ + MOZ_ASSERT(aLineOut); + MOZ_ASSERT(aColumnOut); + + if (aValue.isObject()) { + JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); + RefPtr<DOMException> domException; + + // Try to process as an Error object. Use the file/line/column values + // from the Error as they will be more specific to the root cause of + // the problem. + JSErrorReport* err = obj ? JS_ErrorFromException(aCx, obj) : nullptr; + if (err) { + // Use xpc to extract the error message only. We don't actually send + // this report anywhere. + RefPtr<xpc::ErrorReport> report = new xpc::ErrorReport(); + report->Init(err, + "<unknown>", // toString result + false, // chrome + 0); // window ID + + if (!report->mFileName.IsEmpty()) { + CopyUTF16toUTF8(report->mFileName, aSourceSpecOut); + *aLineOut = report->mLineNumber; + *aColumnOut = report->mColumn; + } + aMessageOut.Assign(report->mErrorMsg); + } + + // Next, try to unwrap the rejection value as a DOMException. + else if(NS_SUCCEEDED(UNWRAP_OBJECT(DOMException, obj, domException))) { + + nsAutoString filename; + domException->GetFilename(aCx, filename); + if (!filename.IsEmpty()) { + CopyUTF16toUTF8(filename, aSourceSpecOut); + *aLineOut = domException->LineNumber(aCx); + *aColumnOut = domException->ColumnNumber(); + } + + domException->GetName(aMessageOut); + aMessageOut.AppendLiteral(": "); + + nsAutoString message; + domException->GetMessageMoz(message); + aMessageOut.Append(message); + } + } + + // If we could not unwrap a specific error type, then perform default safe + // string conversions on primitives. Objects will result in "[Object]" + // unfortunately. + if (aMessageOut.IsEmpty()) { + nsAutoJSString jsString; + if (jsString.init(aCx, aValue)) { + aMessageOut = jsString; + } else { + JS_ClearPendingException(aCx); + } + } +} + +} // anonymous namespace + +class MOZ_STACK_CLASS AutoCancel +{ + RefPtr<RespondWithHandler> mOwner; + nsCString mSourceSpec; + uint32_t mLine; + uint32_t mColumn; + nsCString mMessageName; + nsTArray<nsString> mParams; + +public: + AutoCancel(RespondWithHandler* aOwner, const nsString& aRequestURL) + : mOwner(aOwner) + , mLine(0) + , mColumn(0) + , mMessageName(NS_LITERAL_CSTRING("InterceptionFailedWithURL")) + { + mParams.AppendElement(aRequestURL); + } + + ~AutoCancel() + { + if (mOwner) { + if (mSourceSpec.IsEmpty()) { + mOwner->AsyncLog(mMessageName, mParams); + } else { + mOwner->AsyncLog(mSourceSpec, mLine, mColumn, mMessageName, mParams); + } + mOwner->CancelRequest(NS_ERROR_INTERCEPTION_FAILED); + } + } + + template<typename... Params> + void SetCancelMessage(const nsACString& aMessageName, Params&&... aParams) + { + MOZ_ASSERT(mOwner); + MOZ_ASSERT(mMessageName.EqualsLiteral("InterceptionFailedWithURL")); + MOZ_ASSERT(mParams.Length() == 1); + mMessageName = aMessageName; + mParams.Clear(); + StringArrayAppender::Append(mParams, sizeof...(Params), + Forward<Params>(aParams)...); + } + + template<typename... Params> + void SetCancelMessageAndLocation(const nsACString& aSourceSpec, + uint32_t aLine, uint32_t aColumn, + const nsACString& aMessageName, + Params&&... aParams) + { + MOZ_ASSERT(mOwner); + MOZ_ASSERT(mMessageName.EqualsLiteral("InterceptionFailedWithURL")); + MOZ_ASSERT(mParams.Length() == 1); + + mSourceSpec = aSourceSpec; + mLine = aLine; + mColumn = aColumn; + + mMessageName = aMessageName; + mParams.Clear(); + StringArrayAppender::Append(mParams, sizeof...(Params), + Forward<Params>(aParams)...); + } + + void Reset() + { + mOwner = nullptr; + } +}; + +NS_IMPL_ISUPPORTS0(RespondWithHandler) + +void +RespondWithHandler::ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) +{ + AutoCancel autoCancel(this, mRequestURL); + + if (!aValue.isObject()) { + NS_WARNING("FetchEvent::RespondWith was passed a promise resolved to a non-Object value"); + + nsCString sourceSpec; + uint32_t line = 0; + uint32_t column = 0; + nsString valueString; + ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column, valueString); + + autoCancel.SetCancelMessageAndLocation(sourceSpec, line, column, + NS_LITERAL_CSTRING("InterceptedNonResponseWithURL"), + mRequestURL, valueString); + return; + } + + RefPtr<Response> response; + nsresult rv = UNWRAP_OBJECT(Response, &aValue.toObject(), response); + if (NS_FAILED(rv)) { + nsCString sourceSpec; + uint32_t line = 0; + uint32_t column = 0; + nsString valueString; + ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column, valueString); + + autoCancel.SetCancelMessageAndLocation(sourceSpec, line, column, + NS_LITERAL_CSTRING("InterceptedNonResponseWithURL"), + mRequestURL, valueString); + return; + } + + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + worker->AssertIsOnWorkerThread(); + + // Section "HTTP Fetch", step 3.3: + // If one of the following conditions is true, return a network error: + // * response's type is "error". + // * request's mode is not "no-cors" and response's type is "opaque". + // * request's redirect mode is not "manual" and response's type is + // "opaqueredirect". + // * request's redirect mode is not "follow" and response's url list + // has more than one item. + + if (response->Type() == ResponseType::Error) { + autoCancel.SetCancelMessage( + NS_LITERAL_CSTRING("InterceptedErrorResponseWithURL"), mRequestURL); + return; + } + + MOZ_ASSERT_IF(mIsClientRequest, mRequestMode == RequestMode::Same_origin || + mRequestMode == RequestMode::Navigate); + + if (response->Type() == ResponseType::Opaque && mRequestMode != RequestMode::No_cors) { + uint32_t mode = static_cast<uint32_t>(mRequestMode); + NS_ConvertASCIItoUTF16 modeString(RequestModeValues::strings[mode].value, + RequestModeValues::strings[mode].length); + + autoCancel.SetCancelMessage( + NS_LITERAL_CSTRING("BadOpaqueInterceptionRequestModeWithURL"), + mRequestURL, modeString); + return; + } + + if (mRequestRedirectMode != RequestRedirect::Manual && + response->Type() == ResponseType::Opaqueredirect) { + autoCancel.SetCancelMessage( + NS_LITERAL_CSTRING("BadOpaqueRedirectInterceptionWithURL"), mRequestURL); + return; + } + + if (mRequestRedirectMode != RequestRedirect::Follow && response->Redirected()) { + autoCancel.SetCancelMessage( + NS_LITERAL_CSTRING("BadRedirectModeInterceptionWithURL"), mRequestURL); + return; + } + + if (NS_WARN_IF(response->BodyUsed())) { + autoCancel.SetCancelMessage( + NS_LITERAL_CSTRING("InterceptedUsedResponseWithURL"), mRequestURL); + return; + } + + RefPtr<InternalResponse> ir = response->GetInternalResponse(); + if (NS_WARN_IF(!ir)) { + return; + } + // When an opaque response is encountered, we need the original channel's principal + // to reflect the final URL. Non-opaque responses are either same-origin or CORS-enabled + // cross-origin responses, which are treated as same-origin by consumers. + nsCString responseURL; + if (response->Type() == ResponseType::Opaque) { + responseURL = ir->GetUnfilteredURL(); + if (NS_WARN_IF(responseURL.IsEmpty())) { + return; + } + } + nsAutoPtr<RespondWithClosure> closure(new RespondWithClosure(mInterceptedChannel, + mRegistration, ir, + worker->GetChannelInfo(), + mScriptSpec, + responseURL, + mRequestURL, + mRespondWithScriptSpec, + mRespondWithLineNumber, + mRespondWithColumnNumber)); + nsCOMPtr<nsIInputStream> body; + ir->GetUnfilteredBody(getter_AddRefs(body)); + // Errors and redirects may not have a body. + if (body) { + response->SetBodyUsed(); + + nsCOMPtr<nsIOutputStream> responseBody; + rv = mInterceptedChannel->GetResponseBody(getter_AddRefs(responseBody)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + const uint32_t kCopySegmentSize = 4096; + + // Depending on how the Response passed to .respondWith() was created, we may + // get a non-buffered input stream. In addition, in some configurations the + // destination channel's output stream can be unbuffered. We wrap the output + // stream side here so that NS_AsyncCopy() works. Wrapping the output side + // provides the most consistent operation since there are fewer stream types + // we are writing to. The input stream can be a wide variety of concrete + // objects which may or many not play well with NS_InputStreamIsBuffered(). + if (!NS_OutputStreamIsBuffered(responseBody)) { + nsCOMPtr<nsIOutputStream> buffered; + rv = NS_NewBufferedOutputStream(getter_AddRefs(buffered), responseBody, + kCopySegmentSize); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + responseBody = buffered; + } + + nsCOMPtr<nsIEventTarget> stsThread = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(!stsThread)) { + return; + } + + // XXXnsm, Fix for Bug 1141332 means that if we decide to make this + // streaming at some point, we'll need a different solution to that bug. + rv = NS_AsyncCopy(body, responseBody, stsThread, NS_ASYNCCOPY_VIA_WRITESEGMENTS, + kCopySegmentSize, RespondWithCopyComplete, closure.forget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } else { + RespondWithCopyComplete(closure.forget(), NS_OK); + } + + MOZ_ASSERT(!closure); + autoCancel.Reset(); + mRequestWasHandled = true; +} + +void +RespondWithHandler::RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) +{ + nsCString sourceSpec = mRespondWithScriptSpec; + uint32_t line = mRespondWithLineNumber; + uint32_t column = mRespondWithColumnNumber; + nsString valueString; + + ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column, valueString); + + ::AsyncLog(mInterceptedChannel, sourceSpec, line, column, + NS_LITERAL_CSTRING("InterceptionRejectedResponseWithURL"), + mRequestURL, valueString); + + CancelRequest(NS_ERROR_INTERCEPTION_FAILED); +} + +void +RespondWithHandler::CancelRequest(nsresult aStatus) +{ + nsCOMPtr<nsIRunnable> runnable = + new CancelChannelRunnable(mInterceptedChannel, mRegistration, aStatus); + // Note, this may run off the worker thread during worker termination. + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + if (worker) { + MOZ_ALWAYS_SUCCEEDS(worker->DispatchToMainThread(runnable.forget())); + } else { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable.forget())); + } + mRequestWasHandled = true; +} + +} // namespace + +void +FetchEvent::RespondWith(JSContext* aCx, Promise& aArg, ErrorResult& aRv) +{ + if (EventPhase() == nsIDOMEvent::NONE || mWaitToRespond) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + + // Record where respondWith() was called in the script so we can include the + // information in any error reporting. We should be guaranteed not to get + // a file:// string here because service workers require http/https. + nsCString spec; + uint32_t line = 0; + uint32_t column = 0; + nsJSUtils::GetCallingLocation(aCx, spec, &line, &column); + + RefPtr<InternalRequest> ir = mRequest->GetInternalRequest(); + + nsAutoCString requestURL; + ir->GetURL(requestURL); + + StopImmediatePropagation(); + mWaitToRespond = true; + RefPtr<RespondWithHandler> handler = + new RespondWithHandler(mChannel, mRegistration, mRequest->Mode(), + ir->IsClientRequest(), mRequest->Redirect(), + mScriptSpec, NS_ConvertUTF8toUTF16(requestURL), + spec, line, column); + aArg.AppendNativeHandler(handler); + + // Append directly to the lifecycle promises array. Don't call WaitUntil() + // because that will lead to double-reporting any errors. + mPromises.AppendElement(&aArg); +} + +void +FetchEvent::PreventDefault(JSContext* aCx) +{ + MOZ_ASSERT(aCx); + + if (mPreventDefaultScriptSpec.IsEmpty()) { + // Note when the FetchEvent might have been canceled by script, but don't + // actually log the location until we are sure it matters. This is + // determined in ServiceWorkerPrivate.cpp. We only remember the first + // call to preventDefault() as its the most likely to have actually canceled + // the event. + nsJSUtils::GetCallingLocation(aCx, mPreventDefaultScriptSpec, + &mPreventDefaultLineNumber, + &mPreventDefaultColumnNumber); + } + + Event::PreventDefault(aCx); +} + +void +FetchEvent::ReportCanceled() +{ + MOZ_ASSERT(!mPreventDefaultScriptSpec.IsEmpty()); + + RefPtr<InternalRequest> ir = mRequest->GetInternalRequest(); + nsAutoCString url; + ir->GetURL(url); + + // The variadic template provided by StringArrayAppender requires exactly + // an nsString. + NS_ConvertUTF8toUTF16 requestURL(url); + //nsString requestURL; + //CopyUTF8toUTF16(url, requestURL); + + ::AsyncLog(mChannel.get(), mPreventDefaultScriptSpec, + mPreventDefaultLineNumber, mPreventDefaultColumnNumber, + NS_LITERAL_CSTRING("InterceptionCanceledWithURL"), requestURL); +} + +namespace { + +class WaitUntilHandler final : public PromiseNativeHandler +{ + WorkerPrivate* mWorkerPrivate; + const nsCString mScope; + nsCString mSourceSpec; + uint32_t mLine; + uint32_t mColumn; + nsString mRejectValue; + + ~WaitUntilHandler() + { + } + +public: + NS_DECL_THREADSAFE_ISUPPORTS + + WaitUntilHandler(WorkerPrivate* aWorkerPrivate, JSContext* aCx) + : mWorkerPrivate(aWorkerPrivate) + , mScope(mWorkerPrivate->WorkerName()) + , mLine(0) + , mColumn(0) + { + mWorkerPrivate->AssertIsOnWorkerThread(); + + // Save the location of the waitUntil() call itself as a fallback + // in case the rejection value does not contain any location info. + nsJSUtils::GetCallingLocation(aCx, mSourceSpec, &mLine, &mColumn); + } + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + // do nothing, we are only here to report errors + } + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + mWorkerPrivate->AssertIsOnWorkerThread(); + + nsCString spec; + uint32_t line = 0; + uint32_t column = 0; + ExtractErrorValues(aCx, aValue, spec, &line, &column, mRejectValue); + + // only use the extracted location if we found one + if (!spec.IsEmpty()) { + mSourceSpec = spec; + mLine = line; + mColumn = column; + } + + MOZ_ALWAYS_SUCCEEDS(mWorkerPrivate->DispatchToMainThread( + NewRunnableMethod(this, &WaitUntilHandler::ReportOnMainThread))); + } + + void + ReportOnMainThread() + { + AssertIsOnMainThread(); + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown + return; + } + + // TODO: Make the error message a localized string. (bug 1222720) + nsString message; + message.AppendLiteral("Service worker event waitUntil() was passed a " + "promise that rejected with '"); + message.Append(mRejectValue); + message.AppendLiteral("'."); + + // Note, there is a corner case where this won't report to the window + // that triggered the error. Consider a navigation fetch event that + // rejects waitUntil() without holding respondWith() open. In this case + // there is no controlling document yet, the window did call .register() + // because there is no documeny yet, and the navigation is no longer + // being intercepted. + + swm->ReportToAllClients(mScope, message, NS_ConvertUTF8toUTF16(mSourceSpec), + EmptyString(), mLine, mColumn, + nsIScriptError::errorFlag); + } +}; + +NS_IMPL_ISUPPORTS0(WaitUntilHandler) + +} // anonymous namespace + +NS_IMPL_ADDREF_INHERITED(FetchEvent, ExtendableEvent) +NS_IMPL_RELEASE_INHERITED(FetchEvent, ExtendableEvent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(FetchEvent) +NS_INTERFACE_MAP_END_INHERITING(ExtendableEvent) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(FetchEvent, ExtendableEvent, mRequest) + +ExtendableEvent::ExtendableEvent(EventTarget* aOwner) + : Event(aOwner, nullptr, nullptr) +{ +} + +void +ExtendableEvent::WaitUntil(JSContext* aCx, Promise& aPromise, ErrorResult& aRv) +{ + MOZ_ASSERT(!NS_IsMainThread()); + + if (EventPhase() == nsIDOMEvent::NONE) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + // Append our handler to each waitUntil promise separately so we + // can record the location in script where waitUntil was called. + RefPtr<WaitUntilHandler> handler = + new WaitUntilHandler(GetCurrentThreadWorkerPrivate(), aCx); + aPromise.AppendNativeHandler(handler); + + mPromises.AppendElement(&aPromise); +} + +already_AddRefed<Promise> +ExtendableEvent::GetPromise() +{ + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + worker->AssertIsOnWorkerThread(); + + nsIGlobalObject* globalObj = worker->GlobalScope(); + + AutoJSAPI jsapi; + if (!jsapi.Init(globalObj)) { + return nullptr; + } + JSContext* cx = jsapi.cx(); + + GlobalObject global(cx, globalObj->GetGlobalJSObject()); + + ErrorResult result; + RefPtr<Promise> p = Promise::All(global, Move(mPromises), result); + if (NS_WARN_IF(result.MaybeSetPendingException(cx))) { + return nullptr; + } + + return p.forget(); +} + +NS_IMPL_ADDREF_INHERITED(ExtendableEvent, Event) +NS_IMPL_RELEASE_INHERITED(ExtendableEvent, Event) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(ExtendableEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ExtendableEvent, Event, mPromises) + +namespace { +nsresult +ExtractBytesFromUSVString(const nsAString& aStr, nsTArray<uint8_t>& aBytes) +{ + MOZ_ASSERT(aBytes.IsEmpty()); + nsCOMPtr<nsIUnicodeEncoder> encoder = EncodingUtils::EncoderForEncoding("UTF-8"); + if (NS_WARN_IF(!encoder)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + int32_t srcLen = aStr.Length(); + int32_t destBufferLen; + nsresult rv = encoder->GetMaxLength(aStr.BeginReading(), srcLen, &destBufferLen); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (NS_WARN_IF(!aBytes.SetLength(destBufferLen, fallible))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + char* destBuffer = reinterpret_cast<char*>(aBytes.Elements()); + int32_t outLen = destBufferLen; + rv = encoder->Convert(aStr.BeginReading(), &srcLen, destBuffer, &outLen); + if (NS_WARN_IF(NS_FAILED(rv))) { + aBytes.Clear(); + return rv; + } + + aBytes.TruncateLength(outLen); + + return NS_OK; +} + +nsresult +ExtractBytesFromData(const OwningArrayBufferViewOrArrayBufferOrUSVString& aDataInit, nsTArray<uint8_t>& aBytes) +{ + if (aDataInit.IsArrayBufferView()) { + const ArrayBufferView& view = aDataInit.GetAsArrayBufferView(); + if (NS_WARN_IF(!PushUtil::CopyArrayBufferViewToArray(view, aBytes))) { + return NS_ERROR_OUT_OF_MEMORY; + } + return NS_OK; + } + if (aDataInit.IsArrayBuffer()) { + const ArrayBuffer& buffer = aDataInit.GetAsArrayBuffer(); + if (NS_WARN_IF(!PushUtil::CopyArrayBufferToArray(buffer, aBytes))) { + return NS_ERROR_OUT_OF_MEMORY; + } + return NS_OK; + } + if (aDataInit.IsUSVString()) { + return ExtractBytesFromUSVString(aDataInit.GetAsUSVString(), aBytes); + } + NS_NOTREACHED("Unexpected push message data"); + return NS_ERROR_FAILURE; +} +} + +PushMessageData::PushMessageData(nsISupports* aOwner, + nsTArray<uint8_t>&& aBytes) + : mOwner(aOwner), mBytes(Move(aBytes)) {} + +PushMessageData::~PushMessageData() +{ +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PushMessageData, mOwner) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(PushMessageData) +NS_IMPL_CYCLE_COLLECTING_RELEASE(PushMessageData) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PushMessageData) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +JSObject* +PushMessageData::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return mozilla::dom::PushMessageDataBinding::Wrap(aCx, this, aGivenProto); +} + +void +PushMessageData::Json(JSContext* cx, JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aRv) +{ + if (NS_FAILED(EnsureDecodedText())) { + aRv.Throw(NS_ERROR_DOM_UNKNOWN_ERR); + return; + } + BodyUtil::ConsumeJson(cx, aRetval, mDecodedText, aRv); +} + +void +PushMessageData::Text(nsAString& aData) +{ + if (NS_SUCCEEDED(EnsureDecodedText())) { + aData = mDecodedText; + } +} + +void +PushMessageData::ArrayBuffer(JSContext* cx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) +{ + uint8_t* data = GetContentsCopy(); + if (data) { + BodyUtil::ConsumeArrayBuffer(cx, aRetval, mBytes.Length(), data, aRv); + } +} + +already_AddRefed<mozilla::dom::Blob> +PushMessageData::Blob(ErrorResult& aRv) +{ + uint8_t* data = GetContentsCopy(); + if (data) { + RefPtr<mozilla::dom::Blob> blob = BodyUtil::ConsumeBlob( + mOwner, EmptyString(), mBytes.Length(), data, aRv); + if (blob) { + return blob.forget(); + } + } + return nullptr; +} + +nsresult +PushMessageData::EnsureDecodedText() +{ + if (mBytes.IsEmpty() || !mDecodedText.IsEmpty()) { + return NS_OK; + } + nsresult rv = BodyUtil::ConsumeText( + mBytes.Length(), + reinterpret_cast<uint8_t*>(mBytes.Elements()), + mDecodedText + ); + if (NS_WARN_IF(NS_FAILED(rv))) { + mDecodedText.Truncate(); + return rv; + } + return NS_OK; +} + +uint8_t* +PushMessageData::GetContentsCopy() +{ + uint32_t length = mBytes.Length(); + void* data = malloc(length); + if (!data) { + return nullptr; + } + memcpy(data, mBytes.Elements(), length); + return reinterpret_cast<uint8_t*>(data); +} + +PushEvent::PushEvent(EventTarget* aOwner) + : ExtendableEvent(aOwner) +{ +} + +already_AddRefed<PushEvent> +PushEvent::Constructor(mozilla::dom::EventTarget* aOwner, + const nsAString& aType, + const PushEventInit& aOptions, + ErrorResult& aRv) +{ + RefPtr<PushEvent> e = new PushEvent(aOwner); + bool trusted = e->Init(aOwner); + e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable); + e->SetTrusted(trusted); + e->SetComposed(aOptions.mComposed); + if(aOptions.mData.WasPassed()){ + nsTArray<uint8_t> bytes; + nsresult rv = ExtractBytesFromData(aOptions.mData.Value(), bytes); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return nullptr; + } + e->mData = new PushMessageData(aOwner, Move(bytes)); + } + return e.forget(); +} + +NS_IMPL_ADDREF_INHERITED(PushEvent, ExtendableEvent) +NS_IMPL_RELEASE_INHERITED(PushEvent, ExtendableEvent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(PushEvent) +NS_INTERFACE_MAP_END_INHERITING(ExtendableEvent) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(PushEvent, ExtendableEvent, mData) + +JSObject* +PushEvent::WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return mozilla::dom::PushEventBinding::Wrap(aCx, this, aGivenProto); +} + +ExtendableMessageEvent::ExtendableMessageEvent(EventTarget* aOwner) + : ExtendableEvent(aOwner) + , mData(JS::UndefinedValue()) +{ + mozilla::HoldJSObjects(this); +} + +ExtendableMessageEvent::~ExtendableMessageEvent() +{ + mData.setUndefined(); + DropJSObjects(this); +} + +void +ExtendableMessageEvent::GetData(JSContext* aCx, + JS::MutableHandle<JS::Value> aData, + ErrorResult& aRv) +{ + aData.set(mData); + if (!JS_WrapValue(aCx, aData)) { + aRv.Throw(NS_ERROR_FAILURE); + } +} + +void +ExtendableMessageEvent::GetSource(Nullable<OwningClientOrServiceWorkerOrMessagePort>& aValue) const +{ + if (mClient) { + aValue.SetValue().SetAsClient() = mClient; + } else if (mServiceWorker) { + aValue.SetValue().SetAsServiceWorker() = mServiceWorker; + } else if (mMessagePort) { + aValue.SetValue().SetAsMessagePort() = mMessagePort; + } else { + MOZ_CRASH("Unexpected source value"); + } +} + +/* static */ already_AddRefed<ExtendableMessageEvent> +ExtendableMessageEvent::Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const ExtendableMessageEventInit& aOptions, + ErrorResult& aRv) +{ + nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports()); + return Constructor(t, aType, aOptions, aRv); +} + +/* static */ already_AddRefed<ExtendableMessageEvent> +ExtendableMessageEvent::Constructor(mozilla::dom::EventTarget* aEventTarget, + const nsAString& aType, + const ExtendableMessageEventInit& aOptions, + ErrorResult& aRv) +{ + RefPtr<ExtendableMessageEvent> event = new ExtendableMessageEvent(aEventTarget); + + event->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable); + bool trusted = event->Init(aEventTarget); + event->SetTrusted(trusted); + + event->mData = aOptions.mData; + event->mOrigin = aOptions.mOrigin; + event->mLastEventId = aOptions.mLastEventId; + + if (!aOptions.mSource.IsNull()) { + if (aOptions.mSource.Value().IsClient()) { + event->mClient = aOptions.mSource.Value().GetAsClient(); + } else if (aOptions.mSource.Value().IsServiceWorker()){ + event->mServiceWorker = aOptions.mSource.Value().GetAsServiceWorker(); + } else if (aOptions.mSource.Value().IsMessagePort()){ + event->mMessagePort = aOptions.mSource.Value().GetAsMessagePort(); + } + } + + event->mPorts.AppendElements(aOptions.mPorts); + return event.forget(); +} + +void +ExtendableMessageEvent::GetPorts(nsTArray<RefPtr<MessagePort>>& aPorts) +{ + aPorts = mPorts; +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(ExtendableMessageEvent) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ExtendableMessageEvent, Event) + tmp->mData.setUndefined(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mClient) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mServiceWorker) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mMessagePort) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPorts) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(ExtendableMessageEvent, Event) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mClient) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mServiceWorker) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMessagePort) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPorts) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(ExtendableMessageEvent, Event) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mData) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(ExtendableMessageEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +NS_IMPL_ADDREF_INHERITED(ExtendableMessageEvent, Event) +NS_IMPL_RELEASE_INHERITED(ExtendableMessageEvent, Event) + +END_WORKERS_NAMESPACE diff --git a/dom/workers/ServiceWorkerEvents.h b/dom/workers/ServiceWorkerEvents.h new file mode 100644 index 000000000..25702f8f3 --- /dev/null +++ b/dom/workers/ServiceWorkerEvents.h @@ -0,0 +1,316 @@ +/* -*- 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_workers_serviceworkerevents_h__ +#define mozilla_dom_workers_serviceworkerevents_h__ + +#include "mozilla/dom/Event.h" +#include "mozilla/dom/ExtendableEventBinding.h" +#include "mozilla/dom/ExtendableMessageEventBinding.h" +#include "mozilla/dom/FetchEventBinding.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/workers/bindings/ServiceWorker.h" +#include "mozilla/dom/workers/Workers.h" + +#include "nsProxyRelease.h" +#include "nsContentUtils.h" + +class nsIInterceptedChannel; + +namespace mozilla { +namespace dom { +class Blob; +class MessagePort; +class Request; +class ResponseOrPromise; + +struct PushEventInit; +} // namespace dom +} // namespace mozilla + +BEGIN_WORKERS_NAMESPACE + +class ServiceWorkerRegistrationInfo; + +class CancelChannelRunnable final : public Runnable +{ + nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel; + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration; + const nsresult mStatus; +public: + CancelChannelRunnable(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel, + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration, + nsresult aStatus); + + NS_IMETHOD Run() override; +}; + +class ExtendableEvent : public Event +{ +protected: + nsTArray<RefPtr<Promise>> mPromises; + + explicit ExtendableEvent(mozilla::dom::EventTarget* aOwner); + ~ExtendableEvent() {} + +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ExtendableEvent, Event) + NS_FORWARD_TO_EVENT + + virtual JSObject* WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override + { + return mozilla::dom::ExtendableEventBinding::Wrap(aCx, this, aGivenProto); + } + + static already_AddRefed<ExtendableEvent> + Constructor(mozilla::dom::EventTarget* aOwner, + const nsAString& aType, + const EventInit& aOptions) + { + RefPtr<ExtendableEvent> e = new ExtendableEvent(aOwner); + bool trusted = e->Init(aOwner); + e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable); + e->SetTrusted(trusted); + e->SetComposed(aOptions.mComposed); + return e.forget(); + } + + static already_AddRefed<ExtendableEvent> + Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const EventInit& aOptions, + ErrorResult& aRv) + { + nsCOMPtr<EventTarget> target = do_QueryInterface(aGlobal.GetAsSupports()); + return Constructor(target, aType, aOptions); + } + + void + WaitUntil(JSContext* aCx, Promise& aPromise, ErrorResult& aRv); + + already_AddRefed<Promise> + GetPromise(); + + virtual ExtendableEvent* AsExtendableEvent() override + { + return this; + } +}; + +class FetchEvent final : public ExtendableEvent +{ + nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel; + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration; + RefPtr<Request> mRequest; + nsCString mScriptSpec; + nsCString mPreventDefaultScriptSpec; + nsString mClientId; + uint32_t mPreventDefaultLineNumber; + uint32_t mPreventDefaultColumnNumber; + bool mIsReload; + bool mWaitToRespond; +protected: + explicit FetchEvent(EventTarget* aOwner); + ~FetchEvent(); + +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FetchEvent, ExtendableEvent) + + // Note, we cannot use NS_FORWARD_TO_EVENT because we want a different + // PreventDefault(JSContext*) override. + NS_FORWARD_NSIDOMEVENT(Event::) + + virtual JSObject* WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override + { + return FetchEventBinding::Wrap(aCx, this, aGivenProto); + } + + void PostInit(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel, + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration, + const nsACString& aScriptSpec); + + static already_AddRefed<FetchEvent> + Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const FetchEventInit& aOptions, + ErrorResult& aRv); + + bool + WaitToRespond() const + { + return mWaitToRespond; + } + + Request* + Request_() const + { + MOZ_ASSERT(mRequest); + return mRequest; + } + + void + GetClientId(nsAString& aClientId) const + { + aClientId = mClientId; + } + + bool + IsReload() const + { + return mIsReload; + } + + void + RespondWith(JSContext* aCx, Promise& aArg, ErrorResult& aRv); + + already_AddRefed<Promise> + ForwardTo(const nsAString& aUrl); + + already_AddRefed<Promise> + Default(); + + void + PreventDefault(JSContext* aCx) override; + + void + ReportCanceled(); +}; + +class PushMessageData final : public nsISupports, + public nsWrapperCache +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(PushMessageData) + + virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + nsISupports* GetParentObject() const { + return mOwner; + } + + void Json(JSContext* cx, JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aRv); + void Text(nsAString& aData); + void ArrayBuffer(JSContext* cx, JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv); + already_AddRefed<mozilla::dom::Blob> Blob(ErrorResult& aRv); + + PushMessageData(nsISupports* aOwner, nsTArray<uint8_t>&& aBytes); +private: + nsCOMPtr<nsISupports> mOwner; + nsTArray<uint8_t> mBytes; + nsString mDecodedText; + ~PushMessageData(); + + nsresult EnsureDecodedText(); + uint8_t* GetContentsCopy(); +}; + +class PushEvent final : public ExtendableEvent +{ + RefPtr<PushMessageData> mData; + +protected: + explicit PushEvent(mozilla::dom::EventTarget* aOwner); + ~PushEvent() {} + +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PushEvent, ExtendableEvent) + NS_FORWARD_TO_EVENT + + virtual JSObject* WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<PushEvent> + Constructor(mozilla::dom::EventTarget* aOwner, + const nsAString& aType, + const PushEventInit& aOptions, + ErrorResult& aRv); + + static already_AddRefed<PushEvent> + Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const PushEventInit& aOptions, + ErrorResult& aRv) + { + nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports()); + return Constructor(owner, aType, aOptions, aRv); + } + + PushMessageData* + GetData() const + { + return mData; + } +}; + +class ExtendableMessageEvent final : public ExtendableEvent +{ + JS::Heap<JS::Value> mData; + nsString mOrigin; + nsString mLastEventId; + RefPtr<ServiceWorkerClient> mClient; + RefPtr<ServiceWorker> mServiceWorker; + RefPtr<MessagePort> mMessagePort; + nsTArray<RefPtr<MessagePort>> mPorts; + +protected: + explicit ExtendableMessageEvent(EventTarget* aOwner); + ~ExtendableMessageEvent(); + +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(ExtendableMessageEvent, + ExtendableEvent) + + NS_FORWARD_TO_EVENT + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override + { + return mozilla::dom::ExtendableMessageEventBinding::Wrap(aCx, this, aGivenProto); + } + + static already_AddRefed<ExtendableMessageEvent> + Constructor(mozilla::dom::EventTarget* aOwner, + const nsAString& aType, + const ExtendableMessageEventInit& aOptions, + ErrorResult& aRv); + + static already_AddRefed<ExtendableMessageEvent> + Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const ExtendableMessageEventInit& aOptions, + ErrorResult& aRv); + + void GetData(JSContext* aCx, JS::MutableHandle<JS::Value> aData, + ErrorResult& aRv); + + void GetSource(Nullable<OwningClientOrServiceWorkerOrMessagePort>& aValue) const; + + NS_IMETHOD GetOrigin(nsAString& aOrigin) + { + aOrigin = mOrigin; + return NS_OK; + } + + NS_IMETHOD GetLastEventId(nsAString& aLastEventId) + { + aLastEventId = mLastEventId; + return NS_OK; + } + + void GetPorts(nsTArray<RefPtr<MessagePort>>& aPorts); +}; + +END_WORKERS_NAMESPACE + +#endif /* mozilla_dom_workers_serviceworkerevents_h__ */ diff --git a/dom/workers/ServiceWorkerInfo.cpp b/dom/workers/ServiceWorkerInfo.cpp new file mode 100644 index 000000000..fa08b97a6 --- /dev/null +++ b/dom/workers/ServiceWorkerInfo.cpp @@ -0,0 +1,229 @@ +/* -*- 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 "ServiceWorkerInfo.h" + +#include "ServiceWorkerScriptCache.h" + +BEGIN_WORKERS_NAMESPACE + +static_assert(nsIServiceWorkerInfo::STATE_INSTALLING == static_cast<uint16_t>(ServiceWorkerState::Installing), + "ServiceWorkerState enumeration value should match state values from nsIServiceWorkerInfo."); +static_assert(nsIServiceWorkerInfo::STATE_INSTALLED == static_cast<uint16_t>(ServiceWorkerState::Installed), + "ServiceWorkerState enumeration value should match state values from nsIServiceWorkerInfo."); +static_assert(nsIServiceWorkerInfo::STATE_ACTIVATING == static_cast<uint16_t>(ServiceWorkerState::Activating), + "ServiceWorkerState enumeration value should match state values from nsIServiceWorkerInfo."); +static_assert(nsIServiceWorkerInfo::STATE_ACTIVATED == static_cast<uint16_t>(ServiceWorkerState::Activated), + "ServiceWorkerState enumeration value should match state values from nsIServiceWorkerInfo."); +static_assert(nsIServiceWorkerInfo::STATE_REDUNDANT == static_cast<uint16_t>(ServiceWorkerState::Redundant), + "ServiceWorkerState enumeration value should match state values from nsIServiceWorkerInfo."); +static_assert(nsIServiceWorkerInfo::STATE_UNKNOWN == static_cast<uint16_t>(ServiceWorkerState::EndGuard_), + "ServiceWorkerState enumeration value should match state values from nsIServiceWorkerInfo."); + +NS_IMPL_ISUPPORTS(ServiceWorkerInfo, nsIServiceWorkerInfo) + +NS_IMETHODIMP +ServiceWorkerInfo::GetScriptSpec(nsAString& aScriptSpec) +{ + AssertIsOnMainThread(); + CopyUTF8toUTF16(mScriptSpec, aScriptSpec); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerInfo::GetCacheName(nsAString& aCacheName) +{ + AssertIsOnMainThread(); + aCacheName = mCacheName; + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerInfo::GetState(uint16_t* aState) +{ + MOZ_ASSERT(aState); + AssertIsOnMainThread(); + *aState = static_cast<uint16_t>(mState); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerInfo::GetDebugger(nsIWorkerDebugger** aResult) +{ + if (NS_WARN_IF(!aResult)) { + return NS_ERROR_FAILURE; + } + + return mServiceWorkerPrivate->GetDebugger(aResult); +} + +NS_IMETHODIMP +ServiceWorkerInfo::AttachDebugger() +{ + return mServiceWorkerPrivate->AttachDebugger(); +} + +NS_IMETHODIMP +ServiceWorkerInfo::DetachDebugger() +{ + return mServiceWorkerPrivate->DetachDebugger(); +} + +void +ServiceWorkerInfo::AppendWorker(ServiceWorker* aWorker) +{ + MOZ_ASSERT(aWorker); +#ifdef DEBUG + nsAutoString workerURL; + aWorker->GetScriptURL(workerURL); + MOZ_ASSERT(workerURL.Equals(NS_ConvertUTF8toUTF16(mScriptSpec))); +#endif + MOZ_ASSERT(!mInstances.Contains(aWorker)); + + mInstances.AppendElement(aWorker); + aWorker->SetState(State()); +} + +void +ServiceWorkerInfo::RemoveWorker(ServiceWorker* aWorker) +{ + MOZ_ASSERT(aWorker); +#ifdef DEBUG + nsAutoString workerURL; + aWorker->GetScriptURL(workerURL); + MOZ_ASSERT(workerURL.Equals(NS_ConvertUTF8toUTF16(mScriptSpec))); +#endif + MOZ_ASSERT(mInstances.Contains(aWorker)); + + mInstances.RemoveElement(aWorker); +} + +namespace { + +class ChangeStateUpdater final : public Runnable +{ +public: + ChangeStateUpdater(const nsTArray<ServiceWorker*>& aInstances, + ServiceWorkerState aState) + : mState(aState) + { + for (size_t i = 0; i < aInstances.Length(); ++i) { + mInstances.AppendElement(aInstances[i]); + } + } + + NS_IMETHOD Run() override + { + // We need to update the state of all instances atomically before notifying + // them to make sure that the observed state for all instances inside + // statechange event handlers is correct. + for (size_t i = 0; i < mInstances.Length(); ++i) { + mInstances[i]->SetState(mState); + } + for (size_t i = 0; i < mInstances.Length(); ++i) { + mInstances[i]->DispatchStateChange(mState); + } + + return NS_OK; + } + +private: + AutoTArray<RefPtr<ServiceWorker>, 1> mInstances; + ServiceWorkerState mState; +}; + +} + +void +ServiceWorkerInfo::UpdateState(ServiceWorkerState aState) +{ + AssertIsOnMainThread(); +#ifdef DEBUG + // Any state can directly transition to redundant, but everything else is + // ordered. + if (aState != ServiceWorkerState::Redundant) { + MOZ_ASSERT_IF(mState == ServiceWorkerState::EndGuard_, aState == ServiceWorkerState::Installing); + MOZ_ASSERT_IF(mState == ServiceWorkerState::Installing, aState == ServiceWorkerState::Installed); + MOZ_ASSERT_IF(mState == ServiceWorkerState::Installed, aState == ServiceWorkerState::Activating); + MOZ_ASSERT_IF(mState == ServiceWorkerState::Activating, aState == ServiceWorkerState::Activated); + } + // Activated can only go to redundant. + MOZ_ASSERT_IF(mState == ServiceWorkerState::Activated, aState == ServiceWorkerState::Redundant); +#endif + // Flush any pending functional events to the worker when it transitions to the + // activated state. + // TODO: Do we care that these events will race with the propagation of the + // state change? + if (aState == ServiceWorkerState::Activated && mState != aState) { + mServiceWorkerPrivate->Activated(); + } + mState = aState; + nsCOMPtr<nsIRunnable> r = new ChangeStateUpdater(mInstances, mState); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(r.forget())); + if (mState == ServiceWorkerState::Redundant) { + serviceWorkerScriptCache::PurgeCache(mPrincipal, mCacheName); + } +} + +ServiceWorkerInfo::ServiceWorkerInfo(nsIPrincipal* aPrincipal, + const nsACString& aScope, + const nsACString& aScriptSpec, + const nsAString& aCacheName) + : mPrincipal(aPrincipal) + , mScope(aScope) + , mScriptSpec(aScriptSpec) + , mCacheName(aCacheName) + , mState(ServiceWorkerState::EndGuard_) + , mServiceWorkerID(GetNextID()) + , mServiceWorkerPrivate(new ServiceWorkerPrivate(this)) + , mSkipWaitingFlag(false) +{ + MOZ_ASSERT(mPrincipal); + // cache origin attributes so we can use them off main thread + mOriginAttributes = BasePrincipal::Cast(mPrincipal)->OriginAttributesRef(); + MOZ_ASSERT(!mScope.IsEmpty()); + MOZ_ASSERT(!mScriptSpec.IsEmpty()); + MOZ_ASSERT(!mCacheName.IsEmpty()); +} + +ServiceWorkerInfo::~ServiceWorkerInfo() +{ + MOZ_ASSERT(mServiceWorkerPrivate); + mServiceWorkerPrivate->NoteDeadServiceWorkerInfo(); +} + +static uint64_t gServiceWorkerInfoCurrentID = 0; + +uint64_t +ServiceWorkerInfo::GetNextID() const +{ + return ++gServiceWorkerInfoCurrentID; +} + +already_AddRefed<ServiceWorker> +ServiceWorkerInfo::GetOrCreateInstance(nsPIDOMWindowInner* aWindow) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aWindow); + + RefPtr<ServiceWorker> ref; + + for (uint32_t i = 0; i < mInstances.Length(); ++i) { + MOZ_ASSERT(mInstances[i]); + if (mInstances[i]->GetOwner() == aWindow) { + ref = mInstances[i]; + break; + } + } + + if (!ref) { + ref = new ServiceWorker(aWindow, this); + } + + return ref.forget(); +} + +END_WORKERS_NAMESPACE diff --git a/dom/workers/ServiceWorkerInfo.h b/dom/workers/ServiceWorkerInfo.h new file mode 100644 index 000000000..80910bdad --- /dev/null +++ b/dom/workers/ServiceWorkerInfo.h @@ -0,0 +1,151 @@ +/* -*- 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_workers_serviceworkerinfo_h +#define mozilla_dom_workers_serviceworkerinfo_h + +#include "mozilla/dom/ServiceWorkerBinding.h" // For ServiceWorkerState +#include "nsIServiceWorkerManager.h" + +namespace mozilla { +namespace dom { +namespace workers { + +class ServiceWorker; +class ServiceWorkerPrivate; + +/* + * Wherever the spec treats a worker instance and a description of said worker + * as the same thing; i.e. "Resolve foo with + * _GetNewestWorker(serviceWorkerRegistration)", we represent the description + * by this class and spawn a ServiceWorker in the right global when required. + */ +class ServiceWorkerInfo final : public nsIServiceWorkerInfo +{ +private: + nsCOMPtr<nsIPrincipal> mPrincipal; + const nsCString mScope; + const nsCString mScriptSpec; + const nsString mCacheName; + ServiceWorkerState mState; + PrincipalOriginAttributes mOriginAttributes; + + // This id is shared with WorkerPrivate to match requests issued by service + // workers to their corresponding serviceWorkerInfo. + uint64_t mServiceWorkerID; + + // We hold rawptrs since the ServiceWorker constructor and destructor ensure + // addition and removal. + // There is a high chance of there being at least one ServiceWorker + // associated with this all the time. + AutoTArray<ServiceWorker*, 1> mInstances; + + RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate; + bool mSkipWaitingFlag; + + ~ServiceWorkerInfo(); + + // Generates a unique id for the service worker, with zero being treated as + // invalid. + uint64_t + GetNextID() const; + +public: + NS_DECL_ISUPPORTS + NS_DECL_NSISERVICEWORKERINFO + + class ServiceWorkerPrivate* + WorkerPrivate() const + { + MOZ_ASSERT(mServiceWorkerPrivate); + return mServiceWorkerPrivate; + } + + nsIPrincipal* + GetPrincipal() const + { + return mPrincipal; + } + + const nsCString& + ScriptSpec() const + { + return mScriptSpec; + } + + const nsCString& + Scope() const + { + return mScope; + } + + bool SkipWaitingFlag() const + { + AssertIsOnMainThread(); + return mSkipWaitingFlag; + } + + void SetSkipWaitingFlag() + { + AssertIsOnMainThread(); + mSkipWaitingFlag = true; + } + + ServiceWorkerInfo(nsIPrincipal* aPrincipal, + const nsACString& aScope, + const nsACString& aScriptSpec, + const nsAString& aCacheName); + + ServiceWorkerState + State() const + { + return mState; + } + + const PrincipalOriginAttributes& + GetOriginAttributes() const + { + return mOriginAttributes; + } + + const nsString& + CacheName() const + { + return mCacheName; + } + + uint64_t + ID() const + { + return mServiceWorkerID; + } + + void + UpdateState(ServiceWorkerState aState); + + // Only used to set initial state when loading from disk! + void + SetActivateStateUncheckedWithoutEvent(ServiceWorkerState aState) + { + AssertIsOnMainThread(); + mState = aState; + } + + void + AppendWorker(ServiceWorker* aWorker); + + void + RemoveWorker(ServiceWorker* aWorker); + + already_AddRefed<ServiceWorker> + GetOrCreateInstance(nsPIDOMWindowInner* aWindow); +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_serviceworkerinfo_h diff --git a/dom/workers/ServiceWorkerJob.cpp b/dom/workers/ServiceWorkerJob.cpp new file mode 100644 index 000000000..3d0a8e2cd --- /dev/null +++ b/dom/workers/ServiceWorkerJob.cpp @@ -0,0 +1,246 @@ +/* -*- 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 "ServiceWorkerJob.h" + +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" +#include "Workers.h" + +namespace mozilla { +namespace dom { +namespace workers { + +ServiceWorkerJob::Type +ServiceWorkerJob::GetType() const +{ + return mType; +} + +ServiceWorkerJob::State +ServiceWorkerJob::GetState() const +{ + return mState; +} + +bool +ServiceWorkerJob::Canceled() const +{ + return mCanceled; +} + +bool +ServiceWorkerJob::ResultCallbacksInvoked() const +{ + return mResultCallbacksInvoked; +} + +bool +ServiceWorkerJob::IsEquivalentTo(ServiceWorkerJob* aJob) const +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aJob); + return mType == aJob->mType && + mScope.Equals(aJob->mScope) && + mScriptSpec.Equals(aJob->mScriptSpec) && + mPrincipal->Equals(aJob->mPrincipal); +} + +void +ServiceWorkerJob::AppendResultCallback(Callback* aCallback) +{ + AssertIsOnMainThread(); + MOZ_DIAGNOSTIC_ASSERT(mState != State::Finished); + MOZ_DIAGNOSTIC_ASSERT(aCallback); + MOZ_DIAGNOSTIC_ASSERT(mFinalCallback != aCallback); + MOZ_ASSERT(!mResultCallbackList.Contains(aCallback)); + MOZ_DIAGNOSTIC_ASSERT(!mResultCallbacksInvoked); + mResultCallbackList.AppendElement(aCallback); +} + +void +ServiceWorkerJob::StealResultCallbacksFrom(ServiceWorkerJob* aJob) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aJob); + MOZ_ASSERT(aJob->mState == State::Initial); + + // Take the callbacks from the other job immediately to avoid the + // any possibility of them existing on both jobs at once. + nsTArray<RefPtr<Callback>> callbackList; + callbackList.SwapElements(aJob->mResultCallbackList); + + for (RefPtr<Callback>& callback : callbackList) { + // Use AppendResultCallback() so that assertion checking is performed on + // each callback. + AppendResultCallback(callback); + } +} + +void +ServiceWorkerJob::Start(Callback* aFinalCallback) +{ + AssertIsOnMainThread(); + MOZ_DIAGNOSTIC_ASSERT(!mCanceled); + + MOZ_DIAGNOSTIC_ASSERT(aFinalCallback); + MOZ_DIAGNOSTIC_ASSERT(!mFinalCallback); + MOZ_ASSERT(!mResultCallbackList.Contains(aFinalCallback)); + mFinalCallback = aFinalCallback; + + MOZ_DIAGNOSTIC_ASSERT(mState == State::Initial); + mState = State::Started; + + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod(this, &ServiceWorkerJob::AsyncExecute); + + // We may have to wait for the PBackground actor to be initialized + // before proceeding. We should always be able to get a ServiceWorkerManager, + // however, since Start() should not be called during shutdown. + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown + return; + } + if (!swm->HasBackgroundActor()) { + // waiting to initialize + swm->AppendPendingOperation(runnable); + return; + } + + // Otherwise start asynchronously. We should never run a job synchronously. + MOZ_ALWAYS_TRUE(NS_SUCCEEDED( + NS_DispatchToMainThread(runnable.forget()))); +} + +void +ServiceWorkerJob::Cancel() +{ + AssertIsOnMainThread(); + MOZ_ASSERT(!mCanceled); + mCanceled = true; +} + +ServiceWorkerJob::ServiceWorkerJob(Type aType, + nsIPrincipal* aPrincipal, + const nsACString& aScope, + const nsACString& aScriptSpec) + : mType(aType) + , mPrincipal(aPrincipal) + , mScope(aScope) + , mScriptSpec(aScriptSpec) + , mState(State::Initial) + , mCanceled(false) + , mResultCallbacksInvoked(false) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(mPrincipal); + MOZ_ASSERT(!mScope.IsEmpty()); + // Some job types may have an empty script spec +} + +ServiceWorkerJob::~ServiceWorkerJob() +{ + AssertIsOnMainThread(); + // Jobs must finish or never be started. Destroying an actively running + // job is an error. + MOZ_ASSERT(mState != State::Started); + MOZ_ASSERT_IF(mState == State::Finished, mResultCallbacksInvoked); +} + +void +ServiceWorkerJob::InvokeResultCallbacks(ErrorResult& aRv) +{ + AssertIsOnMainThread(); + MOZ_DIAGNOSTIC_ASSERT(mState == State::Started); + + MOZ_DIAGNOSTIC_ASSERT(!mResultCallbacksInvoked); + mResultCallbacksInvoked = true; + + nsTArray<RefPtr<Callback>> callbackList; + callbackList.SwapElements(mResultCallbackList); + + for (RefPtr<Callback>& callback : callbackList) { + // The callback might consume an exception on the ErrorResult, so we need + // to clone in order to maintain the error for the next callback. + ErrorResult rv; + aRv.CloneTo(rv); + + callback->JobFinished(this, rv); + + // The callback might not consume the error. + rv.SuppressException(); + } +} + +void +ServiceWorkerJob::InvokeResultCallbacks(nsresult aRv) +{ + ErrorResult converted(aRv); + InvokeResultCallbacks(converted); +} + +void +ServiceWorkerJob::Finish(ErrorResult& aRv) +{ + AssertIsOnMainThread(); + + // Avoid double-completion because it can result on operating on cleaned + // up data. This should not happen, though, so also assert to try to + // narrow down the causes. + MOZ_DIAGNOSTIC_ASSERT(mState == State::Started); + if (mState != State::Started) { + return; + } + + // Ensure that we only surface SecurityErr, TypeErr or InvalidStateErr to script. + if (aRv.Failed() && !aRv.ErrorCodeIs(NS_ERROR_DOM_SECURITY_ERR) && + !aRv.ErrorCodeIs(NS_ERROR_DOM_TYPE_ERR) && + !aRv.ErrorCodeIs(NS_ERROR_DOM_INVALID_STATE_ERR)) { + + // Remove the old error code so we can replace it with a TypeError. + aRv.SuppressException(); + + NS_ConvertUTF8toUTF16 scriptSpec(mScriptSpec); + NS_ConvertUTF8toUTF16 scope(mScope); + + // Throw the type error with a generic error message. + aRv.ThrowTypeError<MSG_SW_INSTALL_ERROR>(scriptSpec, scope); + } + + // The final callback may drop the last ref to this object. + RefPtr<ServiceWorkerJob> kungFuDeathGrip = this; + + if (!mResultCallbacksInvoked) { + InvokeResultCallbacks(aRv); + } + + mState = State::Finished; + + MOZ_DIAGNOSTIC_ASSERT(mFinalCallback); + if (mFinalCallback) { + mFinalCallback->JobFinished(this, aRv); + mFinalCallback = nullptr; + } + + // The callback might not consume the error. + aRv.SuppressException(); + + // Async release this object to ensure that our caller methods complete + // as well. + NS_ReleaseOnMainThread(kungFuDeathGrip.forget(), true /* always proxy */); +} + +void +ServiceWorkerJob::Finish(nsresult aRv) +{ + ErrorResult converted(aRv); + Finish(converted); +} + +} // namespace workers +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/ServiceWorkerJob.h b/dom/workers/ServiceWorkerJob.h new file mode 100644 index 000000000..56802ed97 --- /dev/null +++ b/dom/workers/ServiceWorkerJob.h @@ -0,0 +1,155 @@ +/* -*- 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_workers_serviceworkerjob_h +#define mozilla_dom_workers_serviceworkerjob_h + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsTArray.h" + +class nsIPrincipal; + +namespace mozilla { + +class ErrorResult; + +namespace dom { +namespace workers { + +class ServiceWorkerJob +{ +public: + // Implement this interface to receive notification when a job completes. + class Callback + { + public: + // Called once when the job completes. If the job is started, then this + // will be called. If a job is never executed due to browser shutdown, + // then this method will never be called. This method is always called + // on the main thread asynchronously after Start() completes. + virtual void JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) = 0; + + NS_IMETHOD_(MozExternalRefCountType) + AddRef(void) = 0; + + NS_IMETHOD_(MozExternalRefCountType) + Release(void) = 0; + }; + + enum class Type + { + Register, + Update, + Unregister + }; + + enum class State + { + Initial, + Started, + Finished + }; + + Type + GetType() const; + + State + GetState() const; + + // Determine if the job has been canceled. This does not change the + // current State, but indicates that the job should progress to Finished + // as soon as possible. + bool + Canceled() const; + + // Determine if the result callbacks have already been called. This is + // equivalent to the spec checked to see if the job promise has settled. + bool + ResultCallbacksInvoked() const; + + bool + IsEquivalentTo(ServiceWorkerJob* aJob) const; + + // Add a callback that will be invoked when the job's result is available. + // Some job types will invoke this before the job is actually finished. + // If an early callback does not occur, then it will be called automatically + // when Finish() is called. These callbacks will be invoked while the job + // state is Started. + void + AppendResultCallback(Callback* aCallback); + + // This takes ownership of any result callbacks associated with the given job + // and then appends them to this job's callback list. + void + StealResultCallbacksFrom(ServiceWorkerJob* aJob); + + // Start the job. All work will be performed asynchronously on + // the main thread. The Finish() method must be called exactly + // once after this point. A final callback must be provided. It + // will be invoked after all other callbacks have been processed. + void + Start(Callback* aFinalCallback); + + // Set an internal flag indicating that a started job should finish as + // soon as possible. + void + Cancel(); + +protected: + ServiceWorkerJob(Type aType, + nsIPrincipal* aPrincipal, + const nsACString& aScope, + const nsACString& aScriptSpec); + + virtual ~ServiceWorkerJob(); + + // Invoke the result callbacks immediately. The job must be in the + // Started state. The callbacks are cleared after being invoked, + // so subsequent method calls have no effect. + void + InvokeResultCallbacks(ErrorResult& aRv); + + // Convenience method that converts to ErrorResult and calls real method. + void + InvokeResultCallbacks(nsresult aRv); + + // Indicate that the job has completed. The must be called exactly + // once after Start() has initiated job execution. It may not be + // called until Start() has returned. + void + Finish(ErrorResult& aRv); + + // Convenience method that converts to ErrorResult and calls real method. + void + Finish(nsresult aRv); + + // Specific job types should define AsyncExecute to begin their work. + // All errors and successes must result in Finish() being called. + virtual void + AsyncExecute() = 0; + + const Type mType; + nsCOMPtr<nsIPrincipal> mPrincipal; + const nsCString mScope; + const nsCString mScriptSpec; + +private: + RefPtr<Callback> mFinalCallback; + nsTArray<RefPtr<Callback>> mResultCallbackList; + State mState; + bool mCanceled; + bool mResultCallbacksInvoked; + +public: + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerJob) +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_serviceworkerjob_h diff --git a/dom/workers/ServiceWorkerJobQueue.cpp b/dom/workers/ServiceWorkerJobQueue.cpp new file mode 100644 index 000000000..15a798a4d --- /dev/null +++ b/dom/workers/ServiceWorkerJobQueue.cpp @@ -0,0 +1,134 @@ +/* -*- 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 "ServiceWorkerJobQueue.h" + +#include "ServiceWorkerJob.h" +#include "Workers.h" + +namespace mozilla { +namespace dom { +namespace workers { + +class ServiceWorkerJobQueue::Callback final : public ServiceWorkerJob::Callback +{ + RefPtr<ServiceWorkerJobQueue> mQueue; + + ~Callback() + { + } + +public: + explicit Callback(ServiceWorkerJobQueue* aQueue) + : mQueue(aQueue) + { + AssertIsOnMainThread(); + MOZ_ASSERT(mQueue); + } + + virtual void + JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) override + { + AssertIsOnMainThread(); + mQueue->JobFinished(aJob); + } + + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerJobQueue::Callback, override) +}; + +ServiceWorkerJobQueue::~ServiceWorkerJobQueue() +{ + AssertIsOnMainThread(); + MOZ_ASSERT(mJobList.IsEmpty()); +} + +void +ServiceWorkerJobQueue::JobFinished(ServiceWorkerJob* aJob) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aJob); + + // XXX There are some corner cases where jobs can double-complete. Until + // we track all these down we do a non-fatal assert in debug builds and + // a runtime check to verify the queue is in the correct state. + NS_ASSERTION(!mJobList.IsEmpty(), + "Job queue should contain the job that just completed."); + NS_ASSERTION(mJobList.SafeElementAt(0, nullptr) == aJob, + "Job queue should contain the job that just completed."); + if (NS_WARN_IF(mJobList.SafeElementAt(0, nullptr) != aJob)) { + return; + } + + mJobList.RemoveElementAt(0); + + if (mJobList.IsEmpty()) { + return; + } + + RunJob(); +} + +void +ServiceWorkerJobQueue::RunJob() +{ + AssertIsOnMainThread(); + MOZ_ASSERT(!mJobList.IsEmpty()); + MOZ_ASSERT(mJobList[0]->GetState() == ServiceWorkerJob::State::Initial); + + RefPtr<Callback> callback = new Callback(this); + mJobList[0]->Start(callback); +} + +ServiceWorkerJobQueue::ServiceWorkerJobQueue() +{ + AssertIsOnMainThread(); +} + +void +ServiceWorkerJobQueue::ScheduleJob(ServiceWorkerJob* aJob) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aJob); + MOZ_ASSERT(!mJobList.Contains(aJob)); + + if (mJobList.IsEmpty()) { + mJobList.AppendElement(aJob); + RunJob(); + return; + } + + MOZ_ASSERT(mJobList[0]->GetState() == ServiceWorkerJob::State::Started); + + RefPtr<ServiceWorkerJob>& tailJob = mJobList[mJobList.Length() - 1]; + if (!tailJob->ResultCallbacksInvoked() && aJob->IsEquivalentTo(tailJob)) { + tailJob->StealResultCallbacksFrom(aJob); + return; + } + + mJobList.AppendElement(aJob); +} + +void +ServiceWorkerJobQueue::CancelAll() +{ + AssertIsOnMainThread(); + + for (RefPtr<ServiceWorkerJob>& job : mJobList) { + job->Cancel(); + } + + // Remove jobs that are queued but not started since they should never + // run after being canceled. This means throwing away all jobs except + // for the job at the front of the list. + if (!mJobList.IsEmpty()) { + MOZ_ASSERT(mJobList[0]->GetState() == ServiceWorkerJob::State::Started); + mJobList.TruncateLength(1); + } +} + +} // namespace workers +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/ServiceWorkerJobQueue.h b/dom/workers/ServiceWorkerJobQueue.h new file mode 100644 index 000000000..2af8682b3 --- /dev/null +++ b/dom/workers/ServiceWorkerJobQueue.h @@ -0,0 +1,49 @@ +/* -*- 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_workers_serviceworkerjobqueue_h +#define mozilla_dom_workers_serviceworkerjobqueue_h + +#include "mozilla/RefPtr.h" +#include "nsTArray.h" + +namespace mozilla { +namespace dom { +namespace workers { + +class ServiceWorkerJob; + +class ServiceWorkerJobQueue final +{ + class Callback; + + nsTArray<RefPtr<ServiceWorkerJob>> mJobList; + + ~ServiceWorkerJobQueue(); + + void + JobFinished(ServiceWorkerJob* aJob); + + void + RunJob(); + +public: + ServiceWorkerJobQueue(); + + void + ScheduleJob(ServiceWorkerJob* aJob); + + void + CancelAll(); + + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerJobQueue) +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_serviceworkerjobqueue_h diff --git a/dom/workers/ServiceWorkerManager.cpp b/dom/workers/ServiceWorkerManager.cpp new file mode 100644 index 000000000..a66df0731 --- /dev/null +++ b/dom/workers/ServiceWorkerManager.cpp @@ -0,0 +1,3969 @@ +/* -*- 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 "ServiceWorkerManager.h" + +#include "nsAutoPtr.h" +#include "nsIConsoleService.h" +#include "nsIDOMEventTarget.h" +#include "nsIDocument.h" +#include "nsIScriptSecurityManager.h" +#include "nsIStreamLoader.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsINetworkInterceptController.h" +#include "nsIMutableArray.h" +#include "nsIScriptError.h" +#include "nsISimpleEnumerator.h" +#include "nsITimer.h" +#include "nsIUploadChannel2.h" +#include "nsPIDOMWindow.h" +#include "nsScriptLoader.h" +#include "nsServiceManagerUtils.h" +#include "nsDebug.h" +#include "nsISupportsPrimitives.h" + +#include "jsapi.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/LoadContext.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/DOMError.h" +#include "mozilla/dom/ErrorEvent.h" +#include "mozilla/dom/Headers.h" +#include "mozilla/dom/InternalHeaders.h" +#include "mozilla/dom/Navigator.h" +#include "mozilla/dom/NotificationEvent.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/Request.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/Unused.h" +#include "mozilla/EnumSet.h" + +#include "nsContentPolicyUtils.h" +#include "nsContentSecurityManager.h" +#include "nsContentUtils.h" +#include "nsGlobalWindow.h" +#include "nsNetUtil.h" +#include "nsProxyRelease.h" +#include "nsQueryObject.h" +#include "nsTArray.h" + +#include "RuntimeService.h" +#include "ServiceWorker.h" +#include "ServiceWorkerClient.h" +#include "ServiceWorkerContainer.h" +#include "ServiceWorkerInfo.h" +#include "ServiceWorkerJobQueue.h" +#include "ServiceWorkerManagerChild.h" +#include "ServiceWorkerPrivate.h" +#include "ServiceWorkerRegisterJob.h" +#include "ServiceWorkerRegistrar.h" +#include "ServiceWorkerRegistration.h" +#include "ServiceWorkerScriptCache.h" +#include "ServiceWorkerEvents.h" +#include "ServiceWorkerUnregisterJob.h" +#include "ServiceWorkerUpdateJob.h" +#include "SharedWorker.h" +#include "WorkerInlines.h" +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" +#include "WorkerScope.h" + +#ifdef PostMessage +#undef PostMessage +#endif + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::ipc; + +BEGIN_WORKERS_NAMESPACE + +#define PURGE_DOMAIN_DATA "browser:purge-domain-data" +#define PURGE_SESSION_HISTORY "browser:purge-session-history" +#define CLEAR_ORIGIN_DATA "clear-origin-attributes-data" + +static_assert(nsIHttpChannelInternal::CORS_MODE_SAME_ORIGIN == static_cast<uint32_t>(RequestMode::Same_origin), + "RequestMode enumeration value should match Necko CORS mode value."); +static_assert(nsIHttpChannelInternal::CORS_MODE_NO_CORS == static_cast<uint32_t>(RequestMode::No_cors), + "RequestMode enumeration value should match Necko CORS mode value."); +static_assert(nsIHttpChannelInternal::CORS_MODE_CORS == static_cast<uint32_t>(RequestMode::Cors), + "RequestMode enumeration value should match Necko CORS mode value."); +static_assert(nsIHttpChannelInternal::CORS_MODE_NAVIGATE == static_cast<uint32_t>(RequestMode::Navigate), + "RequestMode enumeration value should match Necko CORS mode value."); + +static_assert(nsIHttpChannelInternal::REDIRECT_MODE_FOLLOW == static_cast<uint32_t>(RequestRedirect::Follow), + "RequestRedirect enumeration value should make Necko Redirect mode value."); +static_assert(nsIHttpChannelInternal::REDIRECT_MODE_ERROR == static_cast<uint32_t>(RequestRedirect::Error), + "RequestRedirect enumeration value should make Necko Redirect mode value."); +static_assert(nsIHttpChannelInternal::REDIRECT_MODE_MANUAL == static_cast<uint32_t>(RequestRedirect::Manual), + "RequestRedirect enumeration value should make Necko Redirect mode value."); +static_assert(3 == static_cast<uint32_t>(RequestRedirect::EndGuard_), + "RequestRedirect enumeration value should make Necko Redirect mode value."); + +static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_DEFAULT == static_cast<uint32_t>(RequestCache::Default), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_STORE == static_cast<uint32_t>(RequestCache::No_store), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_RELOAD == static_cast<uint32_t>(RequestCache::Reload), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_NO_CACHE == static_cast<uint32_t>(RequestCache::No_cache), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_FORCE_CACHE == static_cast<uint32_t>(RequestCache::Force_cache), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert(nsIHttpChannelInternal::FETCH_CACHE_MODE_ONLY_IF_CACHED == static_cast<uint32_t>(RequestCache::Only_if_cached), + "RequestCache enumeration value should match Necko Cache mode value."); +static_assert(6 == static_cast<uint32_t>(RequestCache::EndGuard_), + "RequestCache enumeration value should match Necko Cache mode value."); + +static StaticRefPtr<ServiceWorkerManager> gInstance; + +struct ServiceWorkerManager::RegistrationDataPerPrincipal final +{ + // Ordered list of scopes for glob matching. + // Each entry is an absolute URL representing the scope. + // Each value of the hash table is an array of an absolute URLs representing + // the scopes. + // + // An array is used for now since the number of controlled scopes per + // domain is expected to be relatively low. If that assumption was proved + // wrong this should be replaced with a better structure to avoid the + // memmoves associated with inserting stuff in the middle of the array. + nsTArray<nsCString> mOrderedScopes; + + // Scope to registration. + // The scope should be a fully qualified valid URL. + nsRefPtrHashtable<nsCStringHashKey, ServiceWorkerRegistrationInfo> mInfos; + + // Maps scopes to job queues. + nsRefPtrHashtable<nsCStringHashKey, ServiceWorkerJobQueue> mJobQueues; + + // Map scopes to scheduled update timers. + nsInterfaceHashtable<nsCStringHashKey, nsITimer> mUpdateTimers; +}; + +namespace { + +nsresult +PopulateRegistrationData(nsIPrincipal* aPrincipal, + const ServiceWorkerRegistrationInfo* aRegistration, + ServiceWorkerRegistrationData& aData) +{ + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aRegistration); + + if (NS_WARN_IF(!BasePrincipal::Cast(aPrincipal)->IsCodebasePrincipal())) { + return NS_ERROR_FAILURE; + } + + nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &aData.principal()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aData.scope() = aRegistration->mScope; + + RefPtr<ServiceWorkerInfo> newest = aRegistration->Newest(); + if (NS_WARN_IF(!newest)) { + return NS_ERROR_FAILURE; + } + + if (aRegistration->GetActive()) { + aData.currentWorkerURL() = aRegistration->GetActive()->ScriptSpec(); + aData.cacheName() = aRegistration->GetActive()->CacheName(); + } + + return NS_OK; +} + +class TeardownRunnable final : public Runnable +{ +public: + explicit TeardownRunnable(ServiceWorkerManagerChild* aActor) + : mActor(aActor) + { + MOZ_ASSERT(mActor); + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(mActor); + mActor->SendShutdown(); + return NS_OK; + } + +private: + ~TeardownRunnable() {} + + RefPtr<ServiceWorkerManagerChild> mActor; +}; + +} // namespace + +////////////////////////// +// ServiceWorkerManager // +////////////////////////// + +NS_IMPL_ADDREF(ServiceWorkerManager) +NS_IMPL_RELEASE(ServiceWorkerManager) + +NS_INTERFACE_MAP_BEGIN(ServiceWorkerManager) + NS_INTERFACE_MAP_ENTRY(nsIServiceWorkerManager) + NS_INTERFACE_MAP_ENTRY(nsIIPCBackgroundChildCreateCallback) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIServiceWorkerManager) +NS_INTERFACE_MAP_END + +ServiceWorkerManager::ServiceWorkerManager() + : mActor(nullptr) + , mShuttingDown(false) +{ +} + +ServiceWorkerManager::~ServiceWorkerManager() +{ + // The map will assert if it is not empty when destroyed. + mRegistrationInfos.Clear(); + MOZ_ASSERT(!mActor); +} + +void +ServiceWorkerManager::Init(ServiceWorkerRegistrar* aRegistrar) +{ + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + DebugOnly<nsresult> rv; + rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false /* ownsWeak */); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + if (XRE_IsParentProcess()) { + MOZ_DIAGNOSTIC_ASSERT(aRegistrar); + + nsTArray<ServiceWorkerRegistrationData> data; + aRegistrar->GetRegistrations(data); + LoadRegistrations(data); + + if (obs) { + DebugOnly<nsresult> rv; + rv = obs->AddObserver(this, PURGE_SESSION_HISTORY, false /* ownsWeak */); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = obs->AddObserver(this, PURGE_DOMAIN_DATA, false /* ownsWeak */); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = obs->AddObserver(this, CLEAR_ORIGIN_DATA, false /* ownsWeak */); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } + + if (!BackgroundChild::GetOrCreateForCurrentThread(this)) { + // Make sure to do this last as our failure cleanup expects Init() to have + // executed. + ActorFailed(); + } +} + +void +ServiceWorkerManager::MaybeStartShutdown() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (mShuttingDown) { + return; + } + + mShuttingDown = true; + + for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { + for (auto it2 = it1.UserData()->mUpdateTimers.Iter(); !it2.Done(); it2.Next()) { + nsCOMPtr<nsITimer> timer = it2.UserData(); + timer->Cancel(); + } + it1.UserData()->mUpdateTimers.Clear(); + + for (auto it2 = it1.UserData()->mJobQueues.Iter(); !it2.Done(); it2.Next()) { + RefPtr<ServiceWorkerJobQueue> queue = it2.UserData(); + queue->CancelAll(); + } + it1.UserData()->mJobQueues.Clear(); + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + + if (XRE_IsParentProcess()) { + obs->RemoveObserver(this, PURGE_SESSION_HISTORY); + obs->RemoveObserver(this, PURGE_DOMAIN_DATA); + obs->RemoveObserver(this, CLEAR_ORIGIN_DATA); + } + } + + mPendingOperations.Clear(); + + if (!mActor) { + return; + } + + mActor->ManagerShuttingDown(); + + RefPtr<TeardownRunnable> runnable = new TeardownRunnable(mActor); + nsresult rv = NS_DispatchToMainThread(runnable); + Unused << NS_WARN_IF(NS_FAILED(rv)); + mActor = nullptr; +} + +class ServiceWorkerResolveWindowPromiseOnRegisterCallback final : public ServiceWorkerJob::Callback +{ + RefPtr<nsPIDOMWindowInner> mWindow; + // The promise "returned" by the call to Update up to + // navigator.serviceWorker.register(). + RefPtr<Promise> mPromise; + + ~ServiceWorkerResolveWindowPromiseOnRegisterCallback() + {} + + virtual void + JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) override + { + AssertIsOnMainThread(); + MOZ_ASSERT(aJob); + + if (aStatus.Failed()) { + mPromise->MaybeReject(aStatus); + return; + } + + MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Register); + RefPtr<ServiceWorkerRegisterJob> registerJob = + static_cast<ServiceWorkerRegisterJob*>(aJob); + RefPtr<ServiceWorkerRegistrationInfo> reg = registerJob->GetRegistration(); + + RefPtr<ServiceWorkerRegistration> swr = + mWindow->GetServiceWorkerRegistration(NS_ConvertUTF8toUTF16(reg->mScope)); + mPromise->MaybeResolve(swr); + } + +public: + ServiceWorkerResolveWindowPromiseOnRegisterCallback(nsPIDOMWindowInner* aWindow, + Promise* aPromise) + : mWindow(aWindow) + , mPromise(aPromise) + {} + + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerResolveWindowPromiseOnRegisterCallback, override) +}; + +namespace { + +class PropagateSoftUpdateRunnable final : public Runnable +{ +public: + PropagateSoftUpdateRunnable(const PrincipalOriginAttributes& aOriginAttributes, + const nsAString& aScope) + : mOriginAttributes(aOriginAttributes) + , mScope(aScope) + {} + + NS_IMETHOD Run() override + { + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->PropagateSoftUpdate(mOriginAttributes, mScope); + } + + return NS_OK; + } + +private: + ~PropagateSoftUpdateRunnable() + {} + + const PrincipalOriginAttributes mOriginAttributes; + const nsString mScope; +}; + +class PropagateUnregisterRunnable final : public Runnable +{ +public: + PropagateUnregisterRunnable(nsIPrincipal* aPrincipal, + nsIServiceWorkerUnregisterCallback* aCallback, + const nsAString& aScope) + : mPrincipal(aPrincipal) + , mCallback(aCallback) + , mScope(aScope) + { + MOZ_ASSERT(aPrincipal); + } + + NS_IMETHOD Run() override + { + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->PropagateUnregister(mPrincipal, mCallback, mScope); + } + + return NS_OK; + } + +private: + ~PropagateUnregisterRunnable() + {} + + nsCOMPtr<nsIPrincipal> mPrincipal; + nsCOMPtr<nsIServiceWorkerUnregisterCallback> mCallback; + const nsString mScope; +}; + +class RemoveRunnable final : public Runnable +{ +public: + explicit RemoveRunnable(const nsACString& aHost) + {} + + NS_IMETHOD Run() override + { + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->Remove(mHost); + } + + return NS_OK; + } + +private: + ~RemoveRunnable() + {} + + const nsCString mHost; +}; + +class PropagateRemoveRunnable final : public Runnable +{ +public: + explicit PropagateRemoveRunnable(const nsACString& aHost) + {} + + NS_IMETHOD Run() override + { + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->PropagateRemove(mHost); + } + + return NS_OK; + } + +private: + ~PropagateRemoveRunnable() + {} + + const nsCString mHost; +}; + +class PropagateRemoveAllRunnable final : public Runnable +{ +public: + PropagateRemoveAllRunnable() + {} + + NS_IMETHOD Run() override + { + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->PropagateRemoveAll(); + } + + return NS_OK; + } + +private: + ~PropagateRemoveAllRunnable() + {} +}; + +} // namespace + +// This function implements parts of the step 3 of the following algorithm: +// https://w3c.github.io/webappsec/specs/powerfulfeatures/#settings-secure +static bool +IsFromAuthenticatedOrigin(nsIDocument* aDoc) +{ + MOZ_ASSERT(aDoc); + nsCOMPtr<nsIDocument> doc(aDoc); + nsCOMPtr<nsIContentSecurityManager> csm = do_GetService(NS_CONTENTSECURITYMANAGER_CONTRACTID); + if (NS_WARN_IF(!csm)) { + return false; + } + + while (doc && !nsContentUtils::IsChromeDoc(doc)) { + bool trustworthyOrigin = false; + + // The origin of the document may be different from the document URI + // itself. Check the principal, not the document URI itself. + nsCOMPtr<nsIPrincipal> documentPrincipal = doc->NodePrincipal(); + + // The check for IsChromeDoc() above should mean we never see a system + // principal inside the loop. + MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(documentPrincipal)); + + csm->IsOriginPotentiallyTrustworthy(documentPrincipal, &trustworthyOrigin); + if (!trustworthyOrigin) { + return false; + } + + doc = doc->GetParentDocument(); + } + return true; +} + +// If we return an error code here, the ServiceWorkerContainer will +// automatically reject the Promise. +NS_IMETHODIMP +ServiceWorkerManager::Register(mozIDOMWindow* aWindow, + nsIURI* aScopeURI, + nsIURI* aScriptURI, + nsISupports** aPromise) +{ + AssertIsOnMainThread(); + + if (NS_WARN_IF(!aWindow)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + auto* window = nsPIDOMWindowInner::From(aWindow); + nsCOMPtr<nsIDocument> doc = window->GetExtantDoc(); + if (!doc) { + return NS_ERROR_FAILURE; + } + + // Don't allow service workers to register when the *document* is chrome. + if (NS_WARN_IF(nsContentUtils::IsSystemPrincipal(doc->NodePrincipal()))) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + nsCOMPtr<nsPIDOMWindowOuter> outerWindow = window->GetOuterWindow(); + bool serviceWorkersTestingEnabled = + outerWindow->GetServiceWorkersTestingEnabled(); + + bool authenticatedOrigin; + if (Preferences::GetBool("dom.serviceWorkers.testing.enabled") || + serviceWorkersTestingEnabled) { + authenticatedOrigin = true; + } else { + authenticatedOrigin = IsFromAuthenticatedOrigin(doc); + } + + if (!authenticatedOrigin) { + NS_WARNING("ServiceWorker registration from insecure websites is not allowed."); + return NS_ERROR_DOM_SECURITY_ERR; + } + + // Data URLs are not allowed. + nsCOMPtr<nsIPrincipal> documentPrincipal = doc->NodePrincipal(); + + nsresult rv = documentPrincipal->CheckMayLoad(aScriptURI, true /* report */, + false /* allowIfInheritsPrincipal */); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + // Check content policy. + int16_t decision = nsIContentPolicy::ACCEPT; + rv = NS_CheckContentLoadPolicy(nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER, + aScriptURI, + documentPrincipal, + doc, + EmptyCString(), + nullptr, + &decision); + NS_ENSURE_SUCCESS(rv, rv); + if (NS_WARN_IF(decision != nsIContentPolicy::ACCEPT)) { + return NS_ERROR_CONTENT_BLOCKED; + } + + + rv = documentPrincipal->CheckMayLoad(aScopeURI, true /* report */, + false /* allowIfInheritsPrinciple */); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + // The IsOriginPotentiallyTrustworthy() check allows file:// and possibly other + // URI schemes. We need to explicitly only allows http and https schemes. + // Note, we just use the aScriptURI here for the check since its already + // been verified as same origin with the document principal. This also + // is a good block against accidentally allowing blob: script URIs which + // might inherit the origin. + bool isHttp = false; + bool isHttps = false; + aScriptURI->SchemeIs("http", &isHttp); + aScriptURI->SchemeIs("https", &isHttps); + if (NS_WARN_IF(!isHttp && !isHttps)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + nsCString cleanedScope; + rv = aScopeURI->GetSpecIgnoringRef(cleanedScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + + nsAutoCString spec; + rv = aScriptURI->GetSpecIgnoringRef(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIGlobalObject> sgo = do_QueryInterface(window); + ErrorResult result; + RefPtr<Promise> promise = Promise::Create(sgo, result); + if (result.Failed()) { + return result.StealNSResult(); + } + + nsAutoCString scopeKey; + rv = PrincipalToScopeKey(documentPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + AddRegisteringDocument(cleanedScope, doc); + + RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, + cleanedScope); + + RefPtr<ServiceWorkerResolveWindowPromiseOnRegisterCallback> cb = + new ServiceWorkerResolveWindowPromiseOnRegisterCallback(window, promise); + + nsCOMPtr<nsILoadGroup> docLoadGroup = doc->GetDocumentLoadGroup(); + RefPtr<WorkerLoadInfo::InterfaceRequestor> ir = + new WorkerLoadInfo::InterfaceRequestor(documentPrincipal, docLoadGroup); + ir->MaybeAddTabChild(docLoadGroup); + + // Create a load group that is separate from, yet related to, the document's load group. + // This allows checks for interfaces like nsILoadContext to yield the values used by the + // the document, yet will not cancel the update job if the document's load group is cancelled. + nsCOMPtr<nsILoadGroup> loadGroup = do_CreateInstance(NS_LOADGROUP_CONTRACTID); + MOZ_ALWAYS_SUCCEEDS(loadGroup->SetNotificationCallbacks(ir)); + + RefPtr<ServiceWorkerRegisterJob> job = + new ServiceWorkerRegisterJob(documentPrincipal, cleanedScope, spec, + loadGroup); + job->AppendResultCallback(cb); + queue->ScheduleJob(job); + + AssertIsOnMainThread(); + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_REGISTRATIONS, 1); + + promise.forget(aPromise); + return NS_OK; +} + +void +ServiceWorkerManager::AppendPendingOperation(nsIRunnable* aRunnable) +{ + MOZ_ASSERT(!mActor); + MOZ_ASSERT(aRunnable); + + if (!mShuttingDown) { + mPendingOperations.AppendElement(aRunnable); + } +} + +/* + * Implements the async aspects of the getRegistrations algorithm. + */ +class GetRegistrationsRunnable final : public Runnable +{ + nsCOMPtr<nsPIDOMWindowInner> mWindow; + RefPtr<Promise> mPromise; +public: + GetRegistrationsRunnable(nsPIDOMWindowInner* aWindow, Promise* aPromise) + : mWindow(aWindow), mPromise(aPromise) + {} + + NS_IMETHOD + Run() override + { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsIDocument* doc = mWindow->GetExtantDoc(); + if (!doc) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsCOMPtr<nsIURI> docURI = doc->GetDocumentURI(); + if (!docURI) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); + if (!principal) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsTArray<RefPtr<ServiceWorkerRegistration>> array; + + if (NS_WARN_IF(!BasePrincipal::Cast(principal)->IsCodebasePrincipal())) { + return NS_OK; + } + + nsAutoCString scopeKey; + nsresult rv = swm->PrincipalToScopeKey(principal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + ServiceWorkerManager::RegistrationDataPerPrincipal* data; + if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { + mPromise->MaybeResolve(array); + return NS_OK; + } + + for (uint32_t i = 0; i < data->mOrderedScopes.Length(); ++i) { + RefPtr<ServiceWorkerRegistrationInfo> info = + data->mInfos.GetWeak(data->mOrderedScopes[i]); + if (info->mPendingUninstall) { + continue; + } + + NS_ConvertUTF8toUTF16 scope(data->mOrderedScopes[i]); + + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope, nullptr, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPromise->MaybeReject(rv); + break; + } + + rv = principal->CheckMayLoad(scopeURI, true /* report */, + false /* allowIfInheritsPrincipal */); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + RefPtr<ServiceWorkerRegistration> swr = + mWindow->GetServiceWorkerRegistration(scope); + + array.AppendElement(swr); + } + + mPromise->MaybeResolve(array); + return NS_OK; + } +}; + +// If we return an error code here, the ServiceWorkerContainer will +// automatically reject the Promise. +NS_IMETHODIMP +ServiceWorkerManager::GetRegistrations(mozIDOMWindow* aWindow, + nsISupports** aPromise) +{ + AssertIsOnMainThread(); + + if (NS_WARN_IF(!aWindow)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + auto* window = nsPIDOMWindowInner::From(aWindow); + nsCOMPtr<nsIDocument> doc = window->GetExtantDoc(); + if (NS_WARN_IF(!doc)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + // Don't allow service workers to register when the *document* is chrome for + // now. + MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(doc->NodePrincipal())); + + nsCOMPtr<nsIGlobalObject> sgo = do_QueryInterface(window); + ErrorResult result; + RefPtr<Promise> promise = Promise::Create(sgo, result); + if (result.Failed()) { + return result.StealNSResult(); + } + + nsCOMPtr<nsIRunnable> runnable = + new GetRegistrationsRunnable(window, promise); + promise.forget(aPromise); + return NS_DispatchToCurrentThread(runnable); +} + +/* + * Implements the async aspects of the getRegistration algorithm. + */ +class GetRegistrationRunnable final : public Runnable +{ + nsCOMPtr<nsPIDOMWindowInner> mWindow; + RefPtr<Promise> mPromise; + nsString mDocumentURL; + +public: + GetRegistrationRunnable(nsPIDOMWindowInner* aWindow, Promise* aPromise, + const nsAString& aDocumentURL) + : mWindow(aWindow), mPromise(aPromise), mDocumentURL(aDocumentURL) + {} + + NS_IMETHOD + Run() override + { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsIDocument* doc = mWindow->GetExtantDoc(); + if (!doc) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsCOMPtr<nsIURI> docURI = doc->GetDocumentURI(); + if (!docURI) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), mDocumentURL, nullptr, docURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPromise->MaybeReject(rv); + return NS_OK; + } + + nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); + if (!principal) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + rv = principal->CheckMayLoad(uri, true /* report */, + false /* allowIfInheritsPrinciple */); + if (NS_FAILED(rv)) { + mPromise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); + return NS_OK; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + swm->GetServiceWorkerRegistrationInfo(principal, uri); + + if (!registration) { + mPromise->MaybeResolveWithUndefined(); + return NS_OK; + } + + NS_ConvertUTF8toUTF16 scope(registration->mScope); + RefPtr<ServiceWorkerRegistration> swr = + mWindow->GetServiceWorkerRegistration(scope); + mPromise->MaybeResolve(swr); + + return NS_OK; + } +}; + +// If we return an error code here, the ServiceWorkerContainer will +// automatically reject the Promise. +NS_IMETHODIMP +ServiceWorkerManager::GetRegistration(mozIDOMWindow* aWindow, + const nsAString& aDocumentURL, + nsISupports** aPromise) +{ + AssertIsOnMainThread(); + + if (NS_WARN_IF(!aWindow)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + auto* window = nsPIDOMWindowInner::From(aWindow); + nsCOMPtr<nsIDocument> doc = window->GetExtantDoc(); + if (NS_WARN_IF(!doc)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + // Don't allow service workers to register when the *document* is chrome for + // now. + MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(doc->NodePrincipal())); + + nsCOMPtr<nsIGlobalObject> sgo = do_QueryInterface(window); + ErrorResult result; + RefPtr<Promise> promise = Promise::Create(sgo, result); + if (result.Failed()) { + return result.StealNSResult(); + } + + nsCOMPtr<nsIRunnable> runnable = + new GetRegistrationRunnable(window, promise, aDocumentURL); + promise.forget(aPromise); + return NS_DispatchToCurrentThread(runnable); +} + +class GetReadyPromiseRunnable final : public Runnable +{ + nsCOMPtr<nsPIDOMWindowInner> mWindow; + RefPtr<Promise> mPromise; + +public: + GetReadyPromiseRunnable(nsPIDOMWindowInner* aWindow, Promise* aPromise) + : mWindow(aWindow), mPromise(aPromise) + {} + + NS_IMETHOD + Run() override + { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsIDocument* doc = mWindow->GetExtantDoc(); + if (!doc) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + nsCOMPtr<nsIURI> docURI = doc->GetDocumentURI(); + if (!docURI) { + mPromise->MaybeReject(NS_ERROR_UNEXPECTED); + return NS_OK; + } + + if (!swm->CheckReadyPromise(mWindow, docURI, mPromise)) { + swm->StorePendingReadyPromise(mWindow, docURI, mPromise); + } + + return NS_OK; + } +}; + +NS_IMETHODIMP +ServiceWorkerManager::SendPushEvent(const nsACString& aOriginAttributes, + const nsACString& aScope, + uint32_t aDataLength, + uint8_t* aDataBytes, + uint8_t optional_argc) +{ + if (optional_argc == 2) { + nsTArray<uint8_t> data; + if (!data.InsertElementsAt(0, aDataBytes, aDataLength, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + return SendPushEvent(aOriginAttributes, aScope, EmptyString(), Some(data)); + } + MOZ_ASSERT(optional_argc == 0); + return SendPushEvent(aOriginAttributes, aScope, EmptyString(), Nothing()); +} + +nsresult +ServiceWorkerManager::SendPushEvent(const nsACString& aOriginAttributes, + const nsACString& aScope, + const nsAString& aMessageId, + const Maybe<nsTArray<uint8_t>>& aData) +{ + PrincipalOriginAttributes attrs; + if (!attrs.PopulateFromSuffix(aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + ServiceWorkerInfo* serviceWorker = GetActiveWorkerInfoForScope(attrs, aScope); + if (NS_WARN_IF(!serviceWorker)) { + return NS_ERROR_FAILURE; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(serviceWorker->GetPrincipal(), aScope); + MOZ_DIAGNOSTIC_ASSERT(registration); + + return serviceWorker->WorkerPrivate()->SendPushEvent(aMessageId, aData, + registration); +} + +NS_IMETHODIMP +ServiceWorkerManager::SendPushSubscriptionChangeEvent(const nsACString& aOriginAttributes, + const nsACString& aScope) +{ + PrincipalOriginAttributes attrs; + if (!attrs.PopulateFromSuffix(aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(attrs, aScope); + if (!info) { + return NS_ERROR_FAILURE; + } + return info->WorkerPrivate()->SendPushSubscriptionChangeEvent(); +} + +nsresult +ServiceWorkerManager::SendNotificationEvent(const nsAString& aEventName, + const nsACString& aOriginSuffix, + const nsACString& aScope, + const nsAString& aID, + const nsAString& aTitle, + const nsAString& aDir, + const nsAString& aLang, + const nsAString& aBody, + const nsAString& aTag, + const nsAString& aIcon, + const nsAString& aData, + const nsAString& aBehavior) +{ + PrincipalOriginAttributes attrs; + if (!attrs.PopulateFromSuffix(aOriginSuffix)) { + return NS_ERROR_INVALID_ARG; + } + + ServiceWorkerInfo* info = GetActiveWorkerInfoForScope(attrs, aScope); + if (!info) { + return NS_ERROR_FAILURE; + } + + ServiceWorkerPrivate* workerPrivate = info->WorkerPrivate(); + return workerPrivate->SendNotificationEvent(aEventName, aID, aTitle, aDir, + aLang, aBody, aTag, + aIcon, aData, aBehavior, + NS_ConvertUTF8toUTF16(aScope)); +} + +NS_IMETHODIMP +ServiceWorkerManager::SendNotificationClickEvent(const nsACString& aOriginSuffix, + const nsACString& aScope, + const nsAString& aID, + const nsAString& aTitle, + const nsAString& aDir, + const nsAString& aLang, + const nsAString& aBody, + const nsAString& aTag, + const nsAString& aIcon, + const nsAString& aData, + const nsAString& aBehavior) +{ + return SendNotificationEvent(NS_LITERAL_STRING(NOTIFICATION_CLICK_EVENT_NAME), + aOriginSuffix, aScope, aID, aTitle, aDir, aLang, + aBody, aTag, aIcon, aData, aBehavior); +} + +NS_IMETHODIMP +ServiceWorkerManager::SendNotificationCloseEvent(const nsACString& aOriginSuffix, + const nsACString& aScope, + const nsAString& aID, + const nsAString& aTitle, + const nsAString& aDir, + const nsAString& aLang, + const nsAString& aBody, + const nsAString& aTag, + const nsAString& aIcon, + const nsAString& aData, + const nsAString& aBehavior) +{ + return SendNotificationEvent(NS_LITERAL_STRING(NOTIFICATION_CLOSE_EVENT_NAME), + aOriginSuffix, aScope, aID, aTitle, aDir, aLang, + aBody, aTag, aIcon, aData, aBehavior); +} + +NS_IMETHODIMP +ServiceWorkerManager::GetReadyPromise(mozIDOMWindow* aWindow, + nsISupports** aPromise) +{ + AssertIsOnMainThread(); + + if (NS_WARN_IF(!aWindow)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + auto* window = nsPIDOMWindowInner::From(aWindow); + + nsCOMPtr<nsIDocument> doc = window->GetExtantDoc(); + if (NS_WARN_IF(!doc)) { + return NS_ERROR_FAILURE; + } + + // Don't allow service workers to register when the *document* is chrome for + // now. + MOZ_ASSERT(!nsContentUtils::IsSystemPrincipal(doc->NodePrincipal())); + + MOZ_ASSERT(!mPendingReadyPromises.Contains(window)); + + nsCOMPtr<nsIGlobalObject> sgo = do_QueryInterface(window); + ErrorResult result; + RefPtr<Promise> promise = Promise::Create(sgo, result); + if (result.Failed()) { + return result.StealNSResult(); + } + + nsCOMPtr<nsIRunnable> runnable = + new GetReadyPromiseRunnable(window, promise); + promise.forget(aPromise); + return NS_DispatchToCurrentThread(runnable); +} + +NS_IMETHODIMP +ServiceWorkerManager::RemoveReadyPromise(mozIDOMWindow* aWindow) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aWindow); + + if (!aWindow) { + return NS_ERROR_FAILURE; + } + + mPendingReadyPromises.Remove(aWindow); + return NS_OK; +} + +void +ServiceWorkerManager::StorePendingReadyPromise(nsPIDOMWindowInner* aWindow, + nsIURI* aURI, + Promise* aPromise) +{ + PendingReadyPromise* data; + + // We should not have 2 pending promises for the same window. + MOZ_ASSERT(!mPendingReadyPromises.Get(aWindow, &data)); + + data = new PendingReadyPromise(aURI, aPromise); + mPendingReadyPromises.Put(aWindow, data); +} + +void +ServiceWorkerManager::CheckPendingReadyPromises() +{ + for (auto iter = mPendingReadyPromises.Iter(); !iter.Done(); iter.Next()) { + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(iter.Key()); + MOZ_ASSERT(window); + + nsAutoPtr<PendingReadyPromise>& pendingReadyPromise = iter.Data(); + if (CheckReadyPromise(window, pendingReadyPromise->mURI, + pendingReadyPromise->mPromise)) { + iter.Remove(); + } + } +} + +bool +ServiceWorkerManager::CheckReadyPromise(nsPIDOMWindowInner* aWindow, + nsIURI* aURI, Promise* aPromise) +{ + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aURI); + + nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc(); + MOZ_ASSERT(doc); + + nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal(); + MOZ_ASSERT(principal); + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetServiceWorkerRegistrationInfo(principal, aURI); + + if (registration && registration->GetActive()) { + NS_ConvertUTF8toUTF16 scope(registration->mScope); + RefPtr<ServiceWorkerRegistration> swr = + aWindow->GetServiceWorkerRegistration(scope); + aPromise->MaybeResolve(swr); + return true; + } + + return false; +} + +ServiceWorkerInfo* +ServiceWorkerManager::GetActiveWorkerInfoForScope(const PrincipalOriginAttributes& aOriginAttributes, + const nsACString& aScope) +{ + AssertIsOnMainThread(); + + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); + if (NS_FAILED(rv)) { + return nullptr; + } + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateCodebasePrincipal(scopeURI, aOriginAttributes); + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetServiceWorkerRegistrationInfo(principal, scopeURI); + if (!registration) { + return nullptr; + } + + return registration->GetActive(); +} + +ServiceWorkerInfo* +ServiceWorkerManager::GetActiveWorkerInfoForDocument(nsIDocument* aDocument) +{ + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerRegistrationInfo> registration; + GetDocumentRegistration(aDocument, getter_AddRefs(registration)); + + if (!registration) { + return nullptr; + } + + return registration->GetActive(); +} + +namespace { + +class UnregisterJobCallback final : public ServiceWorkerJob::Callback +{ + nsCOMPtr<nsIServiceWorkerUnregisterCallback> mCallback; + + ~UnregisterJobCallback() + { + } + +public: + explicit UnregisterJobCallback(nsIServiceWorkerUnregisterCallback* aCallback) + : mCallback(aCallback) + { + AssertIsOnMainThread(); + MOZ_ASSERT(mCallback); + } + + void + JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aJob); + + if (aStatus.Failed()) { + mCallback->UnregisterFailed(); + return; + } + + MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Unregister); + RefPtr<ServiceWorkerUnregisterJob> unregisterJob = + static_cast<ServiceWorkerUnregisterJob*>(aJob); + mCallback->UnregisterSucceeded(unregisterJob->GetResult()); + } + + NS_INLINE_DECL_REFCOUNTING(UnregisterJobCallback) +}; + +} // anonymous namespace + +NS_IMETHODIMP +ServiceWorkerManager::Unregister(nsIPrincipal* aPrincipal, + nsIServiceWorkerUnregisterCallback* aCallback, + const nsAString& aScope) +{ + AssertIsOnMainThread(); + + if (!aPrincipal) { + return NS_ERROR_FAILURE; + } + + nsresult rv; + +// This is not accessible by content, and callers should always ensure scope is +// a correct URI, so this is wrapped in DEBUG +#ifdef DEBUG + nsCOMPtr<nsIURI> scopeURI; + rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_SECURITY_ERR; + } +#endif + + nsAutoCString scopeKey; + rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + NS_ConvertUTF16toUTF8 scope(aScope); + RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, scope); + + RefPtr<ServiceWorkerUnregisterJob> job = + new ServiceWorkerUnregisterJob(aPrincipal, scope, true /* send to parent */); + + if (aCallback) { + RefPtr<UnregisterJobCallback> cb = new UnregisterJobCallback(aCallback); + job->AppendResultCallback(cb); + } + + queue->ScheduleJob(job); + return NS_OK; +} + +nsresult +ServiceWorkerManager::NotifyUnregister(nsIPrincipal* aPrincipal, + const nsAString& aScope) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + + nsresult rv; + +// This is not accessible by content, and callers should always ensure scope is +// a correct URI, so this is wrapped in DEBUG +#ifdef DEBUG + nsCOMPtr<nsIURI> scopeURI; + rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } +#endif + + nsAutoCString scopeKey; + rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + NS_ConvertUTF16toUTF8 scope(aScope); + RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, scope); + + RefPtr<ServiceWorkerUnregisterJob> job = + new ServiceWorkerUnregisterJob(aPrincipal, scope, + false /* send to parent */); + + queue->ScheduleJob(job); + return NS_OK; +} + +void +ServiceWorkerManager::WorkerIsIdle(ServiceWorkerInfo* aWorker) +{ + AssertIsOnMainThread(); + MOZ_DIAGNOSTIC_ASSERT(aWorker); + + RefPtr<ServiceWorkerRegistrationInfo> reg = + GetRegistration(aWorker->GetPrincipal(), aWorker->Scope()); + if (!reg) { + return; + } + + if (reg->GetActive() != aWorker) { + return; + } + + if (!reg->IsControllingDocuments() && reg->mPendingUninstall) { + RemoveRegistration(reg); + return; + } + + reg->TryToActivateAsync(); +} + +already_AddRefed<ServiceWorkerJobQueue> +ServiceWorkerManager::GetOrCreateJobQueue(const nsACString& aKey, + const nsACString& aScope) +{ + MOZ_ASSERT(!aKey.IsEmpty()); + ServiceWorkerManager::RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(aKey, &data)) { + data = new RegistrationDataPerPrincipal(); + mRegistrationInfos.Put(aKey, data); + } + + RefPtr<ServiceWorkerJobQueue> queue; + if (!data->mJobQueues.Get(aScope, getter_AddRefs(queue))) { + RefPtr<ServiceWorkerJobQueue> newQueue = new ServiceWorkerJobQueue(); + queue = newQueue; + data->mJobQueues.Put(aScope, newQueue.forget()); + } + + return queue.forget(); +} + +/* static */ +already_AddRefed<ServiceWorkerManager> +ServiceWorkerManager::GetInstance() +{ + // Note: We don't simply check gInstance for null-ness here, since otherwise + // this can resurrect the ServiceWorkerManager pretty late during shutdown. + static bool firstTime = true; + if (firstTime) { + RefPtr<ServiceWorkerRegistrar> swr; + + // Don't create the ServiceWorkerManager until the ServiceWorkerRegistrar is + // initialized. + if (XRE_IsParentProcess()) { + swr = ServiceWorkerRegistrar::Get(); + if (!swr) { + return nullptr; + } + } + + firstTime = false; + + AssertIsOnMainThread(); + + gInstance = new ServiceWorkerManager(); + gInstance->Init(swr); + ClearOnShutdown(&gInstance); + } + RefPtr<ServiceWorkerManager> copy = gInstance.get(); + return copy.forget(); +} + +void +ServiceWorkerManager::FinishFetch(ServiceWorkerRegistrationInfo* aRegistration) +{ +} + +void +ServiceWorkerManager::ReportToAllClients(const nsCString& aScope, + const nsString& aMessage, + const nsString& aFilename, + const nsString& aLine, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aFlags) +{ + nsCOMPtr<nsIURI> uri; + nsresult rv; + + if (!aFilename.IsEmpty()) { + rv = NS_NewURI(getter_AddRefs(uri), aFilename); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + + AutoTArray<uint64_t, 16> windows; + + // Report errors to every controlled document. + for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { + ServiceWorkerRegistrationInfo* reg = iter.UserData(); + MOZ_ASSERT(reg); + if (!reg->mScope.Equals(aScope)) { + continue; + } + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(iter.Key()); + if (!doc || !doc->IsCurrentActiveDocument() || !doc->GetWindow()) { + continue; + } + + windows.AppendElement(doc->InnerWindowID()); + + nsContentUtils::ReportToConsoleNonLocalized(aMessage, + aFlags, + NS_LITERAL_CSTRING("Service Workers"), + doc, + uri, + aLine, + aLineNumber, + aColumnNumber, + nsContentUtils::eOMIT_LOCATION); + } + + // Report to any documents that have called .register() for this scope. They + // may not be controlled, but will still want to see error reports. + WeakDocumentList* regList = mRegisteringDocuments.Get(aScope); + if (regList) { + for (int32_t i = regList->Length() - 1; i >= 0; --i) { + nsCOMPtr<nsIDocument> doc = do_QueryReferent(regList->ElementAt(i)); + if (!doc) { + regList->RemoveElementAt(i); + continue; + } + + if (!doc->IsCurrentActiveDocument()) { + continue; + } + + uint64_t innerWindowId = doc->InnerWindowID(); + if (windows.Contains(innerWindowId)) { + continue; + } + + windows.AppendElement(innerWindowId); + + nsContentUtils::ReportToConsoleNonLocalized(aMessage, + aFlags, + NS_LITERAL_CSTRING("Service Workers"), + doc, + uri, + aLine, + aLineNumber, + aColumnNumber, + nsContentUtils::eOMIT_LOCATION); + } + + if (regList->IsEmpty()) { + regList = nullptr; + nsAutoPtr<WeakDocumentList> doomed; + mRegisteringDocuments.RemoveAndForget(aScope, doomed); + } + } + + InterceptionList* intList = mNavigationInterceptions.Get(aScope); + if (intList) { + nsIConsoleService* consoleService = nullptr; + for (uint32_t i = 0; i < intList->Length(); ++i) { + nsCOMPtr<nsIInterceptedChannel> channel = intList->ElementAt(i); + + nsCOMPtr<nsIChannel> inner; + rv = channel->GetChannel(getter_AddRefs(inner)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + uint64_t innerWindowId = nsContentUtils::GetInnerWindowID(inner); + if (innerWindowId == 0 || windows.Contains(innerWindowId)) { + continue; + } + + windows.AppendElement(innerWindowId); + + // Unfortunately the nsContentUtils helpers don't provide a convenient + // way to log to a window ID without a document. Use console service + // directly. + nsCOMPtr<nsIScriptError> errorObject = + do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + rv = errorObject->InitWithWindowID(aMessage, + aFilename, + aLine, + aLineNumber, + aColumnNumber, + aFlags, + NS_LITERAL_CSTRING("Service Workers"), + innerWindowId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + if (!consoleService) { + rv = CallGetService(NS_CONSOLESERVICE_CONTRACTID, &consoleService); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + + consoleService->LogMessage(errorObject); + } + } + + // If there are no documents to report to, at least report something to the + // browser console. + if (windows.IsEmpty()) { + nsContentUtils::ReportToConsoleNonLocalized(aMessage, + aFlags, + NS_LITERAL_CSTRING("Service Workers"), + nullptr, // document + uri, + aLine, + aLineNumber, + aColumnNumber, + nsContentUtils::eOMIT_LOCATION); + return; + } +} + +/* static */ +void +ServiceWorkerManager::LocalizeAndReportToAllClients( + const nsCString& aScope, + const char* aStringKey, + const nsTArray<nsString>& aParamArray, + uint32_t aFlags, + const nsString& aFilename, + const nsString& aLine, + uint32_t aLineNumber, + uint32_t aColumnNumber) +{ + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + return; + } + + nsresult rv; + nsXPIDLString message; + rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES, + aStringKey, aParamArray, message); + if (NS_SUCCEEDED(rv)) { + swm->ReportToAllClients(aScope, message, + aFilename, aLine, aLineNumber, aColumnNumber, + aFlags); + } else { + NS_WARNING("Failed to format and therefore report localized error."); + } +} + +void +ServiceWorkerManager::FlushReportsToAllClients(const nsACString& aScope, + nsIConsoleReportCollector* aReporter) +{ + AutoTArray<uint64_t, 16> windows; + + // Report errors to every controlled document. + for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { + ServiceWorkerRegistrationInfo* reg = iter.UserData(); + MOZ_ASSERT(reg); + if (!reg->mScope.Equals(aScope)) { + continue; + } + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(iter.Key()); + if (!doc || !doc->IsCurrentActiveDocument() || !doc->GetWindow()) { + continue; + } + + windows.AppendElement(doc->InnerWindowID()); + + aReporter->FlushConsoleReports(doc, + nsIConsoleReportCollector::ReportAction::Save); + } + + // Report to any documents that have called .register() for this scope. They + // may not be controlled, but will still want to see error reports. + WeakDocumentList* regList = mRegisteringDocuments.Get(aScope); + if (regList) { + for (int32_t i = regList->Length() - 1; i >= 0; --i) { + nsCOMPtr<nsIDocument> doc = do_QueryReferent(regList->ElementAt(i)); + if (!doc) { + regList->RemoveElementAt(i); + continue; + } + + if (!doc->IsCurrentActiveDocument()) { + continue; + } + + uint64_t innerWindowId = doc->InnerWindowID(); + if (windows.Contains(innerWindowId)) { + continue; + } + + windows.AppendElement(innerWindowId); + + aReporter->FlushConsoleReports(doc, + nsIConsoleReportCollector::ReportAction::Save); + } + + if (regList->IsEmpty()) { + regList = nullptr; + nsAutoPtr<WeakDocumentList> doomed; + mRegisteringDocuments.RemoveAndForget(aScope, doomed); + } + } + + nsresult rv; + InterceptionList* intList = mNavigationInterceptions.Get(aScope); + if (intList) { + for (uint32_t i = 0; i < intList->Length(); ++i) { + nsCOMPtr<nsIInterceptedChannel> channel = intList->ElementAt(i); + + nsCOMPtr<nsIChannel> inner; + rv = channel->GetChannel(getter_AddRefs(inner)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + uint64_t innerWindowId = nsContentUtils::GetInnerWindowID(inner); + if (innerWindowId == 0 || windows.Contains(innerWindowId)) { + continue; + } + + windows.AppendElement(innerWindowId); + + aReporter->FlushReportsByWindowId(innerWindowId, + nsIConsoleReportCollector::ReportAction::Save); + } + } + + // If there are no documents to report to, at least report something to the + // browser console. + if (windows.IsEmpty()) { + aReporter->FlushConsoleReports((nsIDocument*)nullptr); + return; + } + + aReporter->ClearConsoleReports(); +} + +void +ServiceWorkerManager::HandleError(JSContext* aCx, + nsIPrincipal* aPrincipal, + const nsCString& aScope, + const nsString& aWorkerURL, + const nsString& aMessage, + const nsString& aFilename, + const nsString& aLine, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aFlags, + JSExnType aExnType) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + ServiceWorkerManager::RegistrationDataPerPrincipal* data; + if (NS_WARN_IF(!mRegistrationInfos.Get(scopeKey, &data))) { + return; + } + + // Always report any uncaught exceptions or errors to the console of + // each client. + ReportToAllClients(aScope, aMessage, aFilename, aLine, aLineNumber, + aColumnNumber, aFlags); +} + +void +ServiceWorkerManager::LoadRegistration( + const ServiceWorkerRegistrationData& aRegistration) +{ + AssertIsOnMainThread(); + + nsCOMPtr<nsIPrincipal> principal = + PrincipalInfoToPrincipal(aRegistration.principal()); + if (!principal) { + return; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(principal, aRegistration.scope()); + if (!registration) { + registration = CreateNewRegistration(aRegistration.scope(), principal); + } else { + // If active worker script matches our expectations for a "current worker", + // then we are done. + if (registration->GetActive() && + registration->GetActive()->ScriptSpec() == aRegistration.currentWorkerURL()) { + // No needs for updates. + return; + } + } + + const nsCString& currentWorkerURL = aRegistration.currentWorkerURL(); + if (!currentWorkerURL.IsEmpty()) { + registration->SetActive( + new ServiceWorkerInfo(registration->mPrincipal, registration->mScope, + currentWorkerURL, aRegistration.cacheName())); + registration->GetActive()->SetActivateStateUncheckedWithoutEvent(ServiceWorkerState::Activated); + } +} + +void +ServiceWorkerManager::LoadRegistrations( + const nsTArray<ServiceWorkerRegistrationData>& aRegistrations) +{ + AssertIsOnMainThread(); + + for (uint32_t i = 0, len = aRegistrations.Length(); i < len; ++i) { + LoadRegistration(aRegistrations[i]); + } +} + +void +ServiceWorkerManager::ActorFailed() +{ + MOZ_DIAGNOSTIC_ASSERT(!mActor); + MaybeStartShutdown(); +} + +void +ServiceWorkerManager::ActorCreated(mozilla::ipc::PBackgroundChild* aActor) +{ + MOZ_ASSERT(aActor); + MOZ_ASSERT(!mActor); + + if (mShuttingDown) { + MOZ_DIAGNOSTIC_ASSERT(mPendingOperations.IsEmpty()); + return; + } + + PServiceWorkerManagerChild* actor = + aActor->SendPServiceWorkerManagerConstructor(); + if (!actor) { + ActorFailed(); + return; + } + + mActor = static_cast<ServiceWorkerManagerChild*>(actor); + + // Flush the pending requests. + for (uint32_t i = 0, len = mPendingOperations.Length(); i < len; ++i) { + MOZ_ASSERT(mPendingOperations[i]); + nsresult rv = NS_DispatchToCurrentThread(mPendingOperations[i].forget()); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch a runnable."); + } + } + + mPendingOperations.Clear(); +} + +void +ServiceWorkerManager::StoreRegistration( + nsIPrincipal* aPrincipal, + ServiceWorkerRegistrationInfo* aRegistration) +{ + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aRegistration); + + if (mShuttingDown) { + return; + } + + MOZ_DIAGNOSTIC_ASSERT(mActor); + if (!mActor) { + return; + } + + ServiceWorkerRegistrationData data; + nsresult rv = PopulateRegistrationData(aPrincipal, aRegistration, data); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + PrincipalInfo principalInfo; + if (NS_WARN_IF(NS_FAILED(PrincipalToPrincipalInfo(aPrincipal, + &principalInfo)))) { + return; + } + + mActor->SendRegister(data); +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsPIDOMWindowInner* aWindow) +{ + MOZ_ASSERT(aWindow); + nsCOMPtr<nsIDocument> document = aWindow->GetExtantDoc(); + return GetServiceWorkerRegistrationInfo(document); +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsIDocument* aDoc) +{ + MOZ_ASSERT(aDoc); + nsCOMPtr<nsIURI> documentURI = aDoc->GetDocumentURI(); + nsCOMPtr<nsIPrincipal> principal = aDoc->NodePrincipal(); + return GetServiceWorkerRegistrationInfo(principal, documentURI); +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetServiceWorkerRegistrationInfo(nsIPrincipal* aPrincipal, + nsIURI* aURI) +{ + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aURI); + + //XXXnsm Temporary fix until Bug 1171432 is fixed. + if (NS_WARN_IF(BasePrincipal::Cast(aPrincipal)->AppId() == nsIScriptSecurityManager::UNKNOWN_APP_ID)) { + return nullptr; + } + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_FAILED(rv)) { + return nullptr; + } + + return GetServiceWorkerRegistrationInfo(scopeKey, aURI); +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetServiceWorkerRegistrationInfo(const nsACString& aScopeKey, + nsIURI* aURI) +{ + MOZ_ASSERT(aURI); + + nsAutoCString spec; + nsresult rv = aURI->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + nsAutoCString scope; + RegistrationDataPerPrincipal* data; + if (!FindScopeForPath(aScopeKey, spec, &data, scope)) { + return nullptr; + } + + MOZ_ASSERT(data); + + RefPtr<ServiceWorkerRegistrationInfo> registration; + data->mInfos.Get(scope, getter_AddRefs(registration)); + // ordered scopes and registrations better be in sync. + MOZ_ASSERT(registration); + +#ifdef DEBUG + nsAutoCString origin; + rv = registration->mPrincipal->GetOrigin(origin); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + MOZ_ASSERT(origin.Equals(aScopeKey)); +#endif + + if (registration->mPendingUninstall) { + return nullptr; + } + return registration.forget(); +} + +/* static */ nsresult +ServiceWorkerManager::PrincipalToScopeKey(nsIPrincipal* aPrincipal, + nsACString& aKey) +{ + MOZ_ASSERT(aPrincipal); + + if (!BasePrincipal::Cast(aPrincipal)->IsCodebasePrincipal()) { + return NS_ERROR_FAILURE; + } + + nsresult rv = aPrincipal->GetOrigin(aKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +/* static */ void +ServiceWorkerManager::AddScopeAndRegistration(const nsACString& aScope, + ServiceWorkerRegistrationInfo* aInfo) +{ + MOZ_ASSERT(aInfo); + MOZ_ASSERT(aInfo->mPrincipal); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown + return; + } + + nsAutoCString scopeKey; + nsresult rv = swm->PrincipalToScopeKey(aInfo->mPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + MOZ_ASSERT(!scopeKey.IsEmpty()); + + RegistrationDataPerPrincipal* data; + if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { + data = new RegistrationDataPerPrincipal(); + swm->mRegistrationInfos.Put(scopeKey, data); + } + + for (uint32_t i = 0; i < data->mOrderedScopes.Length(); ++i) { + const nsCString& current = data->mOrderedScopes[i]; + + // Perfect match! + if (aScope.Equals(current)) { + data->mInfos.Put(aScope, aInfo); + swm->NotifyListenersOnRegister(aInfo); + return; + } + + // Sort by length, with longest match first. + // /foo/bar should be before /foo/ + // Similarly /foo/b is between the two. + if (StringBeginsWith(aScope, current)) { + data->mOrderedScopes.InsertElementAt(i, aScope); + data->mInfos.Put(aScope, aInfo); + swm->NotifyListenersOnRegister(aInfo); + return; + } + } + + data->mOrderedScopes.AppendElement(aScope); + data->mInfos.Put(aScope, aInfo); + swm->NotifyListenersOnRegister(aInfo); +} + +/* static */ bool +ServiceWorkerManager::FindScopeForPath(const nsACString& aScopeKey, + const nsACString& aPath, + RegistrationDataPerPrincipal** aData, + nsACString& aMatch) +{ + MOZ_ASSERT(aData); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + + if (!swm || !swm->mRegistrationInfos.Get(aScopeKey, aData)) { + return false; + } + + for (uint32_t i = 0; i < (*aData)->mOrderedScopes.Length(); ++i) { + const nsCString& current = (*aData)->mOrderedScopes[i]; + if (StringBeginsWith(aPath, current)) { + aMatch = current; + return true; + } + } + + return false; +} + +/* static */ bool +ServiceWorkerManager::HasScope(nsIPrincipal* aPrincipal, + const nsACString& aScope) +{ + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + return false; + } + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + RegistrationDataPerPrincipal* data; + if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { + return false; + } + + return data->mOrderedScopes.Contains(aScope); +} + +/* static */ void +ServiceWorkerManager::RemoveScopeAndRegistration(ServiceWorkerRegistrationInfo* aRegistration) +{ + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + return; + } + + nsAutoCString scopeKey; + nsresult rv = swm->PrincipalToScopeKey(aRegistration->mPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RegistrationDataPerPrincipal* data; + if (!swm->mRegistrationInfos.Get(scopeKey, &data)) { + return; + } + + nsCOMPtr<nsITimer> timer = data->mUpdateTimers.Get(aRegistration->mScope); + if (timer) { + timer->Cancel(); + data->mUpdateTimers.Remove(aRegistration->mScope); + } + + // The registration should generally only be removed if there are no controlled + // documents, but mControlledDocuments can contain references to potentially + // controlled docs. This happens when the service worker is not active yet. + // We must purge these references since we are evicting the registration. + for (auto iter = swm->mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { + ServiceWorkerRegistrationInfo* reg = iter.UserData(); + MOZ_ASSERT(reg); + if (reg->mScope.Equals(aRegistration->mScope)) { + iter.Remove(); + } + } + + RefPtr<ServiceWorkerRegistrationInfo> info; + data->mInfos.Get(aRegistration->mScope, getter_AddRefs(info)); + + data->mInfos.Remove(aRegistration->mScope); + data->mOrderedScopes.RemoveElement(aRegistration->mScope); + swm->NotifyListenersOnUnregister(info); + + swm->MaybeRemoveRegistrationInfo(scopeKey); + swm->NotifyServiceWorkerRegistrationRemoved(aRegistration); +} + +void +ServiceWorkerManager::MaybeRemoveRegistrationInfo(const nsACString& aScopeKey) +{ + RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(aScopeKey, &data)) { + return; + } + + if (data->mOrderedScopes.IsEmpty() && data->mJobQueues.Count() == 0) { + mRegistrationInfos.Remove(aScopeKey); + } +} + +void +ServiceWorkerManager::MaybeStartControlling(nsIDocument* aDoc, + const nsAString& aDocumentId) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aDoc); + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetServiceWorkerRegistrationInfo(aDoc); + if (registration) { + MOZ_ASSERT(!mControlledDocuments.Contains(aDoc)); + StartControllingADocument(registration, aDoc, aDocumentId); + } +} + +void +ServiceWorkerManager::MaybeStopControlling(nsIDocument* aDoc) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aDoc); + RefPtr<ServiceWorkerRegistrationInfo> registration; + mControlledDocuments.Remove(aDoc, getter_AddRefs(registration)); + // A document which was uncontrolled does not maintain that state itself, so + // it will always call MaybeStopControlling() even if there isn't an + // associated registration. So this check is required. + if (registration) { + StopControllingADocument(registration); + } +} + +void +ServiceWorkerManager::MaybeCheckNavigationUpdate(nsIDocument* aDoc) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aDoc); + // We perform these success path navigation update steps when the + // document tells us its more or less done loading. This avoids + // slowing down page load and also lets pages consistently get + // updatefound events when they fire. + // + // 9.8.20 If respondWithEntered is false, then: + // 9.8.22 Else: (respondWith was entered and succeeded) + // If request is a non-subresource request, then: Invoke Soft Update + // algorithm. + RefPtr<ServiceWorkerRegistrationInfo> registration; + mControlledDocuments.Get(aDoc, getter_AddRefs(registration)); + if (registration) { + registration->MaybeScheduleUpdate(); + } +} + +void +ServiceWorkerManager::StartControllingADocument(ServiceWorkerRegistrationInfo* aRegistration, + nsIDocument* aDoc, + const nsAString& aDocumentId) +{ + MOZ_ASSERT(aRegistration); + MOZ_ASSERT(aDoc); + + aRegistration->StartControllingADocument(); + mControlledDocuments.Put(aDoc, aRegistration); + if (!aDocumentId.IsEmpty()) { + aDoc->SetId(aDocumentId); + } + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_CONTROLLED_DOCUMENTS, 1); +} + +void +ServiceWorkerManager::StopControllingADocument(ServiceWorkerRegistrationInfo* aRegistration) +{ + aRegistration->StopControllingADocument(); + if (aRegistration->IsControllingDocuments() || !aRegistration->IsIdle()) { + return; + } + + if (aRegistration->mPendingUninstall) { + RemoveRegistration(aRegistration); + return; + } + + // We use to aggressively terminate the worker at this point, but it + // caused problems. There are more uses for a service worker than actively + // controlled documents. We need to let the worker naturally terminate + // in case its handling push events, message events, etc. + aRegistration->TryToActivateAsync(); +} + +NS_IMETHODIMP +ServiceWorkerManager::GetScopeForUrl(nsIPrincipal* aPrincipal, + const nsAString& aUrl, nsAString& aScope) +{ + MOZ_ASSERT(aPrincipal); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aUrl, nullptr, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + + RefPtr<ServiceWorkerRegistrationInfo> r = + GetServiceWorkerRegistrationInfo(aPrincipal, uri); + if (!r) { + return NS_ERROR_FAILURE; + } + + aScope = NS_ConvertUTF8toUTF16(r->mScope); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::AddRegistrationEventListener(const nsAString& aScope, + ServiceWorkerRegistrationListener* aListener) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aListener); +#ifdef DEBUG + // Ensure a registration is only listening for it's own scope. + nsAutoString regScope; + aListener->GetScope(regScope); + MOZ_ASSERT(!regScope.IsEmpty()); + MOZ_ASSERT(aScope.Equals(regScope)); +#endif + + MOZ_ASSERT(!mServiceWorkerRegistrationListeners.Contains(aListener)); + mServiceWorkerRegistrationListeners.AppendElement(aListener); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::RemoveRegistrationEventListener(const nsAString& aScope, + ServiceWorkerRegistrationListener* aListener) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aListener); +#ifdef DEBUG + // Ensure a registration is unregistering for it's own scope. + nsAutoString regScope; + aListener->GetScope(regScope); + MOZ_ASSERT(!regScope.IsEmpty()); + MOZ_ASSERT(aScope.Equals(regScope)); +#endif + + MOZ_ASSERT(mServiceWorkerRegistrationListeners.Contains(aListener)); + mServiceWorkerRegistrationListeners.RemoveElement(aListener); + return NS_OK; +} + +void +ServiceWorkerManager::FireUpdateFoundOnServiceWorkerRegistrations( + ServiceWorkerRegistrationInfo* aRegistration) +{ + AssertIsOnMainThread(); + + nsTObserverArray<ServiceWorkerRegistrationListener*>::ForwardIterator it(mServiceWorkerRegistrationListeners); + while (it.HasMore()) { + RefPtr<ServiceWorkerRegistrationListener> target = it.GetNext(); + nsAutoString regScope; + target->GetScope(regScope); + MOZ_ASSERT(!regScope.IsEmpty()); + + NS_ConvertUTF16toUTF8 utf8Scope(regScope); + if (utf8Scope.Equals(aRegistration->mScope)) { + target->UpdateFound(); + } + } +} + +/* + * This is used for installing, waiting and active. + */ +nsresult +ServiceWorkerManager::GetServiceWorkerForScope(nsPIDOMWindowInner* aWindow, + const nsAString& aScope, + WhichServiceWorker aWhichWorker, + nsISupports** aServiceWorker) +{ + AssertIsOnMainThread(); + + if (NS_WARN_IF(!aWindow)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc(); + MOZ_ASSERT(doc); + + /////////////////////////////////////////// + // Security check + nsAutoCString scope = NS_ConvertUTF16toUTF8(aScope); + nsCOMPtr<nsIURI> scopeURI; + // We pass nullptr as the base URI since scopes obtained from + // ServiceWorkerRegistrations MUST be fully qualified URIs. + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), scope, nullptr, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + nsCOMPtr<nsIPrincipal> documentPrincipal = doc->NodePrincipal(); + rv = documentPrincipal->CheckMayLoad(scopeURI, true /* report */, + false /* allowIfInheritsPrinciple */); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_DOM_SECURITY_ERR; + } + //////////////////////////////////////////// + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(documentPrincipal, scope); + if (NS_WARN_IF(!registration)) { + return NS_ERROR_FAILURE; + } + + RefPtr<ServiceWorkerInfo> info; + if (aWhichWorker == WhichServiceWorker::INSTALLING_WORKER) { + info = registration->GetInstalling(); + } else if (aWhichWorker == WhichServiceWorker::WAITING_WORKER) { + info = registration->GetWaiting(); + } else if (aWhichWorker == WhichServiceWorker::ACTIVE_WORKER) { + info = registration->GetActive(); + } else { + MOZ_CRASH("Invalid worker type"); + } + + if (NS_WARN_IF(!info)) { + return NS_ERROR_DOM_NOT_FOUND_ERR; + } + + RefPtr<ServiceWorker> serviceWorker = info->GetOrCreateInstance(aWindow); + + serviceWorker->SetState(info->State()); + serviceWorker.forget(aServiceWorker); + return NS_OK; +} + +namespace { + +class ContinueDispatchFetchEventRunnable : public Runnable +{ + RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate; + nsCOMPtr<nsIInterceptedChannel> mChannel; + nsCOMPtr<nsILoadGroup> mLoadGroup; + nsString mDocumentId; + bool mIsReload; +public: + ContinueDispatchFetchEventRunnable(ServiceWorkerPrivate* aServiceWorkerPrivate, + nsIInterceptedChannel* aChannel, + nsILoadGroup* aLoadGroup, + const nsAString& aDocumentId, + bool aIsReload) + : mServiceWorkerPrivate(aServiceWorkerPrivate) + , mChannel(aChannel) + , mLoadGroup(aLoadGroup) + , mDocumentId(aDocumentId) + , mIsReload(aIsReload) + { + MOZ_ASSERT(aServiceWorkerPrivate); + MOZ_ASSERT(aChannel); + } + + void + HandleError() + { + AssertIsOnMainThread(); + NS_WARNING("Unexpected error while dispatching fetch event!"); + DebugOnly<nsresult> rv = mChannel->ResetInterception(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "Failed to resume intercepted network request"); + } + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + + nsCOMPtr<nsIChannel> channel; + nsresult rv = mChannel->GetChannel(getter_AddRefs(channel)); + if (NS_WARN_IF(NS_FAILED(rv))) { + HandleError(); + return NS_OK; + } + + // The channel might have encountered an unexpected error while ensuring + // the upload stream is cloneable. Check here and reset the interception + // if that happens. + nsresult status; + rv = channel->GetStatus(&status); + if (NS_WARN_IF(NS_FAILED(rv) || NS_FAILED(status))) { + HandleError(); + return NS_OK; + } + + rv = mServiceWorkerPrivate->SendFetchEvent(mChannel, mLoadGroup, + mDocumentId, mIsReload); + if (NS_WARN_IF(NS_FAILED(rv))) { + HandleError(); + } + + return NS_OK; + } +}; + +} // anonymous namespace + +void +ServiceWorkerManager::DispatchFetchEvent(const PrincipalOriginAttributes& aOriginAttributes, + nsIDocument* aDoc, + const nsAString& aDocumentIdForTopLevelNavigation, + nsIInterceptedChannel* aChannel, + bool aIsReload, + bool aIsSubresourceLoad, + ErrorResult& aRv) +{ + MOZ_ASSERT(aChannel); + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerInfo> serviceWorker; + nsCOMPtr<nsILoadGroup> loadGroup; + nsAutoString documentId; + + if (aIsSubresourceLoad) { + MOZ_ASSERT(aDoc); + + serviceWorker = GetActiveWorkerInfoForDocument(aDoc); + if (!serviceWorker) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + loadGroup = aDoc->GetDocumentLoadGroup(); + nsresult rv = aDoc->GetOrCreateId(documentId); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } else { + nsCOMPtr<nsIChannel> internalChannel; + aRv = aChannel->GetChannel(getter_AddRefs(internalChannel)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + internalChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + + // TODO: Use aDocumentIdForTopLevelNavigation for potentialClientId, pending + // the spec change. + + nsCOMPtr<nsIURI> uri; + aRv = aChannel->GetSecureUpgradedChannelURI(getter_AddRefs(uri)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + // non-subresource request means the URI contains the principal + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateCodebasePrincipal(uri, aOriginAttributes); + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetServiceWorkerRegistrationInfo(principal, uri); + if (!registration) { + NS_WARNING("No registration found when dispatching the fetch event"); + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + // While we only enter this method if IsAvailable() previously saw + // an active worker, it is possible for that worker to be removed + // before we get to this point. Therefore we must handle a nullptr + // active worker here. + serviceWorker = registration->GetActive(); + if (!serviceWorker) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + AddNavigationInterception(serviceWorker->Scope(), aChannel); + } + + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + MOZ_DIAGNOSTIC_ASSERT(serviceWorker); + + nsCOMPtr<nsIRunnable> continueRunnable = + new ContinueDispatchFetchEventRunnable(serviceWorker->WorkerPrivate(), + aChannel, loadGroup, + documentId, aIsReload); + + nsCOMPtr<nsIChannel> innerChannel; + aRv = aChannel->GetChannel(getter_AddRefs(innerChannel)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsCOMPtr<nsIUploadChannel2> uploadChannel = do_QueryInterface(innerChannel); + + // If there is no upload stream, then continue immediately + if (!uploadChannel) { + MOZ_ALWAYS_SUCCEEDS(continueRunnable->Run()); + return; + } + // Otherwise, ensure the upload stream can be cloned directly. This may + // require some async copying, so provide a callback. + aRv = uploadChannel->EnsureUploadStreamIsCloneable(continueRunnable); +} + +bool +ServiceWorkerManager::IsAvailable(nsIPrincipal* aPrincipal, + nsIURI* aURI) +{ + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aURI); + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetServiceWorkerRegistrationInfo(aPrincipal, aURI); + return registration && registration->GetActive(); +} + +bool +ServiceWorkerManager::IsControlled(nsIDocument* aDoc, ErrorResult& aRv) +{ + MOZ_ASSERT(aDoc); + + if (nsContentUtils::IsInPrivateBrowsing(aDoc)) { + // Handle the case where a service worker was previously registered in + // a non-private window (bug 1255621). + return false; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration; + nsresult rv = GetDocumentRegistration(aDoc, getter_AddRefs(registration)); + if (NS_WARN_IF(NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE)) { + // It's OK to ignore the case where we don't have a registration. + aRv.Throw(rv); + return false; + } + + return !!registration; +} + +nsresult +ServiceWorkerManager::GetDocumentRegistration(nsIDocument* aDoc, + ServiceWorkerRegistrationInfo** aRegistrationInfo) +{ + RefPtr<ServiceWorkerRegistrationInfo> registration; + if (!mControlledDocuments.Get(aDoc, getter_AddRefs(registration))) { + return NS_ERROR_NOT_AVAILABLE; + } + + // If the document is controlled, the current worker MUST be non-null. + if (!registration->GetActive()) { + return NS_ERROR_NOT_AVAILABLE; + } + + registration.forget(aRegistrationInfo); + return NS_OK; +} + +/* + * The .controller is for the registration associated with the document when + * the document was loaded. + */ +NS_IMETHODIMP +ServiceWorkerManager::GetDocumentController(nsPIDOMWindowInner* aWindow, + nsISupports** aServiceWorker) +{ + if (NS_WARN_IF(!aWindow)) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc(); + if (!doc) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration; + nsresult rv = GetDocumentRegistration(doc, getter_AddRefs(registration)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(registration->GetActive()); + RefPtr<ServiceWorker> serviceWorker = + registration->GetActive()->GetOrCreateInstance(aWindow); + + serviceWorker.forget(aServiceWorker); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::GetInstalling(nsPIDOMWindowInner* aWindow, + const nsAString& aScope, + nsISupports** aServiceWorker) +{ + return GetServiceWorkerForScope(aWindow, aScope, + WhichServiceWorker::INSTALLING_WORKER, + aServiceWorker); +} + +NS_IMETHODIMP +ServiceWorkerManager::GetWaiting(nsPIDOMWindowInner* aWindow, + const nsAString& aScope, + nsISupports** aServiceWorker) +{ + return GetServiceWorkerForScope(aWindow, aScope, + WhichServiceWorker::WAITING_WORKER, + aServiceWorker); +} + +NS_IMETHODIMP +ServiceWorkerManager::GetActive(nsPIDOMWindowInner* aWindow, + const nsAString& aScope, + nsISupports** aServiceWorker) +{ + return GetServiceWorkerForScope(aWindow, aScope, + WhichServiceWorker::ACTIVE_WORKER, + aServiceWorker); +} + +void +ServiceWorkerManager::InvalidateServiceWorkerRegistrationWorker(ServiceWorkerRegistrationInfo* aRegistration, + WhichServiceWorker aWhichOnes) +{ + AssertIsOnMainThread(); + nsTObserverArray<ServiceWorkerRegistrationListener*>::ForwardIterator it(mServiceWorkerRegistrationListeners); + while (it.HasMore()) { + RefPtr<ServiceWorkerRegistrationListener> target = it.GetNext(); + nsAutoString regScope; + target->GetScope(regScope); + MOZ_ASSERT(!regScope.IsEmpty()); + + NS_ConvertUTF16toUTF8 utf8Scope(regScope); + + if (utf8Scope.Equals(aRegistration->mScope)) { + target->InvalidateWorkers(aWhichOnes); + } + } +} + +void +ServiceWorkerManager::NotifyServiceWorkerRegistrationRemoved(ServiceWorkerRegistrationInfo* aRegistration) +{ + AssertIsOnMainThread(); + nsTObserverArray<ServiceWorkerRegistrationListener*>::ForwardIterator it(mServiceWorkerRegistrationListeners); + while (it.HasMore()) { + RefPtr<ServiceWorkerRegistrationListener> target = it.GetNext(); + nsAutoString regScope; + target->GetScope(regScope); + MOZ_ASSERT(!regScope.IsEmpty()); + + NS_ConvertUTF16toUTF8 utf8Scope(regScope); + + if (utf8Scope.Equals(aRegistration->mScope)) { + target->RegistrationRemoved(); + } + } +} + +void +ServiceWorkerManager::SoftUpdate(const PrincipalOriginAttributes& aOriginAttributes, + const nsACString& aScope) +{ + AssertIsOnMainThread(); + + if (mShuttingDown) { + return; + } + + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsCOMPtr<nsIPrincipal> principal = + BasePrincipal::CreateCodebasePrincipal(scopeURI, aOriginAttributes); + if (NS_WARN_IF(!principal)) { + return; + } + + nsAutoCString scopeKey; + rv = PrincipalToScopeKey(principal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(scopeKey, aScope); + if (NS_WARN_IF(!registration)) { + return; + } + + // "If registration's uninstalling flag is set, abort these steps." + if (registration->mPendingUninstall) { + return; + } + + // "If registration's installing worker is not null, abort these steps." + if (registration->GetInstalling()) { + return; + } + + // "Let newestWorker be the result of running Get Newest Worker algorithm + // passing registration as its argument. + // If newestWorker is null, abort these steps." + RefPtr<ServiceWorkerInfo> newest = registration->Newest(); + if (!newest) { + return; + } + + // "If the registration queue for registration is empty, invoke Update algorithm, + // or its equivalent, with client, registration as its argument." + // TODO(catalinb): We don't implement the force bypass cache flag. + // See: https://github.com/slightlyoff/ServiceWorker/issues/759 + RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, + aScope); + + RefPtr<ServiceWorkerUpdateJob> job = + new ServiceWorkerUpdateJob(principal, registration->mScope, + newest->ScriptSpec(), nullptr); + queue->ScheduleJob(job); +} + +namespace { + +class UpdateJobCallback final : public ServiceWorkerJob::Callback +{ + RefPtr<ServiceWorkerUpdateFinishCallback> mCallback; + + ~UpdateJobCallback() + { + } + +public: + explicit UpdateJobCallback(ServiceWorkerUpdateFinishCallback* aCallback) + : mCallback(aCallback) + { + AssertIsOnMainThread(); + MOZ_ASSERT(mCallback); + } + + void + JobFinished(ServiceWorkerJob* aJob, ErrorResult& aStatus) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aJob); + + if (aStatus.Failed()) { + mCallback->UpdateFailed(aStatus); + return; + } + + MOZ_ASSERT(aJob->GetType() == ServiceWorkerJob::Type::Update); + RefPtr<ServiceWorkerUpdateJob> updateJob = + static_cast<ServiceWorkerUpdateJob*>(aJob); + RefPtr<ServiceWorkerRegistrationInfo> reg = updateJob->GetRegistration(); + mCallback->UpdateSucceeded(reg); + } + + NS_INLINE_DECL_REFCOUNTING(UpdateJobCallback) +}; +} // anonymous namespace + +void +ServiceWorkerManager::Update(nsIPrincipal* aPrincipal, + const nsACString& aScope, + ServiceWorkerUpdateFinishCallback* aCallback) +{ + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aCallback); + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(scopeKey, aScope); + if (NS_WARN_IF(!registration)) { + return; + } + + // "Let newestWorker be the result of running Get Newest Worker algorithm + // passing registration as its argument. + // If newestWorker is null, return a promise rejected with "InvalidStateError" + RefPtr<ServiceWorkerInfo> newest = registration->Newest(); + if (!newest) { + ErrorResult error(NS_ERROR_DOM_INVALID_STATE_ERR); + aCallback->UpdateFailed(error); + + // In case the callback does not consume the exception + error.SuppressException(); + + return; + } + + RefPtr<ServiceWorkerJobQueue> queue = GetOrCreateJobQueue(scopeKey, aScope); + + // "Invoke Update algorithm, or its equivalent, with client, registration as + // its argument." + RefPtr<ServiceWorkerUpdateJob> job = + new ServiceWorkerUpdateJob(aPrincipal, registration->mScope, + newest->ScriptSpec(), nullptr); + + RefPtr<UpdateJobCallback> cb = new UpdateJobCallback(aCallback); + job->AppendResultCallback(cb); + + queue->ScheduleJob(job); +} + +namespace { + +static void +FireControllerChangeOnDocument(nsIDocument* aDocument) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aDocument); + + nsCOMPtr<nsPIDOMWindowInner> w = aDocument->GetInnerWindow(); + if (!w) { + NS_WARNING("Failed to dispatch controllerchange event"); + return; + } + + auto* window = nsGlobalWindow::Cast(w.get()); + ErrorResult result; + dom::Navigator* navigator = window->GetNavigator(result); + if (NS_WARN_IF(result.Failed())) { + result.SuppressException(); + return; + } + + RefPtr<ServiceWorkerContainer> container = navigator->ServiceWorker(); + container->ControllerChanged(result); + if (result.Failed()) { + NS_WARNING("Failed to dispatch controllerchange event"); + } +} + +} // anonymous namespace + +UniquePtr<ServiceWorkerClientInfo> +ServiceWorkerManager::GetClient(nsIPrincipal* aPrincipal, + const nsAString& aClientId, + ErrorResult& aRv) +{ + UniquePtr<ServiceWorkerClientInfo> clientInfo; + nsCOMPtr<nsISupportsInterfacePointer> ifptr = + do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID); + if (NS_WARN_IF(!ifptr)) { + return clientInfo; + } + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return clientInfo; + } + + nsresult rv = obs->NotifyObservers(ifptr, "service-worker-get-client", + PromiseFlatString(aClientId).get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return clientInfo; + } + + nsCOMPtr<nsISupports> ptr; + ifptr->GetData(getter_AddRefs(ptr)); + nsCOMPtr<nsIDocument> doc = do_QueryInterface(ptr); + if (NS_WARN_IF(!doc)) { + return clientInfo; + } + + bool equals = false; + aPrincipal->Equals(doc->NodePrincipal(), &equals); + if (!equals) { + return clientInfo; + } + + if (!IsFromAuthenticatedOrigin(doc)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return clientInfo; + } + + clientInfo.reset(new ServiceWorkerClientInfo(doc)); + return clientInfo; +} + +void +ServiceWorkerManager::GetAllClients(nsIPrincipal* aPrincipal, + const nsCString& aScope, + bool aIncludeUncontrolled, + nsTArray<ServiceWorkerClientInfo>& aDocuments) +{ + MOZ_ASSERT(aPrincipal); + + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(aPrincipal, aScope); + + if (!registration) { + // The registration was removed, leave the array empty. + return; + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return; + } + + nsCOMPtr<nsISimpleEnumerator> enumerator; + nsresult rv = obs->EnumerateObservers("service-worker-get-client", + getter_AddRefs(enumerator)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + auto ProcessDocument = [&aDocuments](nsIPrincipal* aPrincipal, nsIDocument* aDoc) { + if (!aDoc || !aDoc->GetWindow()) { + return; + } + + bool equals = false; + aPrincipal->Equals(aDoc->NodePrincipal(), &equals); + if (!equals) { + return; + } + + // Treat http windows with devtools opened as secure if the correct devtools + // setting is enabled. + if (!aDoc->GetWindow()->GetServiceWorkersTestingEnabled() && + !Preferences::GetBool("dom.serviceWorkers.testing.enabled") && + !IsFromAuthenticatedOrigin(aDoc)) { + return; + } + + ServiceWorkerClientInfo clientInfo(aDoc); + aDocuments.AppendElement(aDoc); + }; + + // Since it's not simple to check whether a document is in + // mControlledDocuments, we take different code paths depending on whether we + // need to look at all documents. The common parts of the two loops are + // factored out into the ProcessDocument lambda. + if (aIncludeUncontrolled) { + bool loop = true; + while (NS_SUCCEEDED(enumerator->HasMoreElements(&loop)) && loop) { + nsCOMPtr<nsISupports> ptr; + rv = enumerator->GetNext(getter_AddRefs(ptr)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(ptr); + ProcessDocument(aPrincipal, doc); + } + } else { + for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { + ServiceWorkerRegistrationInfo* thisRegistration = iter.UserData(); + MOZ_ASSERT(thisRegistration); + if (!registration->mScope.Equals(thisRegistration->mScope)) { + continue; + } + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(iter.Key()); + + // All controlled documents must have an outer window. + MOZ_ASSERT(doc->GetWindow()); + + ProcessDocument(aPrincipal, doc); + } + } +} + +void +ServiceWorkerManager::MaybeClaimClient(nsIDocument* aDocument, + ServiceWorkerRegistrationInfo* aWorkerRegistration) +{ + MOZ_ASSERT(aWorkerRegistration); + MOZ_ASSERT(aWorkerRegistration->GetActive()); + + // Same origin check + if (!aWorkerRegistration->mPrincipal->Equals(aDocument->NodePrincipal())) { + return; + } + + // The registration that should be controlling the client + RefPtr<ServiceWorkerRegistrationInfo> matchingRegistration = + GetServiceWorkerRegistrationInfo(aDocument); + + // The registration currently controlling the client + RefPtr<ServiceWorkerRegistrationInfo> controllingRegistration; + GetDocumentRegistration(aDocument, getter_AddRefs(controllingRegistration)); + + if (aWorkerRegistration != matchingRegistration || + aWorkerRegistration == controllingRegistration) { + return; + } + + if (controllingRegistration) { + StopControllingADocument(controllingRegistration); + } + + StartControllingADocument(aWorkerRegistration, aDocument, NS_LITERAL_STRING("")); + FireControllerChangeOnDocument(aDocument); +} + +nsresult +ServiceWorkerManager::ClaimClients(nsIPrincipal* aPrincipal, + const nsCString& aScope, uint64_t aId) +{ + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(aPrincipal, aScope); + + if (!registration || !registration->GetActive() || + !(registration->GetActive()->ID() == aId)) { + // The worker is not active. + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!obs)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsISimpleEnumerator> enumerator; + nsresult rv = obs->EnumerateObservers("service-worker-get-client", + getter_AddRefs(enumerator)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool loop = true; + while (NS_SUCCEEDED(enumerator->HasMoreElements(&loop)) && loop) { + nsCOMPtr<nsISupports> ptr; + rv = enumerator->GetNext(getter_AddRefs(ptr)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(ptr); + MaybeClaimClient(doc, registration); + } + + return NS_OK; +} + +void +ServiceWorkerManager::SetSkipWaitingFlag(nsIPrincipal* aPrincipal, + const nsCString& aScope, + uint64_t aServiceWorkerID) +{ + RefPtr<ServiceWorkerRegistrationInfo> registration = + GetRegistration(aPrincipal, aScope); + if (NS_WARN_IF(!registration)) { + return; + } + + RefPtr<ServiceWorkerInfo> worker = + registration->GetServiceWorkerInfoById(aServiceWorkerID); + + if (NS_WARN_IF(!worker)) { + return; + } + + worker->SetSkipWaitingFlag(); + + if (worker->State() == ServiceWorkerState::Installed) { + registration->TryToActivateAsync(); + } +} + +void +ServiceWorkerManager::FireControllerChange(ServiceWorkerRegistrationInfo* aRegistration) +{ + AssertIsOnMainThread(); + for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { + if (iter.UserData() != aRegistration) { + continue; + } + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(iter.Key()); + if (NS_WARN_IF(!doc)) { + continue; + } + + FireControllerChangeOnDocument(doc); + } +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetRegistration(nsIPrincipal* aPrincipal, + const nsACString& aScope) const +{ + MOZ_ASSERT(aPrincipal); + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return GetRegistration(scopeKey, aScope); +} + +NS_IMETHODIMP +ServiceWorkerManager::GetRegistrationByPrincipal(nsIPrincipal* aPrincipal, + const nsAString& aScope, + nsIServiceWorkerRegistrationInfo** aInfo) +{ + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aInfo); + + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + RefPtr<ServiceWorkerRegistrationInfo> info = + GetServiceWorkerRegistrationInfo(aPrincipal, scopeURI); + if (!info) { + return NS_ERROR_FAILURE; + } + info.forget(aInfo); + + return NS_OK; +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::GetRegistration(const nsACString& aScopeKey, + const nsACString& aScope) const +{ + RefPtr<ServiceWorkerRegistrationInfo> reg; + + RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(aScopeKey, &data)) { + return reg.forget(); + } + + data->mInfos.Get(aScope, getter_AddRefs(reg)); + return reg.forget(); +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerManager::CreateNewRegistration(const nsCString& aScope, + nsIPrincipal* aPrincipal) +{ +#ifdef DEBUG + AssertIsOnMainThread(); + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), aScope, nullptr, nullptr); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + RefPtr<ServiceWorkerRegistrationInfo> tmp = + GetRegistration(aPrincipal, aScope); + MOZ_ASSERT(!tmp); +#endif + + RefPtr<ServiceWorkerRegistrationInfo> registration = + new ServiceWorkerRegistrationInfo(aScope, aPrincipal); + // From now on ownership of registration is with + // mServiceWorkerRegistrationInfos. + AddScopeAndRegistration(aScope, registration); + return registration.forget(); +} + +void +ServiceWorkerManager::MaybeRemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration) +{ + MOZ_ASSERT(aRegistration); + RefPtr<ServiceWorkerInfo> newest = aRegistration->Newest(); + if (!newest && HasScope(aRegistration->mPrincipal, aRegistration->mScope)) { + RemoveRegistration(aRegistration); + } +} + +void +ServiceWorkerManager::RemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration) +{ + // Note, we do not need to call mActor->SendUnregister() here. There are a few + // ways we can get here: + // 1) Through a normal unregister which calls SendUnregister() in the unregister + // job Start() method. + // 2) Through origin storage being purged. These result in ForceUnregister() + // starting unregister jobs which in turn call SendUnregister(). + // 3) Through the failure to install a new service worker. Since we don't store + // the registration until install succeeds, we do not need to call + // SendUnregister here. + // Assert these conditions by testing for pending uninstall (cases 1 and 2) or + // null workers (case 3). +#ifdef DEBUG + RefPtr<ServiceWorkerInfo> newest = aRegistration->Newest(); + MOZ_ASSERT(aRegistration->mPendingUninstall || !newest); +#endif + + MOZ_ASSERT(HasScope(aRegistration->mPrincipal, aRegistration->mScope)); + + // When a registration is removed, we must clear its contents since the DOM + // object may be held by content script. + aRegistration->Clear(); + + RemoveScopeAndRegistration(aRegistration); +} + +namespace { +/** + * See toolkit/modules/sessionstore/Utils.jsm function hasRootDomain(). + * + * Returns true if the |url| passed in is part of the given root |domain|. + * For example, if |url| is "www.mozilla.org", and we pass in |domain| as + * "mozilla.org", this will return true. It would return false the other way + * around. + */ +bool +HasRootDomain(nsIURI* aURI, const nsACString& aDomain) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aURI); + + nsAutoCString host; + nsresult rv = aURI->GetHost(host); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + nsACString::const_iterator start, end; + host.BeginReading(start); + host.EndReading(end); + if (!FindInReadable(aDomain, start, end)) { + return false; + } + + if (host.Equals(aDomain)) { + return true; + } + + // Beginning of the string matches, can't look at the previous char. + if (start.get() == host.BeginReading()) { + // Equals failed so this is fine. + return false; + } + + char prevChar = *(--start); + return prevChar == '.'; +} + +} // namespace + +NS_IMETHODIMP +ServiceWorkerManager::GetAllRegistrations(nsIArray** aResult) +{ + AssertIsOnMainThread(); + + nsCOMPtr<nsIMutableArray> array(do_CreateInstance(NS_ARRAY_CONTRACTID)); + if (!array) { + return NS_ERROR_OUT_OF_MEMORY; + } + + for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { + for (auto it2 = it1.UserData()->mInfos.Iter(); !it2.Done(); it2.Next()) { + ServiceWorkerRegistrationInfo* reg = it2.UserData(); + MOZ_ASSERT(reg); + + if (reg->mPendingUninstall) { + continue; + } + + array->AppendElement(reg, false); + } + } + + array.forget(aResult); + return NS_OK; +} + +// MUST ONLY BE CALLED FROM Remove(), RemoveAll() and RemoveAllRegistrations()! +void +ServiceWorkerManager::ForceUnregister(RegistrationDataPerPrincipal* aRegistrationData, + ServiceWorkerRegistrationInfo* aRegistration) +{ + MOZ_ASSERT(aRegistrationData); + MOZ_ASSERT(aRegistration); + + RefPtr<ServiceWorkerJobQueue> queue; + aRegistrationData->mJobQueues.Get(aRegistration->mScope, getter_AddRefs(queue)); + if (queue) { + queue->CancelAll(); + } + + nsCOMPtr<nsITimer> timer = + aRegistrationData->mUpdateTimers.Get(aRegistration->mScope); + if (timer) { + timer->Cancel(); + aRegistrationData->mUpdateTimers.Remove(aRegistration->mScope); + } + + // Since Unregister is async, it is ok to call it in an enumeration. + Unregister(aRegistration->mPrincipal, nullptr, NS_ConvertUTF8toUTF16(aRegistration->mScope)); +} + +NS_IMETHODIMP +ServiceWorkerManager::RemoveAndPropagate(const nsACString& aHost) +{ + Remove(aHost); + PropagateRemove(aHost); + return NS_OK; +} + +void +ServiceWorkerManager::Remove(const nsACString& aHost) +{ + AssertIsOnMainThread(); + + // We need to postpone this operation in case we don't have an actor because + // this is needed by the ForceUnregister. + if (!mActor) { + RefPtr<nsIRunnable> runnable = new RemoveRunnable(aHost); + AppendPendingOperation(runnable); + return; + } + + for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { + ServiceWorkerManager::RegistrationDataPerPrincipal* data = it1.UserData(); + for (auto it2 = data->mInfos.Iter(); !it2.Done(); it2.Next()) { + ServiceWorkerRegistrationInfo* reg = it2.UserData(); + nsCOMPtr<nsIURI> scopeURI; + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), it2.Key(), + nullptr, nullptr); + // This way subdomains are also cleared. + if (NS_SUCCEEDED(rv) && HasRootDomain(scopeURI, aHost)) { + ForceUnregister(data, reg); + } + } + } +} + +void +ServiceWorkerManager::PropagateRemove(const nsACString& aHost) +{ + AssertIsOnMainThread(); + + if (!mActor) { + RefPtr<nsIRunnable> runnable = new PropagateRemoveRunnable(aHost); + AppendPendingOperation(runnable); + return; + } + + mActor->SendPropagateRemove(nsCString(aHost)); +} + +void +ServiceWorkerManager::RemoveAll() +{ + AssertIsOnMainThread(); + + for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { + ServiceWorkerManager::RegistrationDataPerPrincipal* data = it1.UserData(); + for (auto it2 = data->mInfos.Iter(); !it2.Done(); it2.Next()) { + ServiceWorkerRegistrationInfo* reg = it2.UserData(); + ForceUnregister(data, reg); + } + } +} + +void +ServiceWorkerManager::PropagateRemoveAll() +{ + AssertIsOnMainThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (!mActor) { + RefPtr<nsIRunnable> runnable = new PropagateRemoveAllRunnable(); + AppendPendingOperation(runnable); + return; + } + + mActor->SendPropagateRemoveAll(); +} + +void +ServiceWorkerManager::RemoveAllRegistrations(OriginAttributesPattern* aPattern) +{ + AssertIsOnMainThread(); + + MOZ_ASSERT(aPattern); + + for (auto it1 = mRegistrationInfos.Iter(); !it1.Done(); it1.Next()) { + ServiceWorkerManager::RegistrationDataPerPrincipal* data = it1.UserData(); + + // We can use iteration because ForceUnregister (and Unregister) are + // async. Otherwise doing some R/W operations on an hashtable during + // iteration will crash. + for (auto it2 = data->mInfos.Iter(); !it2.Done(); it2.Next()) { + ServiceWorkerRegistrationInfo* reg = it2.UserData(); + + MOZ_ASSERT(reg); + MOZ_ASSERT(reg->mPrincipal); + + bool matches = + aPattern->Matches(BasePrincipal::Cast(reg->mPrincipal)->OriginAttributesRef()); + if (!matches) { + continue; + } + + ForceUnregister(data, reg); + } + } +} + +NS_IMETHODIMP +ServiceWorkerManager::AddListener(nsIServiceWorkerManagerListener* aListener) +{ + AssertIsOnMainThread(); + + if (!aListener || mListeners.Contains(aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mListeners.AppendElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::RemoveListener(nsIServiceWorkerManagerListener* aListener) +{ + AssertIsOnMainThread(); + + if (!aListener || !mListeners.Contains(aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mListeners.RemoveElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::ShouldReportToWindow(mozIDOMWindowProxy* aWindow, + const nsACString& aScope, + bool* aResult) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aResult); + + *aResult = false; + + // Get the inner window ID to compare to our document windows below. + nsCOMPtr<nsPIDOMWindowOuter> targetWin = nsPIDOMWindowOuter::From(aWindow); + if (NS_WARN_IF(!targetWin)) { + return NS_OK; + } + + targetWin = targetWin->GetScriptableTop(); + uint64_t winId = targetWin->WindowID(); + + // Check our weak registering document references first. This way we clear + // out as many dead weak references as possible when this method is called. + WeakDocumentList* list = mRegisteringDocuments.Get(aScope); + if (list) { + for (int32_t i = list->Length() - 1; i >= 0; --i) { + nsCOMPtr<nsIDocument> doc = do_QueryReferent(list->ElementAt(i)); + if (!doc) { + list->RemoveElementAt(i); + continue; + } + + if (!doc->IsCurrentActiveDocument()) { + continue; + } + + nsCOMPtr<nsPIDOMWindowOuter> win = doc->GetWindow(); + if (!win) { + continue; + } + + win = win->GetScriptableTop(); + + // Match. We should report to this window. + if (win && winId == win->WindowID()) { + *aResult = true; + return NS_OK; + } + } + + if (list->IsEmpty()) { + list = nullptr; + nsAutoPtr<WeakDocumentList> doomed; + mRegisteringDocuments.RemoveAndForget(aScope, doomed); + } + } + + // Examine any windows performing a navigation that we are currently + // intercepting. + InterceptionList* intList = mNavigationInterceptions.Get(aScope); + if (intList) { + for (uint32_t i = 0; i < intList->Length(); ++i) { + nsCOMPtr<nsIInterceptedChannel> channel = intList->ElementAt(i); + + nsCOMPtr<nsIChannel> inner; + nsresult rv = channel->GetChannel(getter_AddRefs(inner)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + uint64_t id = nsContentUtils::GetInnerWindowID(inner); + if (id == 0) { + continue; + } + + nsCOMPtr<nsPIDOMWindowInner> win = nsGlobalWindow::GetInnerWindowWithId(id)->AsInner(); + if (!win) { + continue; + } + + nsCOMPtr<nsPIDOMWindowOuter> outer = win->GetScriptableTop(); + + // Match. We should report to this window. + if (outer && winId == outer->WindowID()) { + *aResult = true; + return NS_OK; + } + } + } + + // Next examine controlled documents to see if the windows match. + for (auto iter = mControlledDocuments.Iter(); !iter.Done(); iter.Next()) { + ServiceWorkerRegistrationInfo* reg = iter.UserData(); + MOZ_ASSERT(reg); + if (!reg->mScope.Equals(aScope)) { + continue; + } + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(iter.Key()); + if (!doc || !doc->IsCurrentActiveDocument()) { + continue; + } + + nsCOMPtr<nsPIDOMWindowOuter> win = doc->GetWindow(); + if (!win) { + continue; + } + + win = win->GetScriptableTop(); + + // Match. We should report to this window. + if (win && winId == win->WindowID()) { + *aResult = true; + return NS_OK; + } + } + + // No match. We should not report to this window. + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) +{ + if (strcmp(aTopic, PURGE_SESSION_HISTORY) == 0) { + MOZ_ASSERT(XRE_IsParentProcess()); + RemoveAll(); + PropagateRemoveAll(); + return NS_OK; + } + + if (strcmp(aTopic, PURGE_DOMAIN_DATA) == 0) { + MOZ_ASSERT(XRE_IsParentProcess()); + nsAutoString domain(aData); + RemoveAndPropagate(NS_ConvertUTF16toUTF8(domain)); + return NS_OK; + } + + if (strcmp(aTopic, CLEAR_ORIGIN_DATA) == 0) { + MOZ_ASSERT(XRE_IsParentProcess()); + OriginAttributesPattern pattern; + MOZ_ALWAYS_TRUE(pattern.Init(nsAutoString(aData))); + + RemoveAllRegistrations(&pattern); + return NS_OK; + } + + if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { + MaybeStartShutdown(); + return NS_OK; + } + + MOZ_CRASH("Received message we aren't supposed to be registered for!"); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::PropagateSoftUpdate(JS::Handle<JS::Value> aOriginAttributes, + const nsAString& aScope, + JSContext* aCx) +{ + AssertIsOnMainThread(); + + PrincipalOriginAttributes attrs; + if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { + return NS_ERROR_INVALID_ARG; + } + + PropagateSoftUpdate(attrs, aScope); + return NS_OK; +} + +void +ServiceWorkerManager::PropagateSoftUpdate(const PrincipalOriginAttributes& aOriginAttributes, + const nsAString& aScope) +{ + AssertIsOnMainThread(); + + if (!mActor) { + RefPtr<nsIRunnable> runnable = + new PropagateSoftUpdateRunnable(aOriginAttributes, aScope); + AppendPendingOperation(runnable); + return; + } + + mActor->SendPropagateSoftUpdate(aOriginAttributes, nsString(aScope)); +} + +NS_IMETHODIMP +ServiceWorkerManager::PropagateUnregister(nsIPrincipal* aPrincipal, + nsIServiceWorkerUnregisterCallback* aCallback, + const nsAString& aScope) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + + if (!mActor) { + RefPtr<nsIRunnable> runnable = + new PropagateUnregisterRunnable(aPrincipal, aCallback, aScope); + AppendPendingOperation(runnable); + return NS_OK; + } + + PrincipalInfo principalInfo; + if (NS_WARN_IF(NS_FAILED(PrincipalToPrincipalInfo(aPrincipal, + &principalInfo)))) { + return NS_ERROR_FAILURE; + } + + mActor->SendPropagateUnregister(principalInfo, nsString(aScope)); + + nsresult rv = Unregister(aPrincipal, aCallback, aScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void +ServiceWorkerManager::NotifyListenersOnRegister( + nsIServiceWorkerRegistrationInfo* aInfo) +{ + nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners(mListeners); + for (size_t index = 0; index < listeners.Length(); ++index) { + listeners[index]->OnRegister(aInfo); + } +} + +void +ServiceWorkerManager::NotifyListenersOnUnregister( + nsIServiceWorkerRegistrationInfo* aInfo) +{ + nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> listeners(mListeners); + for (size_t index = 0; index < listeners.Length(); ++index) { + listeners[index]->OnUnregister(aInfo); + } +} + +void +ServiceWorkerManager::AddRegisteringDocument(const nsACString& aScope, + nsIDocument* aDoc) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(!aScope.IsEmpty()); + MOZ_ASSERT(aDoc); + + WeakDocumentList* list = mRegisteringDocuments.LookupOrAdd(aScope); + MOZ_ASSERT(list); + + for (int32_t i = list->Length() - 1; i >= 0; --i) { + nsCOMPtr<nsIDocument> existing = do_QueryReferent(list->ElementAt(i)); + if (!existing) { + list->RemoveElementAt(i); + continue; + } + if (existing == aDoc) { + return; + } + } + + list->AppendElement(do_GetWeakReference(aDoc)); +} + +class ServiceWorkerManager::InterceptionReleaseHandle final : public nsISupports +{ + const nsCString mScope; + + // Weak reference to channel is safe, because the channel holds a + // reference to this object. Also, the pointer is only used for + // comparison purposes. + nsIInterceptedChannel* mChannel; + + ~InterceptionReleaseHandle() + { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->RemoveNavigationInterception(mScope, mChannel); + } + } + +public: + InterceptionReleaseHandle(const nsACString& aScope, + nsIInterceptedChannel* aChannel) + : mScope(aScope) + , mChannel(aChannel) + { + AssertIsOnMainThread(); + MOZ_ASSERT(!aScope.IsEmpty()); + MOZ_ASSERT(mChannel); + } + + NS_DECL_ISUPPORTS +}; + +NS_IMPL_ISUPPORTS0(ServiceWorkerManager::InterceptionReleaseHandle); + +void +ServiceWorkerManager::AddNavigationInterception(const nsACString& aScope, + nsIInterceptedChannel* aChannel) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(!aScope.IsEmpty()); + MOZ_ASSERT(aChannel); + + InterceptionList* list = + mNavigationInterceptions.LookupOrAdd(aScope); + MOZ_ASSERT(list); + MOZ_ASSERT(!list->Contains(aChannel)); + + nsCOMPtr<nsISupports> releaseHandle = + new InterceptionReleaseHandle(aScope, aChannel); + aChannel->SetReleaseHandle(releaseHandle); + + list->AppendElement(aChannel); +} + +void +ServiceWorkerManager::RemoveNavigationInterception(const nsACString& aScope, + nsIInterceptedChannel* aChannel) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aChannel); + InterceptionList* list = + mNavigationInterceptions.Get(aScope); + if (list) { + MOZ_ALWAYS_TRUE(list->RemoveElement(aChannel)); + MOZ_ASSERT(!list->Contains(aChannel)); + if (list->IsEmpty()) { + list = nullptr; + nsAutoPtr<InterceptionList> doomed; + mNavigationInterceptions.RemoveAndForget(aScope, doomed); + } + } +} + +class UpdateTimerCallback final : public nsITimerCallback +{ + nsCOMPtr<nsIPrincipal> mPrincipal; + const nsCString mScope; + + ~UpdateTimerCallback() + { + } + +public: + UpdateTimerCallback(nsIPrincipal* aPrincipal, const nsACString& aScope) + : mPrincipal(aPrincipal) + , mScope(aScope) + { + AssertIsOnMainThread(); + MOZ_ASSERT(mPrincipal); + MOZ_ASSERT(!mScope.IsEmpty()); + } + + NS_IMETHOD + Notify(nsITimer* aTimer) override + { + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // shutting down, do nothing + return NS_OK; + } + + swm->UpdateTimerFired(mPrincipal, mScope); + return NS_OK; + } + + NS_DECL_ISUPPORTS +}; + +NS_IMPL_ISUPPORTS(UpdateTimerCallback, nsITimerCallback) + +bool +ServiceWorkerManager::MayHaveActiveServiceWorkerInstance(ContentParent* aContent, + nsIPrincipal* aPrincipal) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + + if (mShuttingDown) { + return false; + } + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(scopeKey, &data)) { + return false; + } + + return true; +} + +void +ServiceWorkerManager::ScheduleUpdateTimer(nsIPrincipal* aPrincipal, + const nsACString& aScope) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(!aScope.IsEmpty()); + + if (mShuttingDown) { + return; + } + + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(scopeKey, &data)) { + return; + } + + nsCOMPtr<nsITimer> timer = data->mUpdateTimers.Get(aScope); + if (timer) { + // There is already a timer scheduled. In this case just use the original + // schedule time. We don't want to push it out to a later time since that + // could allow updates to be starved forever if events are continuously + // fired. + return; + } + + timer = do_CreateInstance("@mozilla.org/timer;1", &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsCOMPtr<nsITimerCallback> callback = new UpdateTimerCallback(aPrincipal, + aScope); + + const uint32_t UPDATE_DELAY_MS = 1000; + + rv = timer->InitWithCallback(callback, UPDATE_DELAY_MS, + nsITimer::TYPE_ONE_SHOT); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + data->mUpdateTimers.Put(aScope, timer); +} + +void +ServiceWorkerManager::UpdateTimerFired(nsIPrincipal* aPrincipal, + const nsACString& aScope) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(!aScope.IsEmpty()); + + if (mShuttingDown) { + return; + } + + // First cleanup the timer. + nsAutoCString scopeKey; + nsresult rv = PrincipalToScopeKey(aPrincipal, scopeKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RegistrationDataPerPrincipal* data; + if (!mRegistrationInfos.Get(scopeKey, &data)) { + return; + } + + nsCOMPtr<nsITimer> timer = data->mUpdateTimers.Get(aScope); + if (timer) { + timer->Cancel(); + data->mUpdateTimers.Remove(aScope); + } + + RefPtr<ServiceWorkerRegistrationInfo> registration; + data->mInfos.Get(aScope, getter_AddRefs(registration)); + if (!registration) { + return; + } + + if (!registration->CheckAndClearIfUpdateNeeded()) { + return; + } + + PrincipalOriginAttributes attrs = + BasePrincipal::Cast(aPrincipal)->OriginAttributesRef(); + + SoftUpdate(attrs, aScope); +} + +void +ServiceWorkerManager::MaybeSendUnregister(nsIPrincipal* aPrincipal, + const nsACString& aScope) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(!aScope.IsEmpty()); + + if (!mActor) { + return; + } + + PrincipalInfo principalInfo; + nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &principalInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + Unused << mActor->SendUnregister(principalInfo, NS_ConvertUTF8toUTF16(aScope)); +} + +END_WORKERS_NAMESPACE diff --git a/dom/workers/ServiceWorkerManager.h b/dom/workers/ServiceWorkerManager.h new file mode 100644 index 000000000..f99bc4248 --- /dev/null +++ b/dom/workers/ServiceWorkerManager.h @@ -0,0 +1,521 @@ +/* -*- 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_workers_serviceworkermanager_h +#define mozilla_dom_workers_serviceworkermanager_h + +#include "nsIServiceWorkerManager.h" +#include "nsCOMPtr.h" + +#include "ipc/IPCMessageUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/ConsoleReportCollector.h" +#include "mozilla/LinkedList.h" +#include "mozilla/Preferences.h" +#include "mozilla/TypedEnumBits.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/ServiceWorkerCommon.h" +#include "mozilla/dom/ServiceWorkerRegistrar.h" +#include "mozilla/dom/ServiceWorkerRegistrarTypes.h" +#include "mozilla/dom/workers/ServiceWorkerRegistrationInfo.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "nsClassHashtable.h" +#include "nsDataHashtable.h" +#include "nsIIPCBackgroundChildCreateCallback.h" +#include "nsRefPtrHashtable.h" +#include "nsTArrayForwardDeclare.h" +#include "nsTObserverArray.h" + +class mozIApplicationClearPrivateDataParams; +class nsIConsoleReportCollector; + +namespace mozilla { + +class PrincipalOriginAttributes; + +namespace dom { + +class ServiceWorkerRegistrar; +class ServiceWorkerRegistrationListener; + +namespace workers { + +class ServiceWorkerClientInfo; +class ServiceWorkerInfo; +class ServiceWorkerJobQueue; +class ServiceWorkerManagerChild; +class ServiceWorkerPrivate; + +class ServiceWorkerUpdateFinishCallback +{ +protected: + virtual ~ServiceWorkerUpdateFinishCallback() + {} + +public: + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerUpdateFinishCallback) + + virtual + void UpdateSucceeded(ServiceWorkerRegistrationInfo* aInfo) = 0; + + virtual + void UpdateFailed(ErrorResult& aStatus) = 0; +}; + +#define NS_SERVICEWORKERMANAGER_IMPL_IID \ +{ /* f4f8755a-69ca-46e8-a65d-775745535990 */ \ + 0xf4f8755a, \ + 0x69ca, \ + 0x46e8, \ + { 0xa6, 0x5d, 0x77, 0x57, 0x45, 0x53, 0x59, 0x90 } \ +} + +/* + * The ServiceWorkerManager is a per-process global that deals with the + * installation, querying and event dispatch of ServiceWorkers for all the + * origins in the process. + */ +class ServiceWorkerManager final + : public nsIServiceWorkerManager + , public nsIIPCBackgroundChildCreateCallback + , public nsIObserver +{ + friend class GetReadyPromiseRunnable; + friend class GetRegistrationsRunnable; + friend class GetRegistrationRunnable; + friend class ServiceWorkerJob; + friend class ServiceWorkerRegistrationInfo; + friend class ServiceWorkerUnregisterJob; + friend class ServiceWorkerUpdateJob; + friend class UpdateTimerCallback; + +public: + NS_DECL_ISUPPORTS + NS_DECL_NSISERVICEWORKERMANAGER + NS_DECL_NSIIPCBACKGROUNDCHILDCREATECALLBACK + NS_DECL_NSIOBSERVER + + struct RegistrationDataPerPrincipal; + nsClassHashtable<nsCStringHashKey, RegistrationDataPerPrincipal> mRegistrationInfos; + + nsTObserverArray<ServiceWorkerRegistrationListener*> mServiceWorkerRegistrationListeners; + + nsRefPtrHashtable<nsISupportsHashKey, ServiceWorkerRegistrationInfo> mControlledDocuments; + + // Track all documents that have attempted to register a service worker for a + // given scope. + typedef nsTArray<nsCOMPtr<nsIWeakReference>> WeakDocumentList; + nsClassHashtable<nsCStringHashKey, WeakDocumentList> mRegisteringDocuments; + + // Track all intercepted navigation channels for a given scope. Channels are + // placed in the appropriate list before dispatch the FetchEvent to the worker + // thread and removed once FetchEvent processing dispatches back to the main + // thread. + // + // Note: Its safe to use weak references here because a RAII-style callback + // is registered with the channel before its added to this list. We + // are guaranteed the callback will fire before and remove the ref + // from this list before the channel is destroyed. + typedef nsTArray<nsIInterceptedChannel*> InterceptionList; + nsClassHashtable<nsCStringHashKey, InterceptionList> mNavigationInterceptions; + + bool + IsAvailable(nsIPrincipal* aPrincipal, nsIURI* aURI); + + bool + IsControlled(nsIDocument* aDocument, ErrorResult& aRv); + + // Return true if the given content process could potentially be executing + // service worker code with the given principal. At the current time, this + // just means that we have any registration for the origin, regardless of + // scope. This is a very weak guarantee but is the best we can do when push + // notifications can currently spin up a service worker in content processes + // without our involvement in the parent process. + // + // In the future when there is only a single ServiceWorkerManager in the + // parent process that is entirely in control of spawning and running service + // worker code, we will be able to authoritatively indicate whether there is + // an activate service worker in the given content process. At that time we + // will rename this method HasActiveServiceWorkerInstance and provide + // semantics that ensure this method returns true until the worker is known to + // have shut down in order to allow the caller to induce a crash for security + // reasons without having to worry about shutdown races with the worker. + bool + MayHaveActiveServiceWorkerInstance(ContentParent* aContent, + nsIPrincipal* aPrincipal); + + void + DispatchFetchEvent(const PrincipalOriginAttributes& aOriginAttributes, + nsIDocument* aDoc, + const nsAString& aDocumentIdForTopLevelNavigation, + nsIInterceptedChannel* aChannel, + bool aIsReload, + bool aIsSubresourceLoad, + ErrorResult& aRv); + + void + Update(nsIPrincipal* aPrincipal, + const nsACString& aScope, + ServiceWorkerUpdateFinishCallback* aCallback); + + void + SoftUpdate(const PrincipalOriginAttributes& aOriginAttributes, + const nsACString& aScope); + + void + PropagateSoftUpdate(const PrincipalOriginAttributes& aOriginAttributes, + const nsAString& aScope); + + void + PropagateRemove(const nsACString& aHost); + + void + Remove(const nsACString& aHost); + + void + PropagateRemoveAll(); + + void + RemoveAll(); + + already_AddRefed<ServiceWorkerRegistrationInfo> + GetRegistration(nsIPrincipal* aPrincipal, const nsACString& aScope) const; + + already_AddRefed<ServiceWorkerRegistrationInfo> + CreateNewRegistration(const nsCString& aScope, nsIPrincipal* aPrincipal); + + void + RemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration); + + void StoreRegistration(nsIPrincipal* aPrincipal, + ServiceWorkerRegistrationInfo* aRegistration); + + void + FinishFetch(ServiceWorkerRegistrationInfo* aRegistration); + + /** + * Report an error for the given scope to any window we think might be + * interested, failing over to the Browser Console if we couldn't find any. + * + * Error messages should be localized, so you probably want to call + * LocalizeAndReportToAllClients instead, which in turn calls us after + * localizing the error. + */ + void + ReportToAllClients(const nsCString& aScope, + const nsString& aMessage, + const nsString& aFilename, + const nsString& aLine, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aFlags); + + /** + * Report a localized error for the given scope to any window we think might + * be interested. + * + * Note that this method takes an nsTArray<nsString> for the parameters, not + * bare chart16_t*[]. You can use a std::initializer_list constructor inline + * so that argument might look like: nsTArray<nsString> { some_nsString, + * PromiseFlatString(some_nsSubString_aka_nsAString), + * NS_ConvertUTF8toUTF16(some_nsCString_or_nsCSubString), + * NS_LITERAL_STRING("some literal") }. If you have anything else, like a + * number, you can use an nsAutoString with AppendInt/friends. + * + * @param [aFlags] + * The nsIScriptError flag, one of errorFlag (0x0), warningFlag (0x1), + * infoFlag (0x8). We default to error if omitted because usually we're + * logging exceptional and/or obvious breakage. + */ + static void + LocalizeAndReportToAllClients(const nsCString& aScope, + const char* aStringKey, + const nsTArray<nsString>& aParamArray, + uint32_t aFlags = 0x0, + const nsString& aFilename = EmptyString(), + const nsString& aLine = EmptyString(), + uint32_t aLineNumber = 0, + uint32_t aColumnNumber = 0); + + void + FlushReportsToAllClients(const nsACString& aScope, + nsIConsoleReportCollector* aReporter); + + // Always consumes the error by reporting to consoles of all controlled + // documents. + void + HandleError(JSContext* aCx, + nsIPrincipal* aPrincipal, + const nsCString& aScope, + const nsString& aWorkerURL, + const nsString& aMessage, + const nsString& aFilename, + const nsString& aLine, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aFlags, + JSExnType aExnType); + + UniquePtr<ServiceWorkerClientInfo> + GetClient(nsIPrincipal* aPrincipal, + const nsAString& aClientId, + ErrorResult& aRv); + + void + GetAllClients(nsIPrincipal* aPrincipal, + const nsCString& aScope, + bool aIncludeUncontrolled, + nsTArray<ServiceWorkerClientInfo>& aDocuments); + + void + MaybeClaimClient(nsIDocument* aDocument, + ServiceWorkerRegistrationInfo* aWorkerRegistration); + + nsresult + ClaimClients(nsIPrincipal* aPrincipal, const nsCString& aScope, uint64_t aId); + + void + SetSkipWaitingFlag(nsIPrincipal* aPrincipal, const nsCString& aScope, + uint64_t aServiceWorkerID); + + static already_AddRefed<ServiceWorkerManager> + GetInstance(); + + void + LoadRegistration(const ServiceWorkerRegistrationData& aRegistration); + + void + LoadRegistrations(const nsTArray<ServiceWorkerRegistrationData>& aRegistrations); + + // Used by remove() and removeAll() when clearing history. + // MUST ONLY BE CALLED FROM UnregisterIfMatchesHost! + void + ForceUnregister(RegistrationDataPerPrincipal* aRegistrationData, + ServiceWorkerRegistrationInfo* aRegistration); + + NS_IMETHOD + AddRegistrationEventListener(const nsAString& aScope, + ServiceWorkerRegistrationListener* aListener); + + NS_IMETHOD + RemoveRegistrationEventListener(const nsAString& aScope, + ServiceWorkerRegistrationListener* aListener); + + void + MaybeCheckNavigationUpdate(nsIDocument* aDoc); + + nsresult + SendPushEvent(const nsACString& aOriginAttributes, + const nsACString& aScope, + const nsAString& aMessageId, + const Maybe<nsTArray<uint8_t>>& aData); + + nsresult + NotifyUnregister(nsIPrincipal* aPrincipal, const nsAString& aScope); + + void + WorkerIsIdle(ServiceWorkerInfo* aWorker); + +private: + ServiceWorkerManager(); + ~ServiceWorkerManager(); + + void + Init(ServiceWorkerRegistrar* aRegistrar); + + void + MaybeStartShutdown(); + + already_AddRefed<ServiceWorkerJobQueue> + GetOrCreateJobQueue(const nsACString& aOriginSuffix, + const nsACString& aScope); + + void + MaybeRemoveRegistrationInfo(const nsACString& aScopeKey); + + already_AddRefed<ServiceWorkerRegistrationInfo> + GetRegistration(const nsACString& aScopeKey, + const nsACString& aScope) const; + + void + AbortCurrentUpdate(ServiceWorkerRegistrationInfo* aRegistration); + + nsresult + Update(ServiceWorkerRegistrationInfo* aRegistration); + + nsresult + GetDocumentRegistration(nsIDocument* aDoc, + ServiceWorkerRegistrationInfo** aRegistrationInfo); + + nsresult + GetServiceWorkerForScope(nsPIDOMWindowInner* aWindow, + const nsAString& aScope, + WhichServiceWorker aWhichWorker, + nsISupports** aServiceWorker); + + ServiceWorkerInfo* + GetActiveWorkerInfoForScope(const PrincipalOriginAttributes& aOriginAttributes, + const nsACString& aScope); + + ServiceWorkerInfo* + GetActiveWorkerInfoForDocument(nsIDocument* aDocument); + + void + InvalidateServiceWorkerRegistrationWorker(ServiceWorkerRegistrationInfo* aRegistration, + WhichServiceWorker aWhichOnes); + + void + NotifyServiceWorkerRegistrationRemoved(ServiceWorkerRegistrationInfo* aRegistration); + + void + StartControllingADocument(ServiceWorkerRegistrationInfo* aRegistration, + nsIDocument* aDoc, + const nsAString& aDocumentId); + + void + StopControllingADocument(ServiceWorkerRegistrationInfo* aRegistration); + + already_AddRefed<ServiceWorkerRegistrationInfo> + GetServiceWorkerRegistrationInfo(nsPIDOMWindowInner* aWindow); + + already_AddRefed<ServiceWorkerRegistrationInfo> + GetServiceWorkerRegistrationInfo(nsIDocument* aDoc); + + already_AddRefed<ServiceWorkerRegistrationInfo> + GetServiceWorkerRegistrationInfo(nsIPrincipal* aPrincipal, nsIURI* aURI); + + already_AddRefed<ServiceWorkerRegistrationInfo> + GetServiceWorkerRegistrationInfo(const nsACString& aScopeKey, + nsIURI* aURI); + + // This method generates a key using appId and isInElementBrowser from the + // principal. We don't use the origin because it can change during the + // loading. + static nsresult + PrincipalToScopeKey(nsIPrincipal* aPrincipal, nsACString& aKey); + + static void + AddScopeAndRegistration(const nsACString& aScope, + ServiceWorkerRegistrationInfo* aRegistation); + + static bool + FindScopeForPath(const nsACString& aScopeKey, + const nsACString& aPath, + RegistrationDataPerPrincipal** aData, nsACString& aMatch); + + static bool + HasScope(nsIPrincipal* aPrincipal, const nsACString& aScope); + + static void + RemoveScopeAndRegistration(ServiceWorkerRegistrationInfo* aRegistration); + + void + QueueFireEventOnServiceWorkerRegistrations(ServiceWorkerRegistrationInfo* aRegistration, + const nsAString& aName); + + void + FireUpdateFoundOnServiceWorkerRegistrations(ServiceWorkerRegistrationInfo* aRegistration); + + void + FireControllerChange(ServiceWorkerRegistrationInfo* aRegistration); + + void + StorePendingReadyPromise(nsPIDOMWindowInner* aWindow, nsIURI* aURI, + Promise* aPromise); + + void + CheckPendingReadyPromises(); + + bool + CheckReadyPromise(nsPIDOMWindowInner* aWindow, nsIURI* aURI, + Promise* aPromise); + + struct PendingReadyPromise final + { + PendingReadyPromise(nsIURI* aURI, Promise* aPromise) + : mURI(aURI), mPromise(aPromise) + {} + + nsCOMPtr<nsIURI> mURI; + RefPtr<Promise> mPromise; + }; + + void AppendPendingOperation(nsIRunnable* aRunnable); + + bool HasBackgroundActor() const + { + return !!mActor; + } + + nsClassHashtable<nsISupportsHashKey, PendingReadyPromise> mPendingReadyPromises; + + void + MaybeRemoveRegistration(ServiceWorkerRegistrationInfo* aRegistration); + + // Removes all service worker registrations that matches the given pattern. + void + RemoveAllRegistrations(OriginAttributesPattern* aPattern); + + RefPtr<ServiceWorkerManagerChild> mActor; + + nsTArray<nsCOMPtr<nsIRunnable>> mPendingOperations; + + bool mShuttingDown; + + nsTArray<nsCOMPtr<nsIServiceWorkerManagerListener>> mListeners; + + void + NotifyListenersOnRegister(nsIServiceWorkerRegistrationInfo* aRegistration); + + void + NotifyListenersOnUnregister(nsIServiceWorkerRegistrationInfo* aRegistration); + + void + AddRegisteringDocument(const nsACString& aScope, nsIDocument* aDoc); + + class InterceptionReleaseHandle; + + void + AddNavigationInterception(const nsACString& aScope, + nsIInterceptedChannel* aChannel); + + void + RemoveNavigationInterception(const nsACString& aScope, + nsIInterceptedChannel* aChannel); + + void + ScheduleUpdateTimer(nsIPrincipal* aPrincipal, const nsACString& aScope); + + void + UpdateTimerFired(nsIPrincipal* aPrincipal, const nsACString& aScope); + + void + MaybeSendUnregister(nsIPrincipal* aPrincipal, const nsACString& aScope); + + nsresult + SendNotificationEvent(const nsAString& aEventName, + const nsACString& aOriginSuffix, + const nsACString& aScope, + const nsAString& aID, + const nsAString& aTitle, + const nsAString& aDir, + const nsAString& aLang, + const nsAString& aBody, + const nsAString& aTag, + const nsAString& aIcon, + const nsAString& aData, + const nsAString& aBehavior); +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_serviceworkermanager_h diff --git a/dom/workers/ServiceWorkerManagerChild.cpp b/dom/workers/ServiceWorkerManagerChild.cpp new file mode 100644 index 000000000..47a39d1c2 --- /dev/null +++ b/dom/workers/ServiceWorkerManagerChild.cpp @@ -0,0 +1,107 @@ +/* -*- 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 "ServiceWorkerManagerChild.h" +#include "ServiceWorkerManager.h" +#include "mozilla/Unused.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { +namespace workers { + +bool +ServiceWorkerManagerChild::RecvNotifyRegister( + const ServiceWorkerRegistrationData& aData) +{ + if (mShuttingDown) { + return true; + } + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->LoadRegistration(aData); + } + + return true; +} + +bool +ServiceWorkerManagerChild::RecvNotifySoftUpdate( + const PrincipalOriginAttributes& aOriginAttributes, + const nsString& aScope) +{ + if (mShuttingDown) { + return true; + } + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->SoftUpdate(aOriginAttributes, NS_ConvertUTF16toUTF8(aScope)); + } + + return true; +} + +bool +ServiceWorkerManagerChild::RecvNotifyUnregister(const PrincipalInfo& aPrincipalInfo, + const nsString& aScope) +{ + if (mShuttingDown) { + return true; + } + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown + return true; + } + + nsCOMPtr<nsIPrincipal> principal = PrincipalInfoToPrincipal(aPrincipalInfo); + if (NS_WARN_IF(!principal)) { + return true; + } + + nsresult rv = swm->NotifyUnregister(principal, aScope); + Unused << NS_WARN_IF(NS_FAILED(rv)); + return true; +} + +bool +ServiceWorkerManagerChild::RecvNotifyRemove(const nsCString& aHost) +{ + if (mShuttingDown) { + return true; + } + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->Remove(aHost); + } + + return true; +} + +bool +ServiceWorkerManagerChild::RecvNotifyRemoveAll() +{ + if (mShuttingDown) { + return true; + } + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->RemoveAll(); + } + + return true; +} + +} // namespace workers +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/ServiceWorkerManagerChild.h b/dom/workers/ServiceWorkerManagerChild.h new file mode 100644 index 000000000..d32f3ed10 --- /dev/null +++ b/dom/workers/ServiceWorkerManagerChild.h @@ -0,0 +1,63 @@ +/* -*- 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_ServiceWorkerManagerChild_h +#define mozilla_dom_ServiceWorkerManagerChild_h + +#include "mozilla/dom/PServiceWorkerManagerChild.h" +#include "mozilla/ipc/BackgroundUtils.h" + +namespace mozilla { + +class PrincipalOriginAttributes; + +namespace ipc { +class BackgroundChildImpl; +} // namespace ipc + +namespace dom { +namespace workers { + +class ServiceWorkerManagerChild final : public PServiceWorkerManagerChild +{ + friend class mozilla::ipc::BackgroundChildImpl; + +public: + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerManagerChild) + + void ManagerShuttingDown() + { + mShuttingDown = true; + } + + virtual bool RecvNotifyRegister(const ServiceWorkerRegistrationData& aData) + override; + + virtual bool RecvNotifySoftUpdate(const PrincipalOriginAttributes& aOriginAttributes, + const nsString& aScope) override; + + virtual bool RecvNotifyUnregister(const PrincipalInfo& aPrincipalInfo, + const nsString& aScope) override; + + virtual bool RecvNotifyRemove(const nsCString& aHost) override; + + virtual bool RecvNotifyRemoveAll() override; + +private: + ServiceWorkerManagerChild() + : mShuttingDown(false) + {} + + ~ServiceWorkerManagerChild() {} + + bool mShuttingDown; +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ServiceWorkerManagerChild_h diff --git a/dom/workers/ServiceWorkerManagerParent.cpp b/dom/workers/ServiceWorkerManagerParent.cpp new file mode 100644 index 000000000..bd9afad7a --- /dev/null +++ b/dom/workers/ServiceWorkerManagerParent.cpp @@ -0,0 +1,330 @@ +/* -*- 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 "ServiceWorkerManagerParent.h" +#include "ServiceWorkerManagerService.h" +#include "mozilla/AppProcessChecker.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/ServiceWorkerRegistrar.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/Unused.h" +#include "nsThreadUtils.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { +namespace workers { + +namespace { + +uint64_t sServiceWorkerManagerParentID = 0; + +class RegisterServiceWorkerCallback final : public Runnable +{ +public: + RegisterServiceWorkerCallback(const ServiceWorkerRegistrationData& aData, + uint64_t aParentID) + : mData(aData) + , mParentID(aParentID) + { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + } + + NS_IMETHOD + Run() override + { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + RefPtr<dom::ServiceWorkerRegistrar> service = + dom::ServiceWorkerRegistrar::Get(); + + // Shutdown during the process of trying to update the registrar. Give + // up on this modification. + if (!service) { + return NS_OK; + } + + service->RegisterServiceWorker(mData); + + RefPtr<ServiceWorkerManagerService> managerService = + ServiceWorkerManagerService::Get(); + if (managerService) { + managerService->PropagateRegistration(mParentID, mData); + } + + return NS_OK; + } + +private: + ServiceWorkerRegistrationData mData; + const uint64_t mParentID; +}; + +class UnregisterServiceWorkerCallback final : public Runnable +{ +public: + UnregisterServiceWorkerCallback(const PrincipalInfo& aPrincipalInfo, + const nsString& aScope, + uint64_t aParentID) + : mPrincipalInfo(aPrincipalInfo) + , mScope(aScope) + , mParentID(aParentID) + { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + } + + NS_IMETHOD + Run() override + { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + RefPtr<dom::ServiceWorkerRegistrar> service = + dom::ServiceWorkerRegistrar::Get(); + + // Shutdown during the process of trying to update the registrar. Give + // up on this modification. + if (!service) { + return NS_OK; + } + + service->UnregisterServiceWorker(mPrincipalInfo, + NS_ConvertUTF16toUTF8(mScope)); + + RefPtr<ServiceWorkerManagerService> managerService = + ServiceWorkerManagerService::Get(); + if (managerService) { + managerService->PropagateUnregister(mParentID, mPrincipalInfo, + mScope); + } + + return NS_OK; + } + +private: + const PrincipalInfo mPrincipalInfo; + nsString mScope; + uint64_t mParentID; +}; + +class CheckPrincipalWithCallbackRunnable final : public Runnable +{ +public: + CheckPrincipalWithCallbackRunnable(already_AddRefed<ContentParent> aParent, + const PrincipalInfo& aPrincipalInfo, + Runnable* aCallback) + : mContentParent(aParent) + , mPrincipalInfo(aPrincipalInfo) + , mCallback(aCallback) + , mBackgroundThread(NS_GetCurrentThread()) + { + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + MOZ_ASSERT(mContentParent); + MOZ_ASSERT(mCallback); + MOZ_ASSERT(mBackgroundThread); + } + + NS_IMETHOD Run() override + { + if (NS_IsMainThread()) { + nsCOMPtr<nsIPrincipal> principal = PrincipalInfoToPrincipal(mPrincipalInfo); + AssertAppPrincipal(mContentParent, principal); + mContentParent = nullptr; + + mBackgroundThread->Dispatch(this, NS_DISPATCH_NORMAL); + return NS_OK; + } + + AssertIsOnBackgroundThread(); + mCallback->Run(); + mCallback = nullptr; + + return NS_OK; + } + +private: + RefPtr<ContentParent> mContentParent; + PrincipalInfo mPrincipalInfo; + RefPtr<Runnable> mCallback; + nsCOMPtr<nsIThread> mBackgroundThread; +}; + +} // namespace + +ServiceWorkerManagerParent::ServiceWorkerManagerParent() + : mService(ServiceWorkerManagerService::GetOrCreate()) + , mID(++sServiceWorkerManagerParentID) +{ + AssertIsOnBackgroundThread(); + mService->RegisterActor(this); +} + +ServiceWorkerManagerParent::~ServiceWorkerManagerParent() +{ + AssertIsOnBackgroundThread(); +} + +bool +ServiceWorkerManagerParent::RecvRegister( + const ServiceWorkerRegistrationData& aData) +{ + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + // Basic validation. + if (aData.scope().IsEmpty() || + aData.principal().type() == PrincipalInfo::TNullPrincipalInfo || + aData.principal().type() == PrincipalInfo::TSystemPrincipalInfo) { + return false; + } + + RefPtr<RegisterServiceWorkerCallback> callback = + new RegisterServiceWorkerCallback(aData, mID); + + RefPtr<ContentParent> parent = + BackgroundParent::GetContentParent(Manager()); + + // If the ContentParent is null we are dealing with a same-process actor. + if (!parent) { + callback->Run(); + return true; + } + + RefPtr<CheckPrincipalWithCallbackRunnable> runnable = + new CheckPrincipalWithCallbackRunnable(parent.forget(), aData.principal(), + callback); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable)); + + return true; +} + +bool +ServiceWorkerManagerParent::RecvUnregister(const PrincipalInfo& aPrincipalInfo, + const nsString& aScope) +{ + AssertIsInMainProcess(); + AssertIsOnBackgroundThread(); + + // Basic validation. + if (aScope.IsEmpty() || + aPrincipalInfo.type() == PrincipalInfo::TNullPrincipalInfo || + aPrincipalInfo.type() == PrincipalInfo::TSystemPrincipalInfo) { + return false; + } + + RefPtr<UnregisterServiceWorkerCallback> callback = + new UnregisterServiceWorkerCallback(aPrincipalInfo, aScope, mID); + + RefPtr<ContentParent> parent = + BackgroundParent::GetContentParent(Manager()); + + // If the ContentParent is null we are dealing with a same-process actor. + if (!parent) { + callback->Run(); + return true; + } + + RefPtr<CheckPrincipalWithCallbackRunnable> runnable = + new CheckPrincipalWithCallbackRunnable(parent.forget(), aPrincipalInfo, + callback); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable)); + + return true; +} + +bool +ServiceWorkerManagerParent::RecvPropagateSoftUpdate(const PrincipalOriginAttributes& aOriginAttributes, + const nsString& aScope) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!mService)) { + return false; + } + + mService->PropagateSoftUpdate(mID, aOriginAttributes, aScope); + return true; +} + +bool +ServiceWorkerManagerParent::RecvPropagateUnregister(const PrincipalInfo& aPrincipalInfo, + const nsString& aScope) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!mService)) { + return false; + } + + mService->PropagateUnregister(mID, aPrincipalInfo, aScope); + return true; +} + +bool +ServiceWorkerManagerParent::RecvPropagateRemove(const nsCString& aHost) +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!mService)) { + return false; + } + + mService->PropagateRemove(mID, aHost); + return true; +} + +bool +ServiceWorkerManagerParent::RecvPropagateRemoveAll() +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!mService)) { + return false; + } + + mService->PropagateRemoveAll(mID); + return true; +} + +bool +ServiceWorkerManagerParent::RecvShutdown() +{ + AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(!mService)) { + return false; + } + + mService->UnregisterActor(this); + mService = nullptr; + + Unused << Send__delete__(this); + return true; +} + +void +ServiceWorkerManagerParent::ActorDestroy(ActorDestroyReason aWhy) +{ + AssertIsOnBackgroundThread(); + + if (mService) { + // This object is about to be released and with it, also mService will be + // released too. + mService->UnregisterActor(this); + } +} + +} // namespace workers +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/ServiceWorkerManagerParent.h b/dom/workers/ServiceWorkerManagerParent.h new file mode 100644 index 000000000..83a0a97aa --- /dev/null +++ b/dom/workers/ServiceWorkerManagerParent.h @@ -0,0 +1,72 @@ +/* -*- 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_ServiceWorkerManagerParent_h +#define mozilla_dom_ServiceWorkerManagerParent_h + +#include "mozilla/dom/PServiceWorkerManagerParent.h" + +namespace mozilla { + +class PrincipalOriginAttributes; + +namespace ipc { +class BackgroundParentImpl; +} // namespace ipc + +namespace dom { +namespace workers { + +class ServiceWorkerManagerService; + +class ServiceWorkerManagerParent final : public PServiceWorkerManagerParent +{ + friend class mozilla::ipc::BackgroundParentImpl; + +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ServiceWorkerManagerParent) + + uint64_t ID() const + { + return mID; + } + +private: + ServiceWorkerManagerParent(); + ~ServiceWorkerManagerParent(); + + virtual bool RecvRegister( + const ServiceWorkerRegistrationData& aData) override; + + virtual bool RecvUnregister(const PrincipalInfo& aPrincipalInfo, + const nsString& aScope) override; + + virtual bool RecvPropagateSoftUpdate(const PrincipalOriginAttributes& aOriginAttributes, + const nsString& aScope) override; + + virtual bool RecvPropagateUnregister(const PrincipalInfo& aPrincipalInfo, + const nsString& aScope) override; + + virtual bool RecvPropagateRemove(const nsCString& aHost) override; + + virtual bool RecvPropagateRemoveAll() override; + + virtual bool RecvShutdown() override; + + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + RefPtr<ServiceWorkerManagerService> mService; + + // We use this ID in the Service in order to avoid the sending of messages to + // ourself. + uint64_t mID; +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ServiceWorkerManagerParent_h diff --git a/dom/workers/ServiceWorkerManagerService.cpp b/dom/workers/ServiceWorkerManagerService.cpp new file mode 100644 index 000000000..983bd77db --- /dev/null +++ b/dom/workers/ServiceWorkerManagerService.cpp @@ -0,0 +1,237 @@ +/* -*- 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 "ServiceWorkerManagerService.h" +#include "ServiceWorkerManagerParent.h" +#include "ServiceWorkerRegistrar.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/Unused.h" +#include "nsAutoPtr.h" + +namespace mozilla { + +using namespace ipc; + +namespace dom { +namespace workers { + +namespace { + +ServiceWorkerManagerService* sInstance = nullptr; + +} // namespace + +ServiceWorkerManagerService::ServiceWorkerManagerService() +{ + AssertIsOnBackgroundThread(); + + // sInstance is a raw ServiceWorkerManagerService*. + MOZ_ASSERT(!sInstance); + sInstance = this; +} + +ServiceWorkerManagerService::~ServiceWorkerManagerService() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(sInstance == this); + MOZ_ASSERT(mAgents.Count() == 0); + + sInstance = nullptr; +} + +/* static */ already_AddRefed<ServiceWorkerManagerService> +ServiceWorkerManagerService::Get() +{ + AssertIsOnBackgroundThread(); + + RefPtr<ServiceWorkerManagerService> instance = sInstance; + return instance.forget(); +} + +/* static */ already_AddRefed<ServiceWorkerManagerService> +ServiceWorkerManagerService::GetOrCreate() +{ + AssertIsOnBackgroundThread(); + + RefPtr<ServiceWorkerManagerService> instance = sInstance; + if (!instance) { + instance = new ServiceWorkerManagerService(); + } + return instance.forget(); +} + +void +ServiceWorkerManagerService::RegisterActor(ServiceWorkerManagerParent* aParent) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParent); + MOZ_ASSERT(!mAgents.Contains(aParent)); + + mAgents.PutEntry(aParent); +} + +void +ServiceWorkerManagerService::UnregisterActor(ServiceWorkerManagerParent* aParent) +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aParent); + MOZ_ASSERT(mAgents.Contains(aParent)); + + mAgents.RemoveEntry(aParent); +} + +void +ServiceWorkerManagerService::PropagateRegistration( + uint64_t aParentID, + ServiceWorkerRegistrationData& aData) +{ + AssertIsOnBackgroundThread(); + + DebugOnly<bool> parentFound = false; + for (auto iter = mAgents.Iter(); !iter.Done(); iter.Next()) { + RefPtr<ServiceWorkerManagerParent> parent = iter.Get()->GetKey(); + MOZ_ASSERT(parent); + + if (parent->ID() != aParentID) { + Unused << parent->SendNotifyRegister(aData); +#ifdef DEBUG + } else { + parentFound = true; +#endif + } + } + +#ifdef DEBUG + MOZ_ASSERT(parentFound); +#endif +} + +void +ServiceWorkerManagerService::PropagateSoftUpdate( + uint64_t aParentID, + const PrincipalOriginAttributes& aOriginAttributes, + const nsAString& aScope) +{ + AssertIsOnBackgroundThread(); + + DebugOnly<bool> parentFound = false; + for (auto iter = mAgents.Iter(); !iter.Done(); iter.Next()) { + RefPtr<ServiceWorkerManagerParent> parent = iter.Get()->GetKey(); + MOZ_ASSERT(parent); + + nsString scope(aScope); + Unused << parent->SendNotifySoftUpdate(aOriginAttributes, + scope); + +#ifdef DEBUG + if (parent->ID() == aParentID) { + parentFound = true; + } +#endif + } + +#ifdef DEBUG + MOZ_ASSERT(parentFound); +#endif +} + +void +ServiceWorkerManagerService::PropagateUnregister( + uint64_t aParentID, + const PrincipalInfo& aPrincipalInfo, + const nsAString& aScope) +{ + AssertIsOnBackgroundThread(); + + RefPtr<dom::ServiceWorkerRegistrar> service = + dom::ServiceWorkerRegistrar::Get(); + MOZ_ASSERT(service); + + // It's possible that we don't have any ServiceWorkerManager managing this + // scope but we still need to unregister it from the ServiceWorkerRegistrar. + service->UnregisterServiceWorker(aPrincipalInfo, + NS_ConvertUTF16toUTF8(aScope)); + + DebugOnly<bool> parentFound = false; + for (auto iter = mAgents.Iter(); !iter.Done(); iter.Next()) { + RefPtr<ServiceWorkerManagerParent> parent = iter.Get()->GetKey(); + MOZ_ASSERT(parent); + + if (parent->ID() != aParentID) { + nsString scope(aScope); + Unused << parent->SendNotifyUnregister(aPrincipalInfo, scope); +#ifdef DEBUG + } else { + parentFound = true; +#endif + } + } + +#ifdef DEBUG + MOZ_ASSERT(parentFound); +#endif +} + +void +ServiceWorkerManagerService::PropagateRemove(uint64_t aParentID, + const nsACString& aHost) +{ + AssertIsOnBackgroundThread(); + + DebugOnly<bool> parentFound = false; + for (auto iter = mAgents.Iter(); !iter.Done(); iter.Next()) { + RefPtr<ServiceWorkerManagerParent> parent = iter.Get()->GetKey(); + MOZ_ASSERT(parent); + + if (parent->ID() != aParentID) { + nsCString host(aHost); + Unused << parent->SendNotifyRemove(host); +#ifdef DEBUG + } else { + parentFound = true; +#endif + } + } + +#ifdef DEBUG + MOZ_ASSERT(parentFound); +#endif +} + +void +ServiceWorkerManagerService::PropagateRemoveAll(uint64_t aParentID) +{ + AssertIsOnBackgroundThread(); + + RefPtr<dom::ServiceWorkerRegistrar> service = + dom::ServiceWorkerRegistrar::Get(); + MOZ_ASSERT(service); + + service->RemoveAll(); + + DebugOnly<bool> parentFound = false; + for (auto iter = mAgents.Iter(); !iter.Done(); iter.Next()) { + RefPtr<ServiceWorkerManagerParent> parent = iter.Get()->GetKey(); + MOZ_ASSERT(parent); + + if (parent->ID() != aParentID) { + Unused << parent->SendNotifyRemoveAll(); +#ifdef DEBUG + } else { + parentFound = true; +#endif + } + } + +#ifdef DEBUG + MOZ_ASSERT(parentFound); +#endif +} + +} // namespace workers +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/ServiceWorkerManagerService.h b/dom/workers/ServiceWorkerManagerService.h new file mode 100644 index 000000000..3f3f760e4 --- /dev/null +++ b/dom/workers/ServiceWorkerManagerService.h @@ -0,0 +1,67 @@ +/* -*- 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_ServiceWorkerManagerService_h +#define mozilla_dom_ServiceWorkerManagerService_h + +#include "nsISupportsImpl.h" +#include "nsHashKeys.h" +#include "nsTHashtable.h" + +namespace mozilla { + +class PrincipalOriginAttributes; + +namespace ipc { +class PrincipalInfo; +} // namespace ipc + +namespace dom { + +class ServiceWorkerRegistrationData; + +namespace workers { + +class ServiceWorkerManagerParent; + +class ServiceWorkerManagerService final +{ +public: + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerManagerService) + + static already_AddRefed<ServiceWorkerManagerService> Get(); + static already_AddRefed<ServiceWorkerManagerService> GetOrCreate(); + + void RegisterActor(ServiceWorkerManagerParent* aParent); + void UnregisterActor(ServiceWorkerManagerParent* aParent); + + void PropagateRegistration(uint64_t aParentID, + ServiceWorkerRegistrationData& aData); + + void PropagateSoftUpdate(uint64_t aParentID, + const PrincipalOriginAttributes& aOriginAttributes, + const nsAString& aScope); + + void PropagateUnregister(uint64_t aParentID, + const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + const nsAString& aScope); + + void PropagateRemove(uint64_t aParentID, const nsACString& aHost); + + void PropagateRemoveAll(uint64_t aParentID); + +private: + ServiceWorkerManagerService(); + ~ServiceWorkerManagerService(); + + nsTHashtable<nsPtrHashKey<ServiceWorkerManagerParent>> mAgents; +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ServiceWorkerManagerService_h diff --git a/dom/workers/ServiceWorkerPrivate.cpp b/dom/workers/ServiceWorkerPrivate.cpp new file mode 100644 index 000000000..eaa548f95 --- /dev/null +++ b/dom/workers/ServiceWorkerPrivate.cpp @@ -0,0 +1,2088 @@ +/* -*- 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 "ServiceWorkerPrivate.h" + +#include "ServiceWorkerManager.h" +#include "nsContentUtils.h" +#include "nsIHttpChannelInternal.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsINetworkInterceptController.h" +#include "nsIPushErrorReporter.h" +#include "nsISupportsImpl.h" +#include "nsIUploadChannel2.h" +#include "nsNetUtil.h" +#include "nsProxyRelease.h" +#include "nsQueryObject.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" +#include "WorkerRunnable.h" +#include "WorkerScope.h" +#include "mozilla/Assertions.h" +#include "mozilla/dom/FetchUtil.h" +#include "mozilla/dom/IndexedDatabaseManager.h" +#include "mozilla/dom/InternalHeaders.h" +#include "mozilla/dom/NotificationEvent.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/PushEventBinding.h" +#include "mozilla/dom/RequestBinding.h" +#include "mozilla/Unused.h" + +using namespace mozilla; +using namespace mozilla::dom; + +BEGIN_WORKERS_NAMESPACE + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ServiceWorkerPrivate) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ServiceWorkerPrivate) +NS_IMPL_CYCLE_COLLECTION(ServiceWorkerPrivate, mSupportsArray) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ServiceWorkerPrivate) + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) +NS_INTERFACE_MAP_END + +// Tracks the "dom.disable_open_click_delay" preference. Modified on main +// thread, read on worker threads. +// It is updated every time a "notificationclick" event is dispatched. While +// this is done without synchronization, at the worst, the thread will just get +// an older value within which a popup is allowed to be displayed, which will +// still be a valid value since it was set prior to dispatching the runnable. +Atomic<uint32_t> gDOMDisableOpenClickDelay(0); + +// Used to keep track of pending waitUntil as well as in-flight extendable events. +// When the last token is released, we attempt to terminate the worker. +class KeepAliveToken final : public nsISupports +{ +public: + NS_DECL_ISUPPORTS + + explicit KeepAliveToken(ServiceWorkerPrivate* aPrivate) + : mPrivate(aPrivate) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aPrivate); + mPrivate->AddToken(); + } + +private: + ~KeepAliveToken() + { + AssertIsOnMainThread(); + mPrivate->ReleaseToken(); + } + + RefPtr<ServiceWorkerPrivate> mPrivate; +}; + +NS_IMPL_ISUPPORTS0(KeepAliveToken) + +ServiceWorkerPrivate::ServiceWorkerPrivate(ServiceWorkerInfo* aInfo) + : mInfo(aInfo) + , mDebuggerCount(0) + , mTokenCount(0) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aInfo); + + mIdleWorkerTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + MOZ_ASSERT(mIdleWorkerTimer); +} + +ServiceWorkerPrivate::~ServiceWorkerPrivate() +{ + MOZ_ASSERT(!mWorkerPrivate); + MOZ_ASSERT(!mTokenCount); + MOZ_ASSERT(!mInfo); + MOZ_ASSERT(mSupportsArray.IsEmpty()); + + mIdleWorkerTimer->Cancel(); +} + +namespace { + +class MessageWaitUntilHandler final : public PromiseNativeHandler +{ + nsMainThreadPtrHandle<nsISupports> mKeepAliveToken; + + ~MessageWaitUntilHandler() + { + } + +public: + explicit MessageWaitUntilHandler(const nsMainThreadPtrHandle<nsISupports>& aKeepAliveToken) + : mKeepAliveToken(aKeepAliveToken) + { + MOZ_ASSERT(mKeepAliveToken); + } + + void + ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + mKeepAliveToken = nullptr; + } + + void + RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + mKeepAliveToken = nullptr; + } + + NS_DECL_THREADSAFE_ISUPPORTS +}; + +NS_IMPL_ISUPPORTS0(MessageWaitUntilHandler) + +} // anonymous namespace + +nsresult +ServiceWorkerPrivate::SendMessageEvent(JSContext* aCx, + JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + UniquePtr<ServiceWorkerClientInfo>&& aClientInfo) +{ + ErrorResult rv(SpawnWorkerIfNeeded(MessageEvent, nullptr)); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + nsMainThreadPtrHandle<nsISupports> token( + new nsMainThreadPtrHolder<nsISupports>(CreateEventKeepAliveToken())); + + RefPtr<PromiseNativeHandler> handler = new MessageWaitUntilHandler(token); + + mWorkerPrivate->PostMessageToServiceWorker(aCx, aMessage, aTransferable, + Move(aClientInfo), handler, + rv); + return rv.StealNSResult(); +} + +namespace { + +class CheckScriptEvaluationWithCallback final : public WorkerRunnable +{ + nsMainThreadPtrHandle<KeepAliveToken> mKeepAliveToken; + RefPtr<LifeCycleEventCallback> mCallback; +#ifdef DEBUG + bool mDone; +#endif + +public: + CheckScriptEvaluationWithCallback(WorkerPrivate* aWorkerPrivate, + KeepAliveToken* aKeepAliveToken, + LifeCycleEventCallback* aCallback) + : WorkerRunnable(aWorkerPrivate) + , mKeepAliveToken(new nsMainThreadPtrHolder<KeepAliveToken>(aKeepAliveToken)) + , mCallback(aCallback) +#ifdef DEBUG + , mDone(false) +#endif + { + AssertIsOnMainThread(); + } + + ~CheckScriptEvaluationWithCallback() + { + MOZ_ASSERT(mDone); + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + aWorkerPrivate->AssertIsOnWorkerThread(); + Done(aWorkerPrivate->WorkerScriptExecutedSuccessfully()); + + return true; + } + + nsresult + Cancel() override + { + Done(false); + return WorkerRunnable::Cancel(); + } + +private: + void + Done(bool aResult) + { +#ifdef DEBUG + mDone = true; +#endif + mCallback->SetResult(aResult); + MOZ_ALWAYS_SUCCEEDS(mWorkerPrivate->DispatchToMainThread(mCallback)); + } +}; + +} // anonymous namespace + +nsresult +ServiceWorkerPrivate::CheckScriptEvaluation(LifeCycleEventCallback* aCallback) +{ + nsresult rv = SpawnWorkerIfNeeded(LifeCycleEvent, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<KeepAliveToken> token = CreateEventKeepAliveToken(); + RefPtr<WorkerRunnable> r = new CheckScriptEvaluationWithCallback(mWorkerPrivate, + token, + aCallback); + if (NS_WARN_IF(!r->Dispatch())) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +namespace { + +// Holds the worker alive until the waitUntil promise is resolved or +// rejected. +class KeepAliveHandler final +{ + // Use an internal class to listen for the promise resolve/reject + // callbacks. This class also registers a feature so that it can + // preemptively cleanup if the service worker is timed out and + // terminated. + class InternalHandler final : public PromiseNativeHandler + , public WorkerHolder + { + nsMainThreadPtrHandle<KeepAliveToken> mKeepAliveToken; + + // Worker thread only + WorkerPrivate* mWorkerPrivate; + RefPtr<Promise> mPromise; + bool mWorkerHolderAdded; + + ~InternalHandler() + { + MaybeCleanup(); + } + + bool + UseWorkerHolder() + { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(!mWorkerHolderAdded); + mWorkerHolderAdded = HoldWorker(mWorkerPrivate, Terminating); + return mWorkerHolderAdded; + } + + void + MaybeCleanup() + { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + if (!mPromise) { + return; + } + if (mWorkerHolderAdded) { + ReleaseWorker(); + } + mPromise = nullptr; + mKeepAliveToken = nullptr; + } + + void + ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + MaybeCleanup(); + } + + void + RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + MaybeCleanup(); + } + + bool + Notify(Status aStatus) override + { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + if (aStatus < Terminating) { + return true; + } + MaybeCleanup(); + return true; + } + + InternalHandler(const nsMainThreadPtrHandle<KeepAliveToken>& aKeepAliveToken, + WorkerPrivate* aWorkerPrivate, + Promise* aPromise) + : mKeepAliveToken(aKeepAliveToken) + , mWorkerPrivate(aWorkerPrivate) + , mPromise(aPromise) + , mWorkerHolderAdded(false) + { + MOZ_ASSERT(mKeepAliveToken); + MOZ_ASSERT(mWorkerPrivate); + MOZ_ASSERT(mPromise); + } + + public: + static already_AddRefed<InternalHandler> + Create(const nsMainThreadPtrHandle<KeepAliveToken>& aKeepAliveToken, + WorkerPrivate* aWorkerPrivate, + Promise* aPromise) + { + RefPtr<InternalHandler> ref = new InternalHandler(aKeepAliveToken, + aWorkerPrivate, + aPromise); + + if (NS_WARN_IF(!ref->UseWorkerHolder())) { + return nullptr; + } + + return ref.forget(); + } + + NS_DECL_ISUPPORTS + }; + + // This is really just a wrapper class to keep the InternalHandler + // private. We don't want any code to accidentally call + // Promise::AppendNativeHandler() without also referencing the promise. + // Therefore we force all code through the static CreateAndAttachToPromise() + // and use the private InternalHandler object. + KeepAliveHandler() = delete; + ~KeepAliveHandler() = delete; + +public: + // Create a private handler object and attach it to the given Promise. + // This will also create a strong ref to the Promise in a ref cycle. The + // ref cycle is broken when the Promise is fulfilled or the worker thread + // is Terminated. + static void + CreateAndAttachToPromise(const nsMainThreadPtrHandle<KeepAliveToken>& aKeepAliveToken, + Promise* aPromise) + { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + workerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aKeepAliveToken); + MOZ_ASSERT(aPromise); + + // This creates a strong ref to the promise. + RefPtr<InternalHandler> handler = InternalHandler::Create(aKeepAliveToken, + workerPrivate, + aPromise); + if (NS_WARN_IF(!handler)) { + return; + } + + // This then creates a strong ref cycle between the promise and the + // handler. The cycle is broken when the Promise is fulfilled or + // the worker thread is Terminated. + aPromise->AppendNativeHandler(handler); + } +}; + +NS_IMPL_ISUPPORTS0(KeepAliveHandler::InternalHandler) + +class RegistrationUpdateRunnable : public Runnable +{ + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration; + const bool mNeedTimeCheck; + +public: + RegistrationUpdateRunnable(nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration, + bool aNeedTimeCheck) + : mRegistration(aRegistration) + , mNeedTimeCheck(aNeedTimeCheck) + { + MOZ_DIAGNOSTIC_ASSERT(mRegistration); + } + + NS_IMETHOD + Run() override + { + if (mNeedTimeCheck) { + mRegistration->MaybeScheduleTimeCheckAndUpdate(); + } else { + mRegistration->MaybeScheduleUpdate(); + } + return NS_OK; + } +}; + +class ExtendableEventWorkerRunnable : public WorkerRunnable +{ +protected: + nsMainThreadPtrHandle<KeepAliveToken> mKeepAliveToken; + +public: + ExtendableEventWorkerRunnable(WorkerPrivate* aWorkerPrivate, + KeepAliveToken* aKeepAliveToken) + : WorkerRunnable(aWorkerPrivate) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aKeepAliveToken); + + mKeepAliveToken = + new nsMainThreadPtrHolder<KeepAliveToken>(aKeepAliveToken); + } + + bool + DispatchExtendableEventOnWorkerScope(JSContext* aCx, + WorkerGlobalScope* aWorkerScope, + ExtendableEvent* aEvent, + PromiseNativeHandler* aPromiseHandler) + { + MOZ_ASSERT(aWorkerScope); + MOZ_ASSERT(aEvent); + nsCOMPtr<nsIGlobalObject> sgo = aWorkerScope; + WidgetEvent* internalEvent = aEvent->WidgetEventPtr(); + + ErrorResult result; + result = aWorkerScope->DispatchDOMEvent(nullptr, aEvent, nullptr, nullptr); + if (NS_WARN_IF(result.Failed()) || internalEvent->mFlags.mExceptionWasRaised) { + result.SuppressException(); + return false; + } + + RefPtr<Promise> waitUntilPromise = aEvent->GetPromise(); + if (!waitUntilPromise) { + waitUntilPromise = + Promise::Resolve(sgo, aCx, JS::UndefinedHandleValue, result); + MOZ_RELEASE_ASSERT(!result.Failed()); + } + + MOZ_ASSERT(waitUntilPromise); + + // Make sure to append the caller's promise handler before attaching + // our keep alive handler. This can avoid terminating the worker + // before a success result is delivered to the caller in cases where + // the idle timeout has been set to zero. This low timeout value is + // sometimes set in tests. + if (aPromiseHandler) { + waitUntilPromise->AppendNativeHandler(aPromiseHandler); + } + + KeepAliveHandler::CreateAndAttachToPromise(mKeepAliveToken, + waitUntilPromise); + + return true; + } +}; + +// Handle functional event +// 9.9.7 If the time difference in seconds calculated by the current time minus +// registration's last update check time is greater than 86400, invoke Soft Update +// algorithm. +class ExtendableFunctionalEventWorkerRunnable : public ExtendableEventWorkerRunnable +{ +protected: + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration; +public: + ExtendableFunctionalEventWorkerRunnable(WorkerPrivate* aWorkerPrivate, + KeepAliveToken* aKeepAliveToken, + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration) + : ExtendableEventWorkerRunnable(aWorkerPrivate, aKeepAliveToken) + , mRegistration(aRegistration) + { + MOZ_DIAGNOSTIC_ASSERT(aRegistration); + } + + void + PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aRunResult) + { + // Sub-class PreRun() or WorkerRun() methods could clear our mRegistration. + if (mRegistration) { + nsCOMPtr<nsIRunnable> runnable = + new RegistrationUpdateRunnable(mRegistration, true /* time check */); + aWorkerPrivate->DispatchToMainThread(runnable.forget()); + } + + ExtendableEventWorkerRunnable::PostRun(aCx, aWorkerPrivate, aRunResult); + } +}; + +/* + * Fires 'install' event on the ServiceWorkerGlobalScope. Modifies busy count + * since it fires the event. This is ok since there can't be nested + * ServiceWorkers, so the parent thread -> worker thread requirement for + * runnables is satisfied. + */ +class LifecycleEventWorkerRunnable : public ExtendableEventWorkerRunnable +{ + nsString mEventName; + RefPtr<LifeCycleEventCallback> mCallback; + +public: + LifecycleEventWorkerRunnable(WorkerPrivate* aWorkerPrivate, + KeepAliveToken* aToken, + const nsAString& aEventName, + LifeCycleEventCallback* aCallback) + : ExtendableEventWorkerRunnable(aWorkerPrivate, aToken) + , mEventName(aEventName) + , mCallback(aCallback) + { + AssertIsOnMainThread(); + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + MOZ_ASSERT(aWorkerPrivate); + return DispatchLifecycleEvent(aCx, aWorkerPrivate); + } + + nsresult + Cancel() override + { + mCallback->SetResult(false); + MOZ_ALWAYS_SUCCEEDS(mWorkerPrivate->DispatchToMainThread(mCallback)); + + return WorkerRunnable::Cancel(); + } + +private: + bool + DispatchLifecycleEvent(JSContext* aCx, WorkerPrivate* aWorkerPrivate); + +}; + +/* + * Used to handle ExtendableEvent::waitUntil() and catch abnormal worker + * termination during the execution of life cycle events. It is responsible + * with advancing the job queue for install/activate tasks. + */ +class LifeCycleEventWatcher final : public PromiseNativeHandler, + public WorkerHolder +{ + WorkerPrivate* mWorkerPrivate; + RefPtr<LifeCycleEventCallback> mCallback; + bool mDone; + + ~LifeCycleEventWatcher() + { + if (mDone) { + return; + } + + MOZ_ASSERT(GetCurrentThreadWorkerPrivate() == mWorkerPrivate); + // XXXcatalinb: If all the promises passed to waitUntil go out of scope, + // the resulting Promise.all will be cycle collected and it will drop its + // native handlers (including this object). Instead of waiting for a timeout + // we report the failure now. + ReportResult(false); + } + +public: + NS_DECL_ISUPPORTS + + LifeCycleEventWatcher(WorkerPrivate* aWorkerPrivate, + LifeCycleEventCallback* aCallback) + : mWorkerPrivate(aWorkerPrivate) + , mCallback(aCallback) + , mDone(false) + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + bool + Init() + { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + + // We need to listen for worker termination in case the event handler + // never completes or never resolves the waitUntil promise. There are + // two possible scenarios: + // 1. The keepAlive token expires and the worker is terminated, in which + // case the registration/update promise will be rejected + // 2. A new service worker is registered which will terminate the current + // installing worker. + if (NS_WARN_IF(!HoldWorker(mWorkerPrivate, Terminating))) { + NS_WARNING("LifeCycleEventWatcher failed to add feature."); + ReportResult(false); + return false; + } + + return true; + } + + bool + Notify(Status aStatus) override + { + if (aStatus < Terminating) { + return true; + } + + MOZ_ASSERT(GetCurrentThreadWorkerPrivate() == mWorkerPrivate); + ReportResult(false); + + return true; + } + + void + ReportResult(bool aResult) + { + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mDone) { + return; + } + mDone = true; + + mCallback->SetResult(aResult); + nsresult rv = mWorkerPrivate->DispatchToMainThread(mCallback); + if (NS_WARN_IF(NS_FAILED(rv))) { + NS_RUNTIMEABORT("Failed to dispatch life cycle event handler."); + } + + ReleaseWorker(); + } + + void + ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + MOZ_ASSERT(GetCurrentThreadWorkerPrivate() == mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + + ReportResult(true); + } + + void + RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + MOZ_ASSERT(GetCurrentThreadWorkerPrivate() == mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + + ReportResult(false); + + // Note, all WaitUntil() rejections are reported to client consoles + // by the WaitUntilHandler in ServiceWorkerEvents. This ensures that + // errors in non-lifecycle events like FetchEvent and PushEvent are + // reported properly. + } +}; + +NS_IMPL_ISUPPORTS0(LifeCycleEventWatcher) + +bool +LifecycleEventWorkerRunnable::DispatchLifecycleEvent(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) +{ + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + + RefPtr<ExtendableEvent> event; + RefPtr<EventTarget> target = aWorkerPrivate->GlobalScope(); + + if (mEventName.EqualsASCII("install") || mEventName.EqualsASCII("activate")) { + ExtendableEventInit init; + init.mBubbles = false; + init.mCancelable = false; + event = ExtendableEvent::Constructor(target, mEventName, init); + } else { + MOZ_CRASH("Unexpected lifecycle event"); + } + + event->SetTrusted(true); + + // It is important to initialize the watcher before actually dispatching + // the event in order to catch worker termination while the event handler + // is still executing. This can happen with infinite loops, for example. + RefPtr<LifeCycleEventWatcher> watcher = + new LifeCycleEventWatcher(aWorkerPrivate, mCallback); + + if (!watcher->Init()) { + return true; + } + + if (!DispatchExtendableEventOnWorkerScope(aCx, aWorkerPrivate->GlobalScope(), + event, watcher)) { + watcher->ReportResult(false); + } + + return true; +} + +} // anonymous namespace + +nsresult +ServiceWorkerPrivate::SendLifeCycleEvent(const nsAString& aEventType, + LifeCycleEventCallback* aCallback, + nsIRunnable* aLoadFailure) +{ + nsresult rv = SpawnWorkerIfNeeded(LifeCycleEvent, aLoadFailure); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<KeepAliveToken> token = CreateEventKeepAliveToken(); + RefPtr<WorkerRunnable> r = new LifecycleEventWorkerRunnable(mWorkerPrivate, + token, + aEventType, + aCallback); + if (NS_WARN_IF(!r->Dispatch())) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +namespace { + +class PushErrorReporter final : public PromiseNativeHandler +{ + WorkerPrivate* mWorkerPrivate; + nsString mMessageId; + + ~PushErrorReporter() + { + } + +public: + NS_DECL_THREADSAFE_ISUPPORTS + + PushErrorReporter(WorkerPrivate* aWorkerPrivate, + const nsAString& aMessageId) + : mWorkerPrivate(aWorkerPrivate) + , mMessageId(aMessageId) + { + mWorkerPrivate->AssertIsOnWorkerThread(); + } + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + mWorkerPrivate->AssertIsOnWorkerThread(); + mWorkerPrivate = nullptr; + // Do nothing; we only use this to report errors to the Push service. + } + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + Report(nsIPushErrorReporter::DELIVERY_UNHANDLED_REJECTION); + } + + void Report(uint16_t aReason = nsIPushErrorReporter::DELIVERY_INTERNAL_ERROR) + { + WorkerPrivate* workerPrivate = mWorkerPrivate; + mWorkerPrivate->AssertIsOnWorkerThread(); + mWorkerPrivate = nullptr; + + if (NS_WARN_IF(aReason > nsIPushErrorReporter::DELIVERY_INTERNAL_ERROR) || + mMessageId.IsEmpty()) { + return; + } + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod<uint16_t>(this, + &PushErrorReporter::ReportOnMainThread, aReason); + MOZ_ALWAYS_TRUE(NS_SUCCEEDED( + workerPrivate->DispatchToMainThread(runnable.forget()))); + } + + void ReportOnMainThread(uint16_t aReason) + { + AssertIsOnMainThread(); + nsCOMPtr<nsIPushErrorReporter> reporter = + do_GetService("@mozilla.org/push/Service;1"); + if (reporter) { + nsresult rv = reporter->ReportDeliveryError(mMessageId, aReason); + Unused << NS_WARN_IF(NS_FAILED(rv)); + } + } +}; + +NS_IMPL_ISUPPORTS0(PushErrorReporter) + +class SendPushEventRunnable final : public ExtendableFunctionalEventWorkerRunnable +{ + nsString mMessageId; + Maybe<nsTArray<uint8_t>> mData; + +public: + SendPushEventRunnable(WorkerPrivate* aWorkerPrivate, + KeepAliveToken* aKeepAliveToken, + const nsAString& aMessageId, + const Maybe<nsTArray<uint8_t>>& aData, + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> aRegistration) + : ExtendableFunctionalEventWorkerRunnable( + aWorkerPrivate, aKeepAliveToken, aRegistration) + , mMessageId(aMessageId) + , mData(aData) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + MOZ_ASSERT(aWorkerPrivate); + GlobalObject globalObj(aCx, aWorkerPrivate->GlobalScope()->GetWrapper()); + + RefPtr<PushErrorReporter> errorReporter = + new PushErrorReporter(aWorkerPrivate, mMessageId); + + PushEventInit pei; + if (mData) { + const nsTArray<uint8_t>& bytes = mData.ref(); + JSObject* data = Uint8Array::Create(aCx, bytes.Length(), bytes.Elements()); + if (!data) { + errorReporter->Report(); + return false; + } + pei.mData.Construct().SetAsArrayBufferView().Init(data); + } + pei.mBubbles = false; + pei.mCancelable = false; + + ErrorResult result; + RefPtr<PushEvent> event = + PushEvent::Constructor(globalObj, NS_LITERAL_STRING("push"), pei, result); + if (NS_WARN_IF(result.Failed())) { + result.SuppressException(); + errorReporter->Report(); + return false; + } + event->SetTrusted(true); + + if (!DispatchExtendableEventOnWorkerScope(aCx, aWorkerPrivate->GlobalScope(), + event, errorReporter)) { + errorReporter->Report(nsIPushErrorReporter::DELIVERY_UNCAUGHT_EXCEPTION); + } + + return true; + } +}; + +class SendPushSubscriptionChangeEventRunnable final : public ExtendableEventWorkerRunnable +{ + +public: + explicit SendPushSubscriptionChangeEventRunnable( + WorkerPrivate* aWorkerPrivate, KeepAliveToken* aKeepAliveToken) + : ExtendableEventWorkerRunnable(aWorkerPrivate, aKeepAliveToken) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + MOZ_ASSERT(aWorkerPrivate); + + RefPtr<EventTarget> target = aWorkerPrivate->GlobalScope(); + + ExtendableEventInit init; + init.mBubbles = false; + init.mCancelable = false; + + RefPtr<ExtendableEvent> event = + ExtendableEvent::Constructor(target, + NS_LITERAL_STRING("pushsubscriptionchange"), + init); + + event->SetTrusted(true); + + DispatchExtendableEventOnWorkerScope(aCx, aWorkerPrivate->GlobalScope(), + event, nullptr); + + return true; + } +}; + +} // anonymous namespace + +nsresult +ServiceWorkerPrivate::SendPushEvent(const nsAString& aMessageId, + const Maybe<nsTArray<uint8_t>>& aData, + ServiceWorkerRegistrationInfo* aRegistration) +{ + nsresult rv = SpawnWorkerIfNeeded(PushEvent, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<KeepAliveToken> token = CreateEventKeepAliveToken(); + + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> regInfo( + new nsMainThreadPtrHolder<ServiceWorkerRegistrationInfo>(aRegistration, false)); + + RefPtr<WorkerRunnable> r = new SendPushEventRunnable(mWorkerPrivate, + token, + aMessageId, + aData, + regInfo); + + if (mInfo->State() == ServiceWorkerState::Activating) { + mPendingFunctionalEvents.AppendElement(r.forget()); + return NS_OK; + } + + MOZ_ASSERT(mInfo->State() == ServiceWorkerState::Activated); + + if (NS_WARN_IF(!r->Dispatch())) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult +ServiceWorkerPrivate::SendPushSubscriptionChangeEvent() +{ + nsresult rv = SpawnWorkerIfNeeded(PushSubscriptionChangeEvent, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<KeepAliveToken> token = CreateEventKeepAliveToken(); + RefPtr<WorkerRunnable> r = + new SendPushSubscriptionChangeEventRunnable(mWorkerPrivate, token); + if (NS_WARN_IF(!r->Dispatch())) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +namespace { + +static void +DummyNotificationTimerCallback(nsITimer* aTimer, void* aClosure) +{ + // Nothing. +} + +class AllowWindowInteractionHandler; + +class ClearWindowAllowedRunnable final : public WorkerRunnable +{ +public: + ClearWindowAllowedRunnable(WorkerPrivate* aWorkerPrivate, + AllowWindowInteractionHandler* aHandler) + : WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) + , mHandler(aHandler) + { } + +private: + bool + PreDispatch(WorkerPrivate* aWorkerPrivate) override + { + // WorkerRunnable asserts that the dispatch is from parent thread if + // the busy count modification is WorkerThreadUnchangedBusyCount. + // Since this runnable will be dispatched from the timer thread, we override + // PreDispatch and PostDispatch to skip the check. + return true; + } + + void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override + { + // Silence bad assertions. + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override; + + nsresult + Cancel() override + { + // Always ensure the handler is released on the worker thread, even if we + // are cancelled. + mHandler = nullptr; + return WorkerRunnable::Cancel(); + } + + RefPtr<AllowWindowInteractionHandler> mHandler; +}; + +class AllowWindowInteractionHandler final : public PromiseNativeHandler +{ + friend class ClearWindowAllowedRunnable; + nsCOMPtr<nsITimer> mTimer; + + ~AllowWindowInteractionHandler() + { + } + + void + ClearWindowAllowed(WorkerPrivate* aWorkerPrivate) + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + if (!mTimer) { + return; + } + + // XXXcatalinb: This *might* be executed after the global was unrooted, in + // which case GlobalScope() will return null. Making the check here just + // to be safe. + WorkerGlobalScope* globalScope = aWorkerPrivate->GlobalScope(); + if (!globalScope) { + return; + } + + globalScope->ConsumeWindowInteraction(); + mTimer->Cancel(); + mTimer = nullptr; + MOZ_ALWAYS_TRUE(aWorkerPrivate->ModifyBusyCountFromWorker(false)); + } + + void + StartClearWindowTimer(WorkerPrivate* aWorkerPrivate) + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(!mTimer); + + nsresult rv; + nsCOMPtr<nsITimer> timer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + RefPtr<ClearWindowAllowedRunnable> r = + new ClearWindowAllowedRunnable(aWorkerPrivate, this); + + RefPtr<TimerThreadEventTarget> target = + new TimerThreadEventTarget(aWorkerPrivate, r); + + rv = timer->SetTarget(target); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // The important stuff that *has* to be reversed. + if (NS_WARN_IF(!aWorkerPrivate->ModifyBusyCountFromWorker(true))) { + return; + } + aWorkerPrivate->GlobalScope()->AllowWindowInteraction(); + timer.swap(mTimer); + + // We swap first and then initialize the timer so that even if initializing + // fails, we still clean the busy count and interaction count correctly. + // The timer can't be initialized before modifying the busy count since the + // timer thread could run and call the timeout but the worker may + // already be terminating and modifying the busy count could fail. + rv = mTimer->InitWithFuncCallback(DummyNotificationTimerCallback, nullptr, + gDOMDisableOpenClickDelay, + nsITimer::TYPE_ONE_SHOT); + if (NS_WARN_IF(NS_FAILED(rv))) { + ClearWindowAllowed(aWorkerPrivate); + return; + } + } + +public: + NS_DECL_ISUPPORTS + + explicit AllowWindowInteractionHandler(WorkerPrivate* aWorkerPrivate) + { + StartClearWindowTimer(aWorkerPrivate); + } + + void + ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx); + ClearWindowAllowed(workerPrivate); + } + + void + RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override + { + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx); + ClearWindowAllowed(workerPrivate); + } +}; + +NS_IMPL_ISUPPORTS0(AllowWindowInteractionHandler) + +bool +ClearWindowAllowedRunnable::WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) +{ + mHandler->ClearWindowAllowed(aWorkerPrivate); + mHandler = nullptr; + return true; +} + +class SendNotificationEventRunnable final : public ExtendableEventWorkerRunnable +{ + const nsString mEventName; + const nsString mID; + const nsString mTitle; + const nsString mDir; + const nsString mLang; + const nsString mBody; + const nsString mTag; + const nsString mIcon; + const nsString mData; + const nsString mBehavior; + const nsString mScope; + +public: + SendNotificationEventRunnable(WorkerPrivate* aWorkerPrivate, + KeepAliveToken* aKeepAliveToken, + const nsAString& aEventName, + const nsAString& aID, + const nsAString& aTitle, + const nsAString& aDir, + const nsAString& aLang, + const nsAString& aBody, + const nsAString& aTag, + const nsAString& aIcon, + const nsAString& aData, + const nsAString& aBehavior, + const nsAString& aScope) + : ExtendableEventWorkerRunnable(aWorkerPrivate, aKeepAliveToken) + , mEventName(aEventName) + , mID(aID) + , mTitle(aTitle) + , mDir(aDir) + , mLang(aLang) + , mBody(aBody) + , mTag(aTag) + , mIcon(aIcon) + , mData(aData) + , mBehavior(aBehavior) + , mScope(aScope) + { + AssertIsOnMainThread(); + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + MOZ_ASSERT(aWorkerPrivate); + + RefPtr<EventTarget> target = do_QueryObject(aWorkerPrivate->GlobalScope()); + + ErrorResult result; + RefPtr<Notification> notification = + Notification::ConstructFromFields(aWorkerPrivate->GlobalScope(), mID, + mTitle, mDir, mLang, mBody, mTag, mIcon, + mData, mScope, result); + if (NS_WARN_IF(result.Failed())) { + return false; + } + + NotificationEventInit nei; + nei.mNotification = notification; + nei.mBubbles = false; + nei.mCancelable = false; + + RefPtr<NotificationEvent> event = + NotificationEvent::Constructor(target, mEventName, + nei, result); + if (NS_WARN_IF(result.Failed())) { + return false; + } + + event->SetTrusted(true); + aWorkerPrivate->GlobalScope()->AllowWindowInteraction(); + RefPtr<AllowWindowInteractionHandler> allowWindowInteraction = + new AllowWindowInteractionHandler(aWorkerPrivate); + if (!DispatchExtendableEventOnWorkerScope(aCx, aWorkerPrivate->GlobalScope(), + event, allowWindowInteraction)) { + allowWindowInteraction->RejectedCallback(aCx, JS::UndefinedHandleValue); + } + aWorkerPrivate->GlobalScope()->ConsumeWindowInteraction(); + + return true; + } +}; + +} // namespace anonymous + +nsresult +ServiceWorkerPrivate::SendNotificationEvent(const nsAString& aEventName, + const nsAString& aID, + const nsAString& aTitle, + const nsAString& aDir, + const nsAString& aLang, + const nsAString& aBody, + const nsAString& aTag, + const nsAString& aIcon, + const nsAString& aData, + const nsAString& aBehavior, + const nsAString& aScope) +{ + WakeUpReason why; + if (aEventName.EqualsLiteral(NOTIFICATION_CLICK_EVENT_NAME)) { + why = NotificationClickEvent; + gDOMDisableOpenClickDelay = Preferences::GetInt("dom.disable_open_click_delay"); + } else if (aEventName.EqualsLiteral(NOTIFICATION_CLOSE_EVENT_NAME)) { + why = NotificationCloseEvent; + } else { + MOZ_ASSERT_UNREACHABLE("Invalid notification event name"); + return NS_ERROR_FAILURE; + } + + nsresult rv = SpawnWorkerIfNeeded(why, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<KeepAliveToken> token = CreateEventKeepAliveToken(); + + RefPtr<WorkerRunnable> r = + new SendNotificationEventRunnable(mWorkerPrivate, token, + aEventName, aID, aTitle, aDir, aLang, + aBody, aTag, aIcon, aData, aBehavior, + aScope); + if (NS_WARN_IF(!r->Dispatch())) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +namespace { + +// Inheriting ExtendableEventWorkerRunnable so that the worker is not terminated +// while handling the fetch event, though that's very unlikely. +class FetchEventRunnable : public ExtendableFunctionalEventWorkerRunnable + , public nsIHttpHeaderVisitor { + nsMainThreadPtrHandle<nsIInterceptedChannel> mInterceptedChannel; + const nsCString mScriptSpec; + nsTArray<nsCString> mHeaderNames; + nsTArray<nsCString> mHeaderValues; + nsCString mSpec; + nsCString mFragment; + nsCString mMethod; + nsString mClientId; + bool mIsReload; + RequestCache mCacheMode; + RequestMode mRequestMode; + RequestRedirect mRequestRedirect; + RequestCredentials mRequestCredentials; + nsContentPolicyType mContentPolicyType; + nsCOMPtr<nsIInputStream> mUploadStream; + nsCString mReferrer; + ReferrerPolicy mReferrerPolicy; + nsString mIntegrity; +public: + FetchEventRunnable(WorkerPrivate* aWorkerPrivate, + KeepAliveToken* aKeepAliveToken, + nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel, + // CSP checks might require the worker script spec + // later on. + const nsACString& aScriptSpec, + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration, + const nsAString& aDocumentId, + bool aIsReload) + : ExtendableFunctionalEventWorkerRunnable( + aWorkerPrivate, aKeepAliveToken, aRegistration) + , mInterceptedChannel(aChannel) + , mScriptSpec(aScriptSpec) + , mClientId(aDocumentId) + , mIsReload(aIsReload) + , mCacheMode(RequestCache::Default) + , mRequestMode(RequestMode::No_cors) + , mRequestRedirect(RequestRedirect::Follow) + // By default we set it to same-origin since normal HTTP fetches always + // send credentials to same-origin websites unless explicitly forbidden. + , mRequestCredentials(RequestCredentials::Same_origin) + , mContentPolicyType(nsIContentPolicy::TYPE_INVALID) + , mReferrer(kFETCH_CLIENT_REFERRER_STR) + , mReferrerPolicy(ReferrerPolicy::_empty) + { + MOZ_ASSERT(aWorkerPrivate); + } + + NS_DECL_ISUPPORTS_INHERITED + + NS_IMETHOD + VisitHeader(const nsACString& aHeader, const nsACString& aValue) override + { + mHeaderNames.AppendElement(aHeader); + mHeaderValues.AppendElement(aValue); + return NS_OK; + } + + nsresult + Init() + { + AssertIsOnMainThread(); + nsCOMPtr<nsIChannel> channel; + nsresult rv = mInterceptedChannel->GetChannel(getter_AddRefs(channel)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> uri; + rv = mInterceptedChannel->GetSecureUpgradedChannelURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + // Normally we rely on the Request constructor to strip the fragment, but + // when creating the FetchEvent we bypass the constructor. So strip the + // fragment manually here instead. We can't do it later when we create + // the Request because that code executes off the main thread. + nsCOMPtr<nsIURI> uriNoFragment; + rv = uri->CloneIgnoringRef(getter_AddRefs(uriNoFragment)); + NS_ENSURE_SUCCESS(rv, rv); + rv = uriNoFragment->GetSpec(mSpec); + NS_ENSURE_SUCCESS(rv, rv); + rv = uri->GetRef(mFragment); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t loadFlags; + rv = channel->GetLoadFlags(&loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsILoadInfo> loadInfo; + rv = channel->GetLoadInfo(getter_AddRefs(loadInfo)); + NS_ENSURE_SUCCESS(rv, rv); + mContentPolicyType = loadInfo->InternalContentPolicyType(); + + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel); + MOZ_ASSERT(httpChannel, "How come we don't have an HTTP channel?"); + + nsAutoCString referrer; + // Ignore the return value since the Referer header may not exist. + httpChannel->GetRequestHeader(NS_LITERAL_CSTRING("Referer"), referrer); + if (!referrer.IsEmpty()) { + mReferrer = referrer; + } + + uint32_t referrerPolicy = 0; + rv = httpChannel->GetReferrerPolicy(&referrerPolicy); + NS_ENSURE_SUCCESS(rv, rv); + switch (referrerPolicy) { + case nsIHttpChannel::REFERRER_POLICY_NO_REFERRER: + mReferrerPolicy = ReferrerPolicy::No_referrer; + break; + case nsIHttpChannel::REFERRER_POLICY_ORIGIN: + mReferrerPolicy = ReferrerPolicy::Origin; + break; + case nsIHttpChannel::REFERRER_POLICY_NO_REFERRER_WHEN_DOWNGRADE: + mReferrerPolicy = ReferrerPolicy::No_referrer_when_downgrade; + break; + case nsIHttpChannel::REFERRER_POLICY_ORIGIN_WHEN_XORIGIN: + mReferrerPolicy = ReferrerPolicy::Origin_when_cross_origin; + break; + case nsIHttpChannel::REFERRER_POLICY_UNSAFE_URL: + mReferrerPolicy = ReferrerPolicy::Unsafe_url; + break; + default: + MOZ_ASSERT_UNREACHABLE("Invalid Referrer Policy enum value?"); + break; + } + + rv = httpChannel->GetRequestMethod(mMethod); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIHttpChannelInternal> internalChannel = do_QueryInterface(httpChannel); + NS_ENSURE_TRUE(internalChannel, NS_ERROR_NOT_AVAILABLE); + + mRequestMode = InternalRequest::MapChannelToRequestMode(channel); + + // This is safe due to static_asserts in ServiceWorkerManager.cpp. + uint32_t redirectMode; + internalChannel->GetRedirectMode(&redirectMode); + mRequestRedirect = static_cast<RequestRedirect>(redirectMode); + + // This is safe due to static_asserts in ServiceWorkerManager.cpp. + uint32_t cacheMode; + internalChannel->GetFetchCacheMode(&cacheMode); + mCacheMode = static_cast<RequestCache>(cacheMode); + + internalChannel->GetIntegrityMetadata(mIntegrity); + + mRequestCredentials = InternalRequest::MapChannelToRequestCredentials(channel); + + rv = httpChannel->VisitNonDefaultRequestHeaders(this); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIUploadChannel2> uploadChannel = do_QueryInterface(httpChannel); + if (uploadChannel) { + MOZ_ASSERT(!mUploadStream); + bool bodyHasHeaders = false; + rv = uploadChannel->GetUploadStreamHasHeaders(&bodyHasHeaders); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIInputStream> uploadStream; + rv = uploadChannel->CloneUploadStream(getter_AddRefs(uploadStream)); + NS_ENSURE_SUCCESS(rv, rv); + if (bodyHasHeaders) { + HandleBodyWithHeaders(uploadStream); + } else { + mUploadStream = uploadStream; + } + } + + return NS_OK; + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + MOZ_ASSERT(aWorkerPrivate); + return DispatchFetchEvent(aCx, aWorkerPrivate); + } + + nsresult + Cancel() override + { + nsCOMPtr<nsIRunnable> runnable = new ResumeRequest(mInterceptedChannel); + if (NS_FAILED(mWorkerPrivate->DispatchToMainThread(runnable))) { + NS_WARNING("Failed to resume channel on FetchEventRunnable::Cancel()!\n"); + } + WorkerRunnable::Cancel(); + return NS_OK; + } + +private: + ~FetchEventRunnable() {} + + class ResumeRequest final : public Runnable { + nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel; + public: + explicit ResumeRequest(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel) + : mChannel(aChannel) + { + } + + NS_IMETHOD Run() override + { + AssertIsOnMainThread(); + nsresult rv = mChannel->ResetInterception(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "Failed to resume intercepted network request"); + return rv; + } + }; + + bool + DispatchFetchEvent(JSContext* aCx, WorkerPrivate* aWorkerPrivate) + { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + GlobalObject globalObj(aCx, aWorkerPrivate->GlobalScope()->GetWrapper()); + + RefPtr<InternalHeaders> internalHeaders = new InternalHeaders(HeadersGuardEnum::Request); + MOZ_ASSERT(mHeaderNames.Length() == mHeaderValues.Length()); + for (uint32_t i = 0; i < mHeaderNames.Length(); i++) { + ErrorResult result; + internalHeaders->Set(mHeaderNames[i], mHeaderValues[i], result); + if (NS_WARN_IF(result.Failed())) { + result.SuppressException(); + return false; + } + } + + ErrorResult result; + internalHeaders->SetGuard(HeadersGuardEnum::Immutable, result); + if (NS_WARN_IF(result.Failed())) { + result.SuppressException(); + return false; + } + RefPtr<InternalRequest> internalReq = new InternalRequest(mSpec, + mFragment, + mMethod, + internalHeaders.forget(), + mCacheMode, + mRequestMode, + mRequestRedirect, + mRequestCredentials, + NS_ConvertUTF8toUTF16(mReferrer), + mReferrerPolicy, + mContentPolicyType, + mIntegrity); + internalReq->SetBody(mUploadStream); + // For Telemetry, note that this Request object was created by a Fetch event. + internalReq->SetCreatedByFetchEvent(); + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(globalObj.GetAsSupports()); + if (NS_WARN_IF(!global)) { + return false; + } + RefPtr<Request> request = new Request(global, internalReq); + + MOZ_ASSERT_IF(internalReq->IsNavigationRequest(), + request->Redirect() == RequestRedirect::Manual); + + RootedDictionary<FetchEventInit> init(aCx); + init.mRequest = request; + init.mBubbles = false; + init.mCancelable = true; + if (!mClientId.IsEmpty()) { + init.mClientId = mClientId; + } + init.mIsReload = mIsReload; + RefPtr<FetchEvent> event = + FetchEvent::Constructor(globalObj, NS_LITERAL_STRING("fetch"), init, result); + if (NS_WARN_IF(result.Failed())) { + result.SuppressException(); + return false; + } + + event->PostInit(mInterceptedChannel, mRegistration, mScriptSpec); + event->SetTrusted(true); + + RefPtr<EventTarget> target = do_QueryObject(aWorkerPrivate->GlobalScope()); + nsresult rv2 = target->DispatchDOMEvent(nullptr, event, nullptr, nullptr); + if (NS_WARN_IF(NS_FAILED(rv2)) || !event->WaitToRespond()) { + nsCOMPtr<nsIRunnable> runnable; + if (event->DefaultPrevented(aCx)) { + event->ReportCanceled(); + } else if (event->WidgetEventPtr()->mFlags.mExceptionWasRaised) { + // Exception logged via the WorkerPrivate ErrorReporter + } else { + runnable = new ResumeRequest(mInterceptedChannel); + } + + if (!runnable) { + runnable = new CancelChannelRunnable(mInterceptedChannel, + mRegistration, + NS_ERROR_INTERCEPTION_FAILED); + } + + MOZ_ALWAYS_SUCCEEDS(mWorkerPrivate->DispatchToMainThread(runnable.forget())); + } + + RefPtr<Promise> waitUntilPromise = event->GetPromise(); + if (waitUntilPromise) { + KeepAliveHandler::CreateAndAttachToPromise(mKeepAliveToken, + waitUntilPromise); + } + + return true; + } + + nsresult + HandleBodyWithHeaders(nsIInputStream* aUploadStream) + { + // We are dealing with an nsMIMEInputStream which uses string input streams + // under the hood, so all of the data is available synchronously. + bool nonBlocking = false; + nsresult rv = aUploadStream->IsNonBlocking(&nonBlocking); + NS_ENSURE_SUCCESS(rv, rv); + if (NS_WARN_IF(!nonBlocking)) { + return NS_ERROR_NOT_AVAILABLE; + } + nsAutoCString body; + rv = NS_ConsumeStream(aUploadStream, UINT32_MAX, body); + NS_ENSURE_SUCCESS(rv, rv); + + // Extract the headers in the beginning of the buffer + nsAutoCString::const_iterator begin, end; + body.BeginReading(begin); + body.EndReading(end); + const nsAutoCString::const_iterator body_end = end; + nsAutoCString headerName, headerValue; + bool emptyHeader = false; + while (FetchUtil::ExtractHeader(begin, end, headerName, + headerValue, &emptyHeader) && + !emptyHeader) { + mHeaderNames.AppendElement(headerName); + mHeaderValues.AppendElement(headerValue); + headerName.Truncate(); + headerValue.Truncate(); + } + + // Replace the upload stream with one only containing the body text. + nsCOMPtr<nsIStringInputStream> strStream = + do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + // Skip past the "\r\n" that separates the headers and the body. + ++begin; + ++begin; + body.Assign(Substring(begin, body_end)); + rv = strStream->SetData(body.BeginReading(), body.Length()); + NS_ENSURE_SUCCESS(rv, rv); + mUploadStream = strStream; + + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS_INHERITED(FetchEventRunnable, WorkerRunnable, nsIHttpHeaderVisitor) + +} // anonymous namespace + +nsresult +ServiceWorkerPrivate::SendFetchEvent(nsIInterceptedChannel* aChannel, + nsILoadGroup* aLoadGroup, + const nsAString& aDocumentId, + bool aIsReload) +{ + AssertIsOnMainThread(); + + // if the ServiceWorker script fails to load for some reason, just resume + // the original channel. + nsCOMPtr<nsIRunnable> failRunnable = + NewRunnableMethod(aChannel, &nsIInterceptedChannel::ResetInterception); + + nsresult rv = SpawnWorkerIfNeeded(FetchEvent, failRunnable, aLoadGroup); + NS_ENSURE_SUCCESS(rv, rv); + + nsMainThreadPtrHandle<nsIInterceptedChannel> handle( + new nsMainThreadPtrHolder<nsIInterceptedChannel>(aChannel, false)); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (NS_WARN_IF(!mInfo || !swm)) { + return NS_ERROR_FAILURE; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + swm->GetRegistration(mInfo->GetPrincipal(), mInfo->Scope()); + + // Its possible the registration is removed between starting the interception + // and actually dispatching the fetch event. In these cases we simply + // want to restart the original network request. Since this is a normal + // condition we handle the reset here instead of returning an error which + // would in turn trigger a console report. + if (!registration) { + aChannel->ResetInterception(); + return NS_OK; + } + + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> regInfo( + new nsMainThreadPtrHolder<ServiceWorkerRegistrationInfo>(registration, false)); + + RefPtr<KeepAliveToken> token = CreateEventKeepAliveToken(); + + RefPtr<FetchEventRunnable> r = + new FetchEventRunnable(mWorkerPrivate, token, handle, + mInfo->ScriptSpec(), regInfo, + aDocumentId, aIsReload); + rv = r->Init(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mInfo->State() == ServiceWorkerState::Activating) { + mPendingFunctionalEvents.AppendElement(r.forget()); + return NS_OK; + } + + MOZ_ASSERT(mInfo->State() == ServiceWorkerState::Activated); + + if (NS_WARN_IF(!r->Dispatch())) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult +ServiceWorkerPrivate::SpawnWorkerIfNeeded(WakeUpReason aWhy, + nsIRunnable* aLoadFailedRunnable, + nsILoadGroup* aLoadGroup) +{ + AssertIsOnMainThread(); + + // XXXcatalinb: We need to have a separate load group that's linked to + // an existing tab child to pass security checks on b2g. + // This should be fixed in bug 1125961, but for now we enforce updating + // the overriden load group when intercepting a fetch. + MOZ_ASSERT_IF(aWhy == FetchEvent, aLoadGroup); + + if (mWorkerPrivate) { + mWorkerPrivate->UpdateOverridenLoadGroup(aLoadGroup); + RenewKeepAliveToken(aWhy); + + return NS_OK; + } + + // Sanity check: mSupportsArray should be empty if we're about to + // spin up a new worker. + MOZ_ASSERT(mSupportsArray.IsEmpty()); + + if (NS_WARN_IF(!mInfo)) { + NS_WARNING("Trying to wake up a dead service worker."); + return NS_ERROR_FAILURE; + } + + // TODO(catalinb): Bug 1192138 - Add telemetry for service worker wake-ups. + + // Ensure that the IndexedDatabaseManager is initialized + Unused << NS_WARN_IF(!IndexedDatabaseManager::GetOrCreate()); + + WorkerLoadInfo info; + nsresult rv = NS_NewURI(getter_AddRefs(info.mBaseURI), mInfo->ScriptSpec(), + nullptr, nullptr); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + info.mResolvedScriptURI = info.mBaseURI; + MOZ_ASSERT(!mInfo->CacheName().IsEmpty()); + info.mServiceWorkerCacheName = mInfo->CacheName(); + info.mServiceWorkerID = mInfo->ID(); + info.mLoadGroup = aLoadGroup; + info.mLoadFailedAsyncRunnable = aLoadFailedRunnable; + + rv = info.mBaseURI->GetHost(info.mDomain); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + info.mPrincipal = mInfo->GetPrincipal(); + + nsContentUtils::StorageAccess access = + nsContentUtils::StorageAllowedForPrincipal(info.mPrincipal); + info.mStorageAllowed = access > nsContentUtils::StorageAccess::ePrivateBrowsing; + info.mOriginAttributes = mInfo->GetOriginAttributes(); + + nsCOMPtr<nsIContentSecurityPolicy> csp; + rv = info.mPrincipal->GetCsp(getter_AddRefs(csp)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + info.mCSP = csp; + if (info.mCSP) { + rv = info.mCSP->GetAllowsEval(&info.mReportCSPViolations, + &info.mEvalAllowed); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + info.mEvalAllowed = true; + info.mReportCSPViolations = false; + } + + WorkerPrivate::OverrideLoadInfoLoadGroup(info); + + AutoJSAPI jsapi; + jsapi.Init(); + ErrorResult error; + NS_ConvertUTF8toUTF16 scriptSpec(mInfo->ScriptSpec()); + + mWorkerPrivate = WorkerPrivate::Constructor(jsapi.cx(), + scriptSpec, + false, WorkerTypeService, + mInfo->Scope(), &info, error); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + + RenewKeepAliveToken(aWhy); + + return NS_OK; +} + +void +ServiceWorkerPrivate::StoreISupports(nsISupports* aSupports) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(mWorkerPrivate); + MOZ_ASSERT(!mSupportsArray.Contains(aSupports)); + + mSupportsArray.AppendElement(aSupports); +} + +void +ServiceWorkerPrivate::RemoveISupports(nsISupports* aSupports) +{ + AssertIsOnMainThread(); + mSupportsArray.RemoveElement(aSupports); +} + +void +ServiceWorkerPrivate::TerminateWorker() +{ + AssertIsOnMainThread(); + + mIdleWorkerTimer->Cancel(); + mIdleKeepAliveToken = nullptr; + if (mWorkerPrivate) { + if (Preferences::GetBool("dom.serviceWorkers.testing.enabled")) { + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + if (os) { + os->NotifyObservers(this, "service-worker-shutdown", nullptr); + } + } + + Unused << NS_WARN_IF(!mWorkerPrivate->Terminate()); + mWorkerPrivate = nullptr; + mSupportsArray.Clear(); + + // Any pending events are never going to fire on this worker. Cancel + // them so that intercepted channels can be reset and other resources + // cleaned up. + nsTArray<RefPtr<WorkerRunnable>> pendingEvents; + mPendingFunctionalEvents.SwapElements(pendingEvents); + for (uint32_t i = 0; i < pendingEvents.Length(); ++i) { + pendingEvents[i]->Cancel(); + } + } +} + +void +ServiceWorkerPrivate::NoteDeadServiceWorkerInfo() +{ + AssertIsOnMainThread(); + mInfo = nullptr; + TerminateWorker(); +} + +void +ServiceWorkerPrivate::Activated() +{ + AssertIsOnMainThread(); + + // If we had to queue up events due to the worker activating, that means + // the worker must be currently running. We should be called synchronously + // when the worker becomes activated. + MOZ_ASSERT_IF(!mPendingFunctionalEvents.IsEmpty(), mWorkerPrivate); + + nsTArray<RefPtr<WorkerRunnable>> pendingEvents; + mPendingFunctionalEvents.SwapElements(pendingEvents); + + for (uint32_t i = 0; i < pendingEvents.Length(); ++i) { + RefPtr<WorkerRunnable> r = pendingEvents[i].forget(); + if (NS_WARN_IF(!r->Dispatch())) { + NS_WARNING("Failed to dispatch pending functional event!"); + } + } +} + +nsresult +ServiceWorkerPrivate::GetDebugger(nsIWorkerDebugger** aResult) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aResult); + + if (!mDebuggerCount) { + return NS_OK; + } + + MOZ_ASSERT(mWorkerPrivate); + + nsCOMPtr<nsIWorkerDebugger> debugger = do_QueryInterface(mWorkerPrivate->Debugger()); + debugger.forget(aResult); + + return NS_OK; +} + +nsresult +ServiceWorkerPrivate::AttachDebugger() +{ + AssertIsOnMainThread(); + + // When the first debugger attaches to a worker, we spawn a worker if needed, + // and cancel the idle timeout. The idle timeout should not be reset until + // the last debugger detached from the worker. + if (!mDebuggerCount) { + nsresult rv = SpawnWorkerIfNeeded(AttachEvent, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + mIdleWorkerTimer->Cancel(); + } + + ++mDebuggerCount; + + return NS_OK; +} + +nsresult +ServiceWorkerPrivate::DetachDebugger() +{ + AssertIsOnMainThread(); + + if (!mDebuggerCount) { + return NS_ERROR_UNEXPECTED; + } + + --mDebuggerCount; + + // When the last debugger detaches from a worker, we either reset the idle + // timeout, or terminate the worker if there are no more active tokens. + if (!mDebuggerCount) { + if (mTokenCount) { + ResetIdleTimeout(); + } else { + TerminateWorker(); + } + } + + return NS_OK; +} + +bool +ServiceWorkerPrivate::IsIdle() const +{ + AssertIsOnMainThread(); + return mTokenCount == 0 || (mTokenCount == 1 && mIdleKeepAliveToken); +} + +namespace { + +class ServiceWorkerPrivateTimerCallback final : public nsITimerCallback +{ +public: + typedef void (ServiceWorkerPrivate::*Method)(nsITimer*); + + ServiceWorkerPrivateTimerCallback(ServiceWorkerPrivate* aServiceWorkerPrivate, + Method aMethod) + : mServiceWorkerPrivate(aServiceWorkerPrivate) + , mMethod(aMethod) + { + } + + NS_IMETHOD + Notify(nsITimer* aTimer) override + { + (mServiceWorkerPrivate->*mMethod)(aTimer); + mServiceWorkerPrivate = nullptr; + return NS_OK; + } + +private: + ~ServiceWorkerPrivateTimerCallback() = default; + + RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate; + Method mMethod; + + NS_DECL_THREADSAFE_ISUPPORTS +}; + +NS_IMPL_ISUPPORTS(ServiceWorkerPrivateTimerCallback, nsITimerCallback); + +} // anonymous namespace + +void +ServiceWorkerPrivate::NoteIdleWorkerCallback(nsITimer* aTimer) +{ + AssertIsOnMainThread(); + + MOZ_ASSERT(aTimer == mIdleWorkerTimer, "Invalid timer!"); + + // Release ServiceWorkerPrivate's token, since the grace period has ended. + mIdleKeepAliveToken = nullptr; + + if (mWorkerPrivate) { + // If we still have a workerPrivate at this point it means there are pending + // waitUntil promises. Wait a bit more until we forcibly terminate the + // worker. + uint32_t timeout = Preferences::GetInt("dom.serviceWorkers.idle_extended_timeout"); + nsCOMPtr<nsITimerCallback> cb = new ServiceWorkerPrivateTimerCallback( + this, &ServiceWorkerPrivate::TerminateWorkerCallback); + DebugOnly<nsresult> rv = + mIdleWorkerTimer->InitWithCallback(cb, timeout, nsITimer::TYPE_ONE_SHOT); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } +} + +void +ServiceWorkerPrivate::TerminateWorkerCallback(nsITimer* aTimer) +{ + AssertIsOnMainThread(); + + MOZ_ASSERT(aTimer == this->mIdleWorkerTimer, "Invalid timer!"); + + // mInfo must be non-null at this point because NoteDeadServiceWorkerInfo + // which zeroes it calls TerminateWorker which cancels our timer which will + // ensure we don't get invoked even if the nsTimerEvent is in the event queue. + ServiceWorkerManager::LocalizeAndReportToAllClients( + mInfo->Scope(), + "ServiceWorkerGraceTimeoutTermination", + nsTArray<nsString> { NS_ConvertUTF8toUTF16(mInfo->Scope()) }); + + TerminateWorker(); +} + +void +ServiceWorkerPrivate::RenewKeepAliveToken(WakeUpReason aWhy) +{ + // We should have an active worker if we're renewing the keep alive token. + MOZ_ASSERT(mWorkerPrivate); + + // If there is at least one debugger attached to the worker, the idle worker + // timeout was canceled when the first debugger attached to the worker. It + // should not be reset until the last debugger detaches from the worker. + if (!mDebuggerCount) { + ResetIdleTimeout(); + } + + if (!mIdleKeepAliveToken) { + mIdleKeepAliveToken = new KeepAliveToken(this); + } +} + +void +ServiceWorkerPrivate::ResetIdleTimeout() +{ + uint32_t timeout = Preferences::GetInt("dom.serviceWorkers.idle_timeout"); + nsCOMPtr<nsITimerCallback> cb = new ServiceWorkerPrivateTimerCallback( + this, &ServiceWorkerPrivate::NoteIdleWorkerCallback); + DebugOnly<nsresult> rv = + mIdleWorkerTimer->InitWithCallback(cb, timeout, nsITimer::TYPE_ONE_SHOT); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +void +ServiceWorkerPrivate::AddToken() +{ + AssertIsOnMainThread(); + ++mTokenCount; +} + +void +ServiceWorkerPrivate::ReleaseToken() +{ + AssertIsOnMainThread(); + + MOZ_ASSERT(mTokenCount > 0); + --mTokenCount; + if (!mTokenCount) { + TerminateWorker(); + } + + // mInfo can be nullptr here if NoteDeadServiceWorkerInfo() is called while + // the KeepAliveToken is being proxy released as a runnable. + else if (mInfo && IsIdle()) { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->WorkerIsIdle(mInfo); + } + } +} + +already_AddRefed<KeepAliveToken> +ServiceWorkerPrivate::CreateEventKeepAliveToken() +{ + AssertIsOnMainThread(); + MOZ_ASSERT(mWorkerPrivate); + MOZ_ASSERT(mIdleKeepAliveToken); + RefPtr<KeepAliveToken> ref = new KeepAliveToken(this); + return ref.forget(); +} + +void +ServiceWorkerPrivate::AddPendingWindow(Runnable* aPendingWindow) +{ + AssertIsOnMainThread(); + pendingWindows.AppendElement(aPendingWindow); +} + +nsresult +ServiceWorkerPrivate::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) +{ + AssertIsOnMainThread(); + + nsCString topic(aTopic); + if (!topic.Equals(NS_LITERAL_CSTRING("BrowserChrome:Ready"))) { + MOZ_ASSERT(false, "Unexpected topic."); + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + NS_ENSURE_STATE(os); + os->RemoveObserver(static_cast<nsIObserver*>(this), "BrowserChrome:Ready"); + + size_t len = pendingWindows.Length(); + for (int i = len-1; i >= 0; i--) { + RefPtr<Runnable> runnable = pendingWindows[i]; + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable)); + pendingWindows.RemoveElementAt(i); + } + + return NS_OK; +} + +END_WORKERS_NAMESPACE diff --git a/dom/workers/ServiceWorkerPrivate.h b/dom/workers/ServiceWorkerPrivate.h new file mode 100644 index 000000000..8d59ea1d0 --- /dev/null +++ b/dom/workers/ServiceWorkerPrivate.h @@ -0,0 +1,236 @@ +/* -*- 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_workers_serviceworkerprivate_h +#define mozilla_dom_workers_serviceworkerprivate_h + +#include "nsCOMPtr.h" + +#include "WorkerPrivate.h" + +#define NOTIFICATION_CLICK_EVENT_NAME "notificationclick" +#define NOTIFICATION_CLOSE_EVENT_NAME "notificationclose" + +class nsIInterceptedChannel; + +namespace mozilla { +namespace dom { +namespace workers { + +class ServiceWorkerInfo; +class ServiceWorkerRegistrationInfo; +class KeepAliveToken; + +class LifeCycleEventCallback : public Runnable +{ +public: + // Called on the worker thread. + virtual void + SetResult(bool aResult) = 0; +}; + +// ServiceWorkerPrivate is a wrapper for managing the on-demand aspect of +// service workers. It handles all event dispatching to the worker and ensures +// the worker thread is running when needed. +// +// Lifetime management: To spin up the worker thread we own a |WorkerPrivate| +// object which can be cancelled if no events are received for a certain +// amount of time. The worker is kept alive by holding a |KeepAliveToken| +// reference. +// +// Extendable events hold tokens for the duration of their handler execution +// and until their waitUntil promise is resolved, while ServiceWorkerPrivate +// will hold a token for |dom.serviceWorkers.idle_timeout| seconds after each +// new event. +// +// Note: All timer events must be handled on the main thread because the +// worker may block indefinitely the worker thread (e. g. infinite loop in the +// script). +// +// There are 3 cases where we may ignore keep alive tokens: +// 1. When ServiceWorkerPrivate's token expired, if there are still waitUntil +// handlers holding tokens, we wait another |dom.serviceWorkers.idle_extended_timeout| +// seconds before forcibly terminating the worker. +// 2. If the worker stopped controlling documents and it is not handling push +// events. +// 3. The content process is shutting down. +// +// Adding an API function for a new event requires calling |SpawnWorkerIfNeeded| +// with an appropriate reason before any runnable is dispatched to the worker. +// If the event is extendable then the runnable should inherit +// ExtendableEventWorkerRunnable. +class ServiceWorkerPrivate final : public nsIObserver +{ + friend class KeepAliveToken; + +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(ServiceWorkerPrivate) + NS_DECL_NSIOBSERVER + + explicit ServiceWorkerPrivate(ServiceWorkerInfo* aInfo); + + nsresult + SendMessageEvent(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + UniquePtr<ServiceWorkerClientInfo>&& aClientInfo); + + // This is used to validate the worker script and continue the installation + // process. + nsresult + CheckScriptEvaluation(LifeCycleEventCallback* aCallback); + + nsresult + SendLifeCycleEvent(const nsAString& aEventType, + LifeCycleEventCallback* aCallback, + nsIRunnable* aLoadFailure); + + nsresult + SendPushEvent(const nsAString& aMessageId, + const Maybe<nsTArray<uint8_t>>& aData, + ServiceWorkerRegistrationInfo* aRegistration); + + nsresult + SendPushSubscriptionChangeEvent(); + + nsresult + SendNotificationEvent(const nsAString& aEventName, + const nsAString& aID, + const nsAString& aTitle, + const nsAString& aDir, + const nsAString& aLang, + const nsAString& aBody, + const nsAString& aTag, + const nsAString& aIcon, + const nsAString& aData, + const nsAString& aBehavior, + const nsAString& aScope); + + nsresult + SendFetchEvent(nsIInterceptedChannel* aChannel, + nsILoadGroup* aLoadGroup, + const nsAString& aDocumentId, + bool aIsReload); + + void + StoreISupports(nsISupports* aSupports); + + void + RemoveISupports(nsISupports* aSupports); + + // This will terminate the current running worker thread and drop the + // workerPrivate reference. + // Called by ServiceWorkerInfo when [[Clear Registration]] is invoked + // or whenever the spec mandates that we terminate the worker. + // This is a no-op if the worker has already been stopped. + void + TerminateWorker(); + + void + NoteDeadServiceWorkerInfo(); + + void + NoteStoppedControllingDocuments(); + + void + Activated(); + + nsresult + GetDebugger(nsIWorkerDebugger** aResult); + + nsresult + AttachDebugger(); + + nsresult + DetachDebugger(); + + bool + IsIdle() const; + + void + AddPendingWindow(Runnable* aPendingWindow); + +private: + enum WakeUpReason { + FetchEvent = 0, + PushEvent, + PushSubscriptionChangeEvent, + MessageEvent, + NotificationClickEvent, + NotificationCloseEvent, + LifeCycleEvent, + AttachEvent + }; + + // Timer callbacks + void + NoteIdleWorkerCallback(nsITimer* aTimer); + + void + TerminateWorkerCallback(nsITimer* aTimer); + + void + RenewKeepAliveToken(WakeUpReason aWhy); + + void + ResetIdleTimeout(); + + void + AddToken(); + + void + ReleaseToken(); + + // |aLoadFailedRunnable| is a runnable dispatched to the main thread + // if the script loader failed for some reason, but can be null. + nsresult + SpawnWorkerIfNeeded(WakeUpReason aWhy, + nsIRunnable* aLoadFailedRunnable, + nsILoadGroup* aLoadGroup = nullptr); + + ~ServiceWorkerPrivate(); + + already_AddRefed<KeepAliveToken> + CreateEventKeepAliveToken(); + + // The info object owns us. It is possible to outlive it for a brief period + // of time if there are pending waitUntil promises, in which case it + // will be null and |SpawnWorkerIfNeeded| will always fail. + ServiceWorkerInfo* MOZ_NON_OWNING_REF mInfo; + + // The WorkerPrivate object can only be closed by this class or by the + // RuntimeService class if gecko is shutting down. Closing the worker + // multiple times is OK, since the second attempt will be a no-op. + RefPtr<WorkerPrivate> mWorkerPrivate; + + nsCOMPtr<nsITimer> mIdleWorkerTimer; + + // We keep a token for |dom.serviceWorkers.idle_timeout| seconds to give the + // worker a grace period after each event. + RefPtr<KeepAliveToken> mIdleKeepAliveToken; + + uint64_t mDebuggerCount; + + uint64_t mTokenCount; + + // Meant for keeping objects alive while handling requests from the worker + // on the main thread. Access to this array is provided through + // |StoreISupports| and |RemoveISupports|. Note that the array is also + // cleared whenever the worker is terminated. + nsTArray<nsCOMPtr<nsISupports>> mSupportsArray; + + // Array of function event worker runnables that are pending due to + // the worker activating. Main thread only. + nsTArray<RefPtr<WorkerRunnable>> mPendingFunctionalEvents; + + nsTArray<Runnable*> pendingWindows; +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_serviceworkerprivate_h diff --git a/dom/workers/ServiceWorkerRegisterJob.cpp b/dom/workers/ServiceWorkerRegisterJob.cpp new file mode 100644 index 000000000..8f771e762 --- /dev/null +++ b/dom/workers/ServiceWorkerRegisterJob.cpp @@ -0,0 +1,67 @@ +/* -*- 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 "ServiceWorkerRegisterJob.h" + +#include "Workers.h" + +namespace mozilla { +namespace dom { +namespace workers { + +ServiceWorkerRegisterJob::ServiceWorkerRegisterJob(nsIPrincipal* aPrincipal, + const nsACString& aScope, + const nsACString& aScriptSpec, + nsILoadGroup* aLoadGroup) + : ServiceWorkerUpdateJob(Type::Register, aPrincipal, aScope, aScriptSpec, + aLoadGroup) +{ +} + +void +ServiceWorkerRegisterJob::AsyncExecute() +{ + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (Canceled() || !swm) { + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + swm->GetRegistration(mPrincipal, mScope); + + if (registration) { + // If we are resurrecting an uninstalling registration, then persist + // it to disk again. We preemptively removed it earlier during + // unregister so that closing the window by shutting down the browser + // results in the registration being gone on restart. + if (registration->mPendingUninstall) { + swm->StoreRegistration(mPrincipal, registration); + } + registration->mPendingUninstall = false; + RefPtr<ServiceWorkerInfo> newest = registration->Newest(); + if (newest && mScriptSpec.Equals(newest->ScriptSpec())) { + SetRegistration(registration); + Finish(NS_OK); + return; + } + } else { + registration = swm->CreateNewRegistration(mScope, mPrincipal); + } + + SetRegistration(registration); + Update(); +} + +ServiceWorkerRegisterJob::~ServiceWorkerRegisterJob() +{ +} + +} // namespace workers +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/ServiceWorkerRegisterJob.h b/dom/workers/ServiceWorkerRegisterJob.h new file mode 100644 index 000000000..a459e25b6 --- /dev/null +++ b/dom/workers/ServiceWorkerRegisterJob.h @@ -0,0 +1,40 @@ +/* -*- 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_workers_serviceworkerregisterjob_h +#define mozilla_dom_workers_serviceworkerregisterjob_h + +#include "ServiceWorkerUpdateJob.h" + +namespace mozilla { +namespace dom { +namespace workers { + +// The register job. This implements the steps in the spec Register algorithm, +// but then uses ServiceWorkerUpdateJob to implement the Update and Install +// spec algorithms. +class ServiceWorkerRegisterJob final : public ServiceWorkerUpdateJob +{ +public: + ServiceWorkerRegisterJob(nsIPrincipal* aPrincipal, + const nsACString& aScope, + const nsACString& aScriptSpec, + nsILoadGroup* aLoadGroup); + +private: + // Implement the Register algorithm steps and then call the parent class + // Update() to complete the job execution. + virtual void + AsyncExecute() override; + + virtual ~ServiceWorkerRegisterJob(); +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_serviceworkerregisterjob_h diff --git a/dom/workers/ServiceWorkerRegistrar.cpp b/dom/workers/ServiceWorkerRegistrar.cpp new file mode 100644 index 000000000..a4757ea54 --- /dev/null +++ b/dom/workers/ServiceWorkerRegistrar.cpp @@ -0,0 +1,884 @@ + +/* -*- 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 "ServiceWorkerRegistrar.h" +#include "mozilla/dom/ServiceWorkerRegistrarTypes.h" + +#include "nsIEventTarget.h" +#include "nsIInputStream.h" +#include "nsILineInputStream.h" +#include "nsIObserverService.h" +#include "nsIOutputStream.h" +#include "nsISafeOutputStream.h" + +#include "MainThreadUtils.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ModuleUtils.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPtr.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsContentUtils.h" +#include "nsDirectoryServiceUtils.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" + +using namespace mozilla::ipc; + +namespace mozilla { +namespace dom { + +namespace { + +static const char* gSupportedRegistrarVersions[] = { + SERVICEWORKERREGISTRAR_VERSION, + "3", + "2" +}; + +StaticRefPtr<ServiceWorkerRegistrar> gServiceWorkerRegistrar; + +} // namespace + +NS_IMPL_ISUPPORTS(ServiceWorkerRegistrar, + nsIObserver) + +void +ServiceWorkerRegistrar::Initialize() +{ + MOZ_ASSERT(!gServiceWorkerRegistrar); + + if (!XRE_IsParentProcess()) { + return; + } + + gServiceWorkerRegistrar = new ServiceWorkerRegistrar(); + ClearOnShutdown(&gServiceWorkerRegistrar); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + DebugOnly<nsresult> rv = obs->AddObserver(gServiceWorkerRegistrar, + "profile-after-change", false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + rv = obs->AddObserver(gServiceWorkerRegistrar, "profile-before-change", + false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } +} + +/* static */ already_AddRefed<ServiceWorkerRegistrar> +ServiceWorkerRegistrar::Get() +{ + MOZ_ASSERT(XRE_IsParentProcess()); + + MOZ_ASSERT(gServiceWorkerRegistrar); + RefPtr<ServiceWorkerRegistrar> service = gServiceWorkerRegistrar.get(); + return service.forget(); +} + +ServiceWorkerRegistrar::ServiceWorkerRegistrar() + : mMonitor("ServiceWorkerRegistrar.mMonitor") + , mDataLoaded(false) + , mShuttingDown(false) + , mShutdownCompleteFlag(nullptr) + , mRunnableCounter(0) +{ + MOZ_ASSERT(NS_IsMainThread()); +} + +ServiceWorkerRegistrar::~ServiceWorkerRegistrar() +{ + MOZ_ASSERT(!mRunnableCounter); +} + +void +ServiceWorkerRegistrar::GetRegistrations( + nsTArray<ServiceWorkerRegistrationData>& aValues) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aValues.IsEmpty()); + + MonitorAutoLock lock(mMonitor); + + // If we don't have the profile directory, profile is not started yet (and + // probably we are in a utest). + if (!mProfileDir) { + return; + } + + // We care just about the first execution because this can be blocked by + // loading data from disk. + static bool firstTime = true; + TimeStamp startTime; + + if (firstTime) { + startTime = TimeStamp::NowLoRes(); + } + + // Waiting for data loaded. + mMonitor.AssertCurrentThreadOwns(); + while (!mDataLoaded) { + mMonitor.Wait(); + } + + aValues.AppendElements(mData); + + if (firstTime) { + firstTime = false; + Telemetry::AccumulateTimeDelta( + Telemetry::SERVICE_WORKER_REGISTRATION_LOADING, + startTime); + } +} + +namespace { + +bool Equivalent(const ServiceWorkerRegistrationData& aLeft, + const ServiceWorkerRegistrationData& aRight) +{ + MOZ_ASSERT(aLeft.principal().type() == + mozilla::ipc::PrincipalInfo::TContentPrincipalInfo); + MOZ_ASSERT(aRight.principal().type() == + mozilla::ipc::PrincipalInfo::TContentPrincipalInfo); + + const auto& leftPrincipal = aLeft.principal().get_ContentPrincipalInfo(); + const auto& rightPrincipal = aRight.principal().get_ContentPrincipalInfo(); + + // Only compare the attributes, not the spec part of the principal. + // The scope comparison above already covers the origin and codebase + // principals include the full path in their spec which is not what + // we want here. + return aLeft.scope() == aRight.scope() && + leftPrincipal.attrs() == rightPrincipal.attrs(); +} + +} // anonymous namespace + +void +ServiceWorkerRegistrar::RegisterServiceWorker( + const ServiceWorkerRegistrationData& aData) +{ + AssertIsOnBackgroundThread(); + + if (mShuttingDown) { + NS_WARNING("Failed to register a serviceWorker during shutting down."); + return; + } + + { + MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(mDataLoaded); + RegisterServiceWorkerInternal(aData); + } + + ScheduleSaveData(); +} + +void +ServiceWorkerRegistrar::UnregisterServiceWorker( + const PrincipalInfo& aPrincipalInfo, + const nsACString& aScope) +{ + AssertIsOnBackgroundThread(); + + if (mShuttingDown) { + NS_WARNING("Failed to unregister a serviceWorker during shutting down."); + return; + } + + bool deleted = false; + + { + MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(mDataLoaded); + + ServiceWorkerRegistrationData tmp; + tmp.principal() = aPrincipalInfo; + tmp.scope() = aScope; + + for (uint32_t i = 0; i < mData.Length(); ++i) { + if (Equivalent(tmp, mData[i])) { + mData.RemoveElementAt(i); + deleted = true; + break; + } + } + } + + if (deleted) { + ScheduleSaveData(); + } +} + +void +ServiceWorkerRegistrar::RemoveAll() +{ + AssertIsOnBackgroundThread(); + + if (mShuttingDown) { + NS_WARNING("Failed to remove all the serviceWorkers during shutting down."); + return; + } + + bool deleted = false; + + { + MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(mDataLoaded); + + deleted = !mData.IsEmpty(); + mData.Clear(); + } + + if (deleted) { + ScheduleSaveData(); + } +} + +void +ServiceWorkerRegistrar::LoadData() +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!mDataLoaded); + + nsresult rv = ReadData(); + + if (NS_WARN_IF(NS_FAILED(rv))) { + DeleteData(); + // Also if the reading failed we have to notify what is waiting for data. + } + + MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(!mDataLoaded); + mDataLoaded = true; + mMonitor.Notify(); +} + +nsresult +ServiceWorkerRegistrar::ReadData() +{ + // We cannot assert about the correct thread because normally this method + // runs on a IO thread, but in gTests we call it from the main-thread. + + nsCOMPtr<nsIFile> file; + + { + MonitorAutoLock lock(mMonitor); + + if (!mProfileDir) { + return NS_ERROR_FAILURE; + } + + nsresult rv = mProfileDir->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + nsresult rv = file->Append(NS_LITERAL_STRING(SERVICEWORKERREGISTRAR_FILE)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + bool exists; + rv = file->Exists(&exists); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!exists) { + return NS_OK; + } + + nsCOMPtr<nsIInputStream> stream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), file); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsILineInputStream> lineInputStream = do_QueryInterface(stream); + MOZ_ASSERT(lineInputStream); + + nsAutoCString version; + bool hasMoreLines; + rv = lineInputStream->ReadLine(version, &hasMoreLines); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!IsSupportedVersion(version)) { + nsContentUtils::LogMessageToConsole(nsPrintfCString( + "Unsupported service worker registrar version: %s", version.get()).get()); + return NS_ERROR_FAILURE; + } + + nsTArray<ServiceWorkerRegistrationData> tmpData; + + bool overwrite = false; + bool dedupe = false; + while (hasMoreLines) { + ServiceWorkerRegistrationData* entry = tmpData.AppendElement(); + +#define GET_LINE(x) \ + rv = lineInputStream->ReadLine(x, &hasMoreLines); \ + if (NS_WARN_IF(NS_FAILED(rv))) { \ + return rv; \ + } \ + if (NS_WARN_IF(!hasMoreLines)) { \ + return NS_ERROR_FAILURE; \ + } + + nsAutoCString line; + nsAutoCString unused; + if (version.EqualsLiteral(SERVICEWORKERREGISTRAR_VERSION)) { + nsAutoCString suffix; + GET_LINE(suffix); + + PrincipalOriginAttributes attrs; + if (!attrs.PopulateFromSuffix(suffix)) { + return NS_ERROR_INVALID_ARG; + } + + GET_LINE(entry->scope()); + + entry->principal() = + mozilla::ipc::ContentPrincipalInfo(attrs, void_t(), entry->scope()); + + GET_LINE(entry->currentWorkerURL()); + + nsAutoCString cacheName; + GET_LINE(cacheName); + CopyUTF8toUTF16(cacheName, entry->cacheName()); + } else if (version.EqualsLiteral("3")) { + overwrite = true; + dedupe = true; + + nsAutoCString suffix; + GET_LINE(suffix); + + PrincipalOriginAttributes attrs; + if (!attrs.PopulateFromSuffix(suffix)) { + return NS_ERROR_INVALID_ARG; + } + + // principal spec is no longer used; we use scope directly instead + GET_LINE(unused); + + GET_LINE(entry->scope()); + + entry->principal() = + mozilla::ipc::ContentPrincipalInfo(attrs, void_t(), entry->scope()); + + GET_LINE(entry->currentWorkerURL()); + + nsAutoCString cacheName; + GET_LINE(cacheName); + CopyUTF8toUTF16(cacheName, entry->cacheName()); + } else if (version.EqualsLiteral("2")) { + overwrite = true; + dedupe = true; + + nsAutoCString suffix; + GET_LINE(suffix); + + PrincipalOriginAttributes attrs; + if (!attrs.PopulateFromSuffix(suffix)) { + return NS_ERROR_INVALID_ARG; + } + + // principal spec is no longer used; we use scope directly instead + GET_LINE(unused); + + GET_LINE(entry->scope()); + + entry->principal() = + mozilla::ipc::ContentPrincipalInfo(attrs, void_t(), entry->scope()); + + // scriptSpec is no more used in latest version. + GET_LINE(unused); + + GET_LINE(entry->currentWorkerURL()); + + nsAutoCString cacheName; + GET_LINE(cacheName); + CopyUTF8toUTF16(cacheName, entry->cacheName()); + + // waitingCacheName is no more used in latest version. + GET_LINE(unused); + } else { + MOZ_ASSERT_UNREACHABLE("Should never get here!"); + } + +#undef GET_LINE + + rv = lineInputStream->ReadLine(line, &hasMoreLines); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!line.EqualsLiteral(SERVICEWORKERREGISTRAR_TERMINATOR)) { + return NS_ERROR_FAILURE; + } + } + + stream->Close(); + + // Copy data over to mData. + for (uint32_t i = 0; i < tmpData.Length(); ++i) { + bool match = false; + if (dedupe) { + MOZ_ASSERT(overwrite); + // If this is an old profile, then we might need to deduplicate. In + // theory this can be removed in the future (Bug 1248449) + for (uint32_t j = 0; j < mData.Length(); ++j) { + // Use same comparison as RegisterServiceWorker. Scope contains + // basic origin information. Combine with any principal attributes. + if (Equivalent(tmpData[i], mData[j])) { + // Last match wins, just like legacy loading used to do in + // the ServiceWorkerManager. + mData[j] = tmpData[i]; + // Dupe found, so overwrite file with reduced list. + match = true; + break; + } + } + } else { +#ifdef DEBUG + // Otherwise assert no duplications in debug builds. + for (uint32_t j = 0; j < mData.Length(); ++j) { + MOZ_ASSERT(!Equivalent(tmpData[i], mData[j])); + } +#endif + } + if (!match) { + mData.AppendElement(tmpData[i]); + } + } + + // Overwrite previous version. + // Cannot call SaveData directly because gtest uses main-thread. + if (overwrite && NS_FAILED(WriteData())) { + NS_WARNING("Failed to write data for the ServiceWorker Registations."); + DeleteData(); + } + + return NS_OK; +} + +void +ServiceWorkerRegistrar::DeleteData() +{ + // We cannot assert about the correct thread because normally this method + // runs on a IO thread, but in gTests we call it from the main-thread. + + nsCOMPtr<nsIFile> file; + + { + MonitorAutoLock lock(mMonitor); + mData.Clear(); + + if (!mProfileDir) { + return; + } + + nsresult rv = mProfileDir->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + + nsresult rv = file->Append(NS_LITERAL_STRING(SERVICEWORKERREGISTRAR_FILE)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + rv = file->Remove(false); + if (rv == NS_ERROR_FILE_NOT_FOUND) { + return; + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } +} + +void +ServiceWorkerRegistrar::RegisterServiceWorkerInternal(const ServiceWorkerRegistrationData& aData) +{ + bool found = false; + for (uint32_t i = 0, len = mData.Length(); i < len; ++i) { + if (Equivalent(aData, mData[i])) { + mData[i] = aData; + found = true; + break; + } + } + + if (!found) { + mData.AppendElement(aData); + } +} + +class ServiceWorkerRegistrarSaveDataRunnable final : public Runnable +{ +public: + ServiceWorkerRegistrarSaveDataRunnable() + : mThread(do_GetCurrentThread()) + { + AssertIsOnBackgroundThread(); + } + + NS_IMETHOD + Run() override + { + RefPtr<ServiceWorkerRegistrar> service = ServiceWorkerRegistrar::Get(); + MOZ_ASSERT(service); + + service->SaveData(); + + RefPtr<Runnable> runnable = + NewRunnableMethod(service, &ServiceWorkerRegistrar::DataSaved); + nsresult rv = mThread->Dispatch(runnable, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + +private: + nsCOMPtr<nsIThread> mThread; +}; + +void +ServiceWorkerRegistrar::ScheduleSaveData() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mShuttingDown); + + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(target, "Must have stream transport service"); + + RefPtr<Runnable> runnable = + new ServiceWorkerRegistrarSaveDataRunnable(); + nsresult rv = target->Dispatch(runnable, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + ++mRunnableCounter; +} + +void +ServiceWorkerRegistrar::ShutdownCompleted() +{ + MOZ_ASSERT(NS_IsMainThread()); + + MOZ_ASSERT(mShutdownCompleteFlag && !*mShutdownCompleteFlag); + *mShutdownCompleteFlag = true; +} + +void +ServiceWorkerRegistrar::SaveData() +{ + MOZ_ASSERT(!NS_IsMainThread()); + + nsresult rv = WriteData(); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to write data for the ServiceWorker Registations."); + DeleteData(); + } +} + +void +ServiceWorkerRegistrar::DataSaved() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mRunnableCounter); + + --mRunnableCounter; + MaybeScheduleShutdownCompleted(); +} + +void +ServiceWorkerRegistrar::MaybeScheduleShutdownCompleted() +{ + AssertIsOnBackgroundThread(); + + if (mRunnableCounter || !mShuttingDown) { + return; + } + + RefPtr<Runnable> runnable = + NewRunnableMethod(this, &ServiceWorkerRegistrar::ShutdownCompleted); + nsresult rv = NS_DispatchToMainThread(runnable); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } +} + +bool +ServiceWorkerRegistrar::IsSupportedVersion(const nsACString& aVersion) const +{ + uint32_t numVersions = ArrayLength(gSupportedRegistrarVersions); + for (uint32_t i = 0; i < numVersions; i++) { + if (aVersion.EqualsASCII(gSupportedRegistrarVersions[i])) { + return true; + } + } + return false; +} + +nsresult +ServiceWorkerRegistrar::WriteData() +{ + // We cannot assert about the correct thread because normally this method + // runs on a IO thread, but in gTests we call it from the main-thread. + + nsCOMPtr<nsIFile> file; + + { + MonitorAutoLock lock(mMonitor); + + if (!mProfileDir) { + return NS_ERROR_FAILURE; + } + + nsresult rv = mProfileDir->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + nsresult rv = file->Append(NS_LITERAL_STRING(SERVICEWORKERREGISTRAR_FILE)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // We need a lock to take a snapshot of the data. + nsTArray<ServiceWorkerRegistrationData> data; + { + MonitorAutoLock lock(mMonitor); + data = mData; + } + + nsCOMPtr<nsIOutputStream> stream; + rv = NS_NewSafeLocalFileOutputStream(getter_AddRefs(stream), file); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString buffer; + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_VERSION); + buffer.Append('\n'); + + uint32_t count; + rv = stream->Write(buffer.Data(), buffer.Length(), &count); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (count != buffer.Length()) { + return NS_ERROR_UNEXPECTED; + } + + for (uint32_t i = 0, len = data.Length(); i < len; ++i) { + const mozilla::ipc::PrincipalInfo& info = data[i].principal(); + + MOZ_ASSERT(info.type() == mozilla::ipc::PrincipalInfo::TContentPrincipalInfo); + + const mozilla::ipc::ContentPrincipalInfo& cInfo = + info.get_ContentPrincipalInfo(); + + nsAutoCString suffix; + cInfo.attrs().CreateSuffix(suffix); + + buffer.Truncate(); + buffer.Append(suffix.get()); + buffer.Append('\n'); + + buffer.Append(data[i].scope()); + buffer.Append('\n'); + + buffer.Append(data[i].currentWorkerURL()); + buffer.Append('\n'); + + buffer.Append(NS_ConvertUTF16toUTF8(data[i].cacheName())); + buffer.Append('\n'); + + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR); + buffer.Append('\n'); + + rv = stream->Write(buffer.Data(), buffer.Length(), &count); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (count != buffer.Length()) { + return NS_ERROR_UNEXPECTED; + } + } + + nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(stream); + MOZ_ASSERT(safeStream); + + rv = safeStream->Finish(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void +ServiceWorkerRegistrar::ProfileStarted() +{ + MOZ_ASSERT(NS_IsMainThread()); + + MonitorAutoLock lock(mMonitor); + MOZ_DIAGNOSTIC_ASSERT(!mProfileDir); + + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mProfileDir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_ASSERT(target, "Must have stream transport service"); + + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod(this, &ServiceWorkerRegistrar::LoadData); + rv = target->Dispatch(runnable, NS_DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch the LoadDataRunnable."); + } +} + +void +ServiceWorkerRegistrar::ProfileStopped() +{ + MOZ_ASSERT(NS_IsMainThread()); + + MonitorAutoLock lock(mMonitor); + + if (!mProfileDir) { + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mProfileDir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + + // We must set the pointer before potentially entering the fast-path shutdown + // below. + bool completed = false; + mShutdownCompleteFlag = &completed; + + PBackgroundChild* child = BackgroundChild::GetForCurrentThread(); + if (!child) { + // Mutations to the ServiceWorkerRegistrar happen on the PBackground thread, + // issued by the ServiceWorkerManagerService, so the appropriate place to + // trigger shutdown is on that thread. + // + // However, it's quite possible that the PBackground thread was not brought + // into existence for xpcshell tests. We don't cause it to be created + // ourselves for any reason, for example. + // + // In this scenario, we know that: + // - We will receive exactly one call to ourself from BlockShutdown() and + // BlockShutdown() will be called (at most) once. + // - The only way our Shutdown() method gets called is via + // BackgroundParentImpl::RecvShutdownServiceWorkerRegistrar() being + // invoked, which only happens if we get to that send below here that we + // can't get to. + // - All Shutdown() does is set mShuttingDown=true (essential for + // invariants) and invoke MaybeScheduleShutdownCompleted(). + // - Since there is no PBackground thread, mRunnableCounter must be 0 + // because only ScheduleSaveData() increments it and it only runs on the + // background thread, so it cannot have run. And so we would expect + // MaybeScheduleShutdownCompleted() to schedule an invocation of + // ShutdownCompleted on the main thread. + // + // So it's appropriate for us to set mShuttingDown=true (as Shutdown would + // do) and directly invoke ShutdownCompleted() (as Shutdown would indirectly + // do via MaybeScheduleShutdownCompleted). + mShuttingDown = true; + ShutdownCompleted(); + return; + } + + child->SendShutdownServiceWorkerRegistrar(); + + nsCOMPtr<nsIThread> thread(do_GetCurrentThread()); + while (true) { + MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(thread)); + if (completed) { + break; + } + } +} + +void +ServiceWorkerRegistrar::Shutdown() +{ + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mShuttingDown); + + mShuttingDown = true; + MaybeScheduleShutdownCompleted(); +} + +NS_IMETHODIMP +ServiceWorkerRegistrar::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!strcmp(aTopic, "profile-after-change")) { + nsCOMPtr<nsIObserverService> observerService = + services::GetObserverService(); + observerService->RemoveObserver(this, "profile-after-change"); + + // The profile is fully loaded, now we can proceed with the loading of data + // from disk. + ProfileStarted(); + + return NS_OK; + } + + if (!strcmp(aTopic, "profile-before-change")) { + // Hygiene; gServiceWorkerRegistrar should still be keeping a reference + // alive well past this phase of shutdown, but it's bad form to drop your + // last potentially owning reference and then make a call that requires you + // to still be alive, especially when you spin a nested event loop. + RefPtr<ServiceWorkerRegistrar> kungFuDeathGrip(this); + + nsCOMPtr<nsIObserverService> observerService = + services::GetObserverService(); + observerService->RemoveObserver(this, "profile-before-change"); + + // Shutting down, let's sync the data. + ProfileStopped(); + + return NS_OK; + } + + MOZ_ASSERT(false, "ServiceWorkerRegistrar got unexpected topic!"); + return NS_ERROR_UNEXPECTED; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/ServiceWorkerRegistrar.h b/dom/workers/ServiceWorkerRegistrar.h new file mode 100644 index 000000000..0c476ad9b --- /dev/null +++ b/dom/workers/ServiceWorkerRegistrar.h @@ -0,0 +1,101 @@ +/* -*- 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_workers_ServiceWorkerRegistrar_h +#define mozilla_dom_workers_ServiceWorkerRegistrar_h + +#include "mozilla/Monitor.h" +#include "mozilla/Telemetry.h" +#include "nsClassHashtable.h" +#include "nsIObserver.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsTArray.h" + +#define SERVICEWORKERREGISTRAR_FILE "serviceworker.txt" +#define SERVICEWORKERREGISTRAR_VERSION "4" +#define SERVICEWORKERREGISTRAR_TERMINATOR "#" +#define SERVICEWORKERREGISTRAR_TRUE "true" +#define SERVICEWORKERREGISTRAR_FALSE "false" + +class nsIFile; + +namespace mozilla { + +namespace ipc { +class PrincipalInfo; +} // namespace ipc + +namespace dom { + +class ServiceWorkerRegistrationData; + +class ServiceWorkerRegistrar : public nsIObserver +{ + friend class ServiceWorkerRegistrarSaveDataRunnable; + +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + + static void Initialize(); + + void Shutdown(); + + void DataSaved(); + + static already_AddRefed<ServiceWorkerRegistrar> Get(); + + void GetRegistrations(nsTArray<ServiceWorkerRegistrationData>& aValues); + + void RegisterServiceWorker(const ServiceWorkerRegistrationData& aData); + void UnregisterServiceWorker(const mozilla::ipc::PrincipalInfo& aPrincipalInfo, + const nsACString& aScope); + void RemoveAll(); + +protected: + // These methods are protected because we test this class using gTest + // subclassing it. + void LoadData(); + void SaveData(); + + nsresult ReadData(); + nsresult WriteData(); + void DeleteData(); + + void RegisterServiceWorkerInternal(const ServiceWorkerRegistrationData& aData); + + ServiceWorkerRegistrar(); + virtual ~ServiceWorkerRegistrar(); + +private: + void ProfileStarted(); + void ProfileStopped(); + + void ScheduleSaveData(); + void ShutdownCompleted(); + void MaybeScheduleShutdownCompleted(); + + bool IsSupportedVersion(const nsACString& aVersion) const; + + mozilla::Monitor mMonitor; + +protected: + // protected by mMonitor. + nsCOMPtr<nsIFile> mProfileDir; + nsTArray<ServiceWorkerRegistrationData> mData; + bool mDataLoaded; + + // PBackground thread only + bool mShuttingDown; + bool* mShutdownCompleteFlag; + uint32_t mRunnableCounter; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_ServiceWorkerRegistrar_h diff --git a/dom/workers/ServiceWorkerRegistrarTypes.ipdlh b/dom/workers/ServiceWorkerRegistrarTypes.ipdlh new file mode 100644 index 000000000..7754a19e6 --- /dev/null +++ b/dom/workers/ServiceWorkerRegistrarTypes.ipdlh @@ -0,0 +1,23 @@ +/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */ +/* vim: set sw=4 ts=8 et tw=80 ft=cpp : */ +/* 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 PBackgroundSharedTypes; + +namespace mozilla { +namespace dom { + +struct ServiceWorkerRegistrationData +{ + nsCString scope; + nsCString currentWorkerURL; + + nsString cacheName; + + PrincipalInfo principal; +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/ServiceWorkerRegistration.cpp b/dom/workers/ServiceWorkerRegistration.cpp new file mode 100644 index 000000000..451bd2be9 --- /dev/null +++ b/dom/workers/ServiceWorkerRegistration.cpp @@ -0,0 +1,1327 @@ +/* -*- 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 "ServiceWorkerRegistration.h" + +#include "ipc/ErrorIPCUtils.h" +#include "mozilla/dom/Notification.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseWorkerProxy.h" +#include "mozilla/dom/PushManagerBinding.h" +#include "mozilla/dom/PushManager.h" +#include "mozilla/dom/ServiceWorkerRegistrationBinding.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/Unused.h" +#include "nsCycleCollectionParticipant.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "ServiceWorker.h" +#include "ServiceWorkerManager.h" + +#include "nsIDocument.h" +#include "nsIServiceWorkerManager.h" +#include "nsISupportsPrimitives.h" +#include "nsPIDOMWindow.h" +#include "nsContentUtils.h" + +#include "WorkerPrivate.h" +#include "Workers.h" +#include "WorkerScope.h" + +using namespace mozilla::dom::workers; + +namespace mozilla { +namespace dom { + +/* static */ bool +ServiceWorkerRegistration::Visible(JSContext* aCx, JSObject* aObj) +{ + if (NS_IsMainThread()) { + return Preferences::GetBool("dom.serviceWorkers.enabled", false); + } + + // Otherwise check the pref via the work private helper + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx); + if (!workerPrivate) { + return false; + } + + return workerPrivate->ServiceWorkersEnabled(); +} + +/* static */ bool +ServiceWorkerRegistration::NotificationAPIVisible(JSContext* aCx, JSObject* aObj) +{ + if (NS_IsMainThread()) { + return Preferences::GetBool("dom.webnotifications.serviceworker.enabled", false); + } + + // Otherwise check the pref via the work private helper + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx); + if (!workerPrivate) { + return false; + } + + return workerPrivate->DOMServiceWorkerNotificationEnabled(); +} + +//////////////////////////////////////////////////// +// Main Thread implementation + +class ServiceWorkerRegistrationMainThread final : public ServiceWorkerRegistration, + public ServiceWorkerRegistrationListener +{ + friend nsPIDOMWindowInner; +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ServiceWorkerRegistrationMainThread, + ServiceWorkerRegistration) + + ServiceWorkerRegistrationMainThread(nsPIDOMWindowInner* aWindow, + const nsAString& aScope); + + already_AddRefed<Promise> + Update(ErrorResult& aRv) override; + + already_AddRefed<Promise> + Unregister(ErrorResult& aRv) override; + + // Partial interface from Notification API. + already_AddRefed<Promise> + ShowNotification(JSContext* aCx, + const nsAString& aTitle, + const NotificationOptions& aOptions, + ErrorResult& aRv) override; + + already_AddRefed<Promise> + GetNotifications(const GetNotificationOptions& aOptions, + ErrorResult& aRv) override; + + already_AddRefed<ServiceWorker> + GetInstalling() override; + + already_AddRefed<ServiceWorker> + GetWaiting() override; + + already_AddRefed<ServiceWorker> + GetActive() override; + + already_AddRefed<PushManager> + GetPushManager(JSContext* aCx, ErrorResult& aRv) override; + + // DOMEventTargethelper + void DisconnectFromOwner() override + { + StopListeningForEvents(); + ServiceWorkerRegistration::DisconnectFromOwner(); + } + + // ServiceWorkerRegistrationListener + void + UpdateFound() override; + + void + InvalidateWorkers(WhichServiceWorker aWhichOnes) override; + + void + RegistrationRemoved() override; + + void + GetScope(nsAString& aScope) const override + { + aScope = mScope; + } + +private: + ~ServiceWorkerRegistrationMainThread(); + + already_AddRefed<ServiceWorker> + GetWorkerReference(WhichServiceWorker aWhichOne); + + void + StartListeningForEvents(); + + void + StopListeningForEvents(); + + bool mListeningForEvents; + + // The following properties are cached here to ensure JS equality is satisfied + // instead of acquiring a new worker instance from the ServiceWorkerManager + // for every access. A null value is considered a cache miss. + // These three may change to a new worker at any time. + RefPtr<ServiceWorker> mInstallingWorker; + RefPtr<ServiceWorker> mWaitingWorker; + RefPtr<ServiceWorker> mActiveWorker; + + RefPtr<PushManager> mPushManager; +}; + +NS_IMPL_ADDREF_INHERITED(ServiceWorkerRegistrationMainThread, ServiceWorkerRegistration) +NS_IMPL_RELEASE_INHERITED(ServiceWorkerRegistrationMainThread, ServiceWorkerRegistration) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(ServiceWorkerRegistrationMainThread) +NS_INTERFACE_MAP_END_INHERITING(ServiceWorkerRegistration) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ServiceWorkerRegistrationMainThread, + ServiceWorkerRegistration, + mPushManager, + mInstallingWorker, mWaitingWorker, mActiveWorker); + +ServiceWorkerRegistrationMainThread::ServiceWorkerRegistrationMainThread(nsPIDOMWindowInner* aWindow, + const nsAString& aScope) + : ServiceWorkerRegistration(aWindow, aScope) + , mListeningForEvents(false) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->IsInnerWindow()); + StartListeningForEvents(); +} + +ServiceWorkerRegistrationMainThread::~ServiceWorkerRegistrationMainThread() +{ + StopListeningForEvents(); + MOZ_ASSERT(!mListeningForEvents); +} + + +already_AddRefed<ServiceWorker> +ServiceWorkerRegistrationMainThread::GetWorkerReference(WhichServiceWorker aWhichOne) +{ + nsCOMPtr<nsPIDOMWindowInner> window = GetOwner(); + if (!window) { + return nullptr; + } + + nsresult rv; + nsCOMPtr<nsIServiceWorkerManager> swm = + do_GetService(SERVICEWORKERMANAGER_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + nsCOMPtr<nsISupports> serviceWorker; + switch(aWhichOne) { + case WhichServiceWorker::INSTALLING_WORKER: + rv = swm->GetInstalling(window, mScope, getter_AddRefs(serviceWorker)); + break; + case WhichServiceWorker::WAITING_WORKER: + rv = swm->GetWaiting(window, mScope, getter_AddRefs(serviceWorker)); + break; + case WhichServiceWorker::ACTIVE_WORKER: + rv = swm->GetActive(window, mScope, getter_AddRefs(serviceWorker)); + break; + default: + MOZ_CRASH("Invalid enum value"); + } + + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv) || rv == NS_ERROR_DOM_NOT_FOUND_ERR, + "Unexpected error getting service worker instance from " + "ServiceWorkerManager"); + if (NS_FAILED(rv)) { + return nullptr; + } + + RefPtr<ServiceWorker> ref = + static_cast<ServiceWorker*>(serviceWorker.get()); + return ref.forget(); +} + +// XXXnsm, maybe this can be optimized to only add when a event handler is +// registered. +void +ServiceWorkerRegistrationMainThread::StartListeningForEvents() +{ + AssertIsOnMainThread(); + MOZ_ASSERT(!mListeningForEvents); + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->AddRegistrationEventListener(mScope, this); + mListeningForEvents = true; + } +} + +void +ServiceWorkerRegistrationMainThread::StopListeningForEvents() +{ + AssertIsOnMainThread(); + if (!mListeningForEvents) { + return; + } + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->RemoveRegistrationEventListener(mScope, this); + } + mListeningForEvents = false; +} + +already_AddRefed<ServiceWorker> +ServiceWorkerRegistrationMainThread::GetInstalling() +{ + AssertIsOnMainThread(); + if (!mInstallingWorker) { + mInstallingWorker = GetWorkerReference(WhichServiceWorker::INSTALLING_WORKER); + } + + RefPtr<ServiceWorker> ret = mInstallingWorker; + return ret.forget(); +} + +already_AddRefed<ServiceWorker> +ServiceWorkerRegistrationMainThread::GetWaiting() +{ + AssertIsOnMainThread(); + if (!mWaitingWorker) { + mWaitingWorker = GetWorkerReference(WhichServiceWorker::WAITING_WORKER); + } + + RefPtr<ServiceWorker> ret = mWaitingWorker; + return ret.forget(); +} + +already_AddRefed<ServiceWorker> +ServiceWorkerRegistrationMainThread::GetActive() +{ + AssertIsOnMainThread(); + if (!mActiveWorker) { + mActiveWorker = GetWorkerReference(WhichServiceWorker::ACTIVE_WORKER); + } + + RefPtr<ServiceWorker> ret = mActiveWorker; + return ret.forget(); +} + +void +ServiceWorkerRegistrationMainThread::UpdateFound() +{ + DispatchTrustedEvent(NS_LITERAL_STRING("updatefound")); +} + +void +ServiceWorkerRegistrationMainThread::InvalidateWorkers(WhichServiceWorker aWhichOnes) +{ + AssertIsOnMainThread(); + if (aWhichOnes & WhichServiceWorker::INSTALLING_WORKER) { + mInstallingWorker = nullptr; + } + + if (aWhichOnes & WhichServiceWorker::WAITING_WORKER) { + mWaitingWorker = nullptr; + } + + if (aWhichOnes & WhichServiceWorker::ACTIVE_WORKER) { + mActiveWorker = nullptr; + } + +} + +void +ServiceWorkerRegistrationMainThread::RegistrationRemoved() +{ + // If the registration is being removed completely, remove it from the + // window registration hash table so that a new registration would get a new + // wrapper JS object. + if (nsCOMPtr<nsPIDOMWindowInner> window = GetOwner()) { + window->InvalidateServiceWorkerRegistration(mScope); + } +} + +namespace { + +void +UpdateInternal(nsIPrincipal* aPrincipal, + const nsAString& aScope, + ServiceWorkerUpdateFinishCallback* aCallback) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aCallback); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown + return; + } + + swm->Update(aPrincipal, NS_ConvertUTF16toUTF8(aScope), aCallback); +} + +class MainThreadUpdateCallback final : public ServiceWorkerUpdateFinishCallback +{ + RefPtr<Promise> mPromise; + + ~MainThreadUpdateCallback() + { } + +public: + explicit MainThreadUpdateCallback(Promise* aPromise) + : mPromise(aPromise) + { + AssertIsOnMainThread(); + } + + void + UpdateSucceeded(ServiceWorkerRegistrationInfo* aRegistration) override + { + mPromise->MaybeResolveWithUndefined(); + } + + void + UpdateFailed(ErrorResult& aStatus) override + { + mPromise->MaybeReject(aStatus); + } +}; + +class UpdateResultRunnable final : public WorkerRunnable +{ + RefPtr<PromiseWorkerProxy> mPromiseProxy; + IPC::Message mSerializedErrorResult; + + ~UpdateResultRunnable() + {} + +public: + UpdateResultRunnable(PromiseWorkerProxy* aPromiseProxy, ErrorResult& aStatus) + : WorkerRunnable(aPromiseProxy->GetWorkerPrivate()) + , mPromiseProxy(aPromiseProxy) + { + // ErrorResult is not thread safe. Serialize it for transfer across + // threads. + IPC::WriteParam(&mSerializedErrorResult, aStatus); + aStatus.SuppressException(); + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + // Deserialize the ErrorResult now that we are back in the worker + // thread. + ErrorResult status; + PickleIterator iter = PickleIterator(mSerializedErrorResult); + Unused << IPC::ReadParam(&mSerializedErrorResult, &iter, &status); + + Promise* promise = mPromiseProxy->WorkerPromise(); + if (status.Failed()) { + promise->MaybeReject(status); + } else { + promise->MaybeResolveWithUndefined(); + } + status.SuppressException(); + mPromiseProxy->CleanUp(); + return true; + } +}; + +class WorkerThreadUpdateCallback final : public ServiceWorkerUpdateFinishCallback +{ + RefPtr<PromiseWorkerProxy> mPromiseProxy; + + ~WorkerThreadUpdateCallback() + { + } + +public: + explicit WorkerThreadUpdateCallback(PromiseWorkerProxy* aPromiseProxy) + : mPromiseProxy(aPromiseProxy) + { + AssertIsOnMainThread(); + } + + void + UpdateSucceeded(ServiceWorkerRegistrationInfo* aRegistration) override + { + ErrorResult rv(NS_OK); + Finish(rv); + } + + void + UpdateFailed(ErrorResult& aStatus) override + { + Finish(aStatus); + } + + void + Finish(ErrorResult& aStatus) + { + if (!mPromiseProxy) { + return; + } + + RefPtr<PromiseWorkerProxy> proxy = mPromiseProxy.forget(); + + MutexAutoLock lock(proxy->Lock()); + if (proxy->CleanedUp()) { + return; + } + + RefPtr<UpdateResultRunnable> r = + new UpdateResultRunnable(proxy, aStatus); + r->Dispatch(); + } +}; + +class UpdateRunnable final : public Runnable +{ +public: + UpdateRunnable(PromiseWorkerProxy* aPromiseProxy, + const nsAString& aScope) + : mPromiseProxy(aPromiseProxy) + , mScope(aScope) + {} + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + ErrorResult result; + + nsCOMPtr<nsIPrincipal> principal; + // UpdateInternal may try to reject the promise synchronously leading + // to a deadlock. + { + MutexAutoLock lock(mPromiseProxy->Lock()); + if (mPromiseProxy->CleanedUp()) { + return NS_OK; + } + + principal = mPromiseProxy->GetWorkerPrivate()->GetPrincipal(); + } + MOZ_ASSERT(principal); + + RefPtr<WorkerThreadUpdateCallback> cb = + new WorkerThreadUpdateCallback(mPromiseProxy); + UpdateInternal(principal, mScope, cb); + return NS_OK; + } + +private: + ~UpdateRunnable() + {} + + RefPtr<PromiseWorkerProxy> mPromiseProxy; + const nsString mScope; +}; + +class UnregisterCallback final : public nsIServiceWorkerUnregisterCallback +{ + RefPtr<Promise> mPromise; + +public: + NS_DECL_ISUPPORTS + + explicit UnregisterCallback(Promise* aPromise) + : mPromise(aPromise) + { + MOZ_ASSERT(mPromise); + } + + NS_IMETHOD + UnregisterSucceeded(bool aState) override + { + AssertIsOnMainThread(); + mPromise->MaybeResolve(aState); + return NS_OK; + } + + NS_IMETHOD + UnregisterFailed() override + { + AssertIsOnMainThread(); + + mPromise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); + return NS_OK; + } + +private: + ~UnregisterCallback() + { } +}; + +NS_IMPL_ISUPPORTS(UnregisterCallback, nsIServiceWorkerUnregisterCallback) + +class FulfillUnregisterPromiseRunnable final : public WorkerRunnable +{ + RefPtr<PromiseWorkerProxy> mPromiseWorkerProxy; + Maybe<bool> mState; +public: + FulfillUnregisterPromiseRunnable(PromiseWorkerProxy* aProxy, + Maybe<bool> aState) + : WorkerRunnable(aProxy->GetWorkerPrivate()) + , mPromiseWorkerProxy(aProxy) + , mState(aState) + { + AssertIsOnMainThread(); + MOZ_ASSERT(mPromiseWorkerProxy); + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + RefPtr<Promise> promise = mPromiseWorkerProxy->WorkerPromise(); + if (mState.isSome()) { + promise->MaybeResolve(mState.value()); + } else { + promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); + } + + mPromiseWorkerProxy->CleanUp(); + return true; + } +}; + +class WorkerUnregisterCallback final : public nsIServiceWorkerUnregisterCallback +{ + RefPtr<PromiseWorkerProxy> mPromiseWorkerProxy; +public: + NS_DECL_ISUPPORTS + + explicit WorkerUnregisterCallback(PromiseWorkerProxy* aProxy) + : mPromiseWorkerProxy(aProxy) + { + MOZ_ASSERT(aProxy); + } + + NS_IMETHOD + UnregisterSucceeded(bool aState) override + { + AssertIsOnMainThread(); + Finish(Some(aState)); + return NS_OK; + } + + NS_IMETHOD + UnregisterFailed() override + { + AssertIsOnMainThread(); + Finish(Nothing()); + return NS_OK; + } + +private: + ~WorkerUnregisterCallback() + {} + + void + Finish(Maybe<bool> aState) + { + AssertIsOnMainThread(); + if (!mPromiseWorkerProxy) { + return; + } + + RefPtr<PromiseWorkerProxy> proxy = mPromiseWorkerProxy.forget(); + MutexAutoLock lock(proxy->Lock()); + if (proxy->CleanedUp()) { + return; + } + + RefPtr<WorkerRunnable> r = + new FulfillUnregisterPromiseRunnable(proxy, aState); + + r->Dispatch(); + } +}; + +NS_IMPL_ISUPPORTS(WorkerUnregisterCallback, nsIServiceWorkerUnregisterCallback); + +/* + * If the worker goes away, we still continue to unregister, but we don't try to + * resolve the worker Promise (which doesn't exist by that point). + */ +class StartUnregisterRunnable final : public Runnable +{ + RefPtr<PromiseWorkerProxy> mPromiseWorkerProxy; + const nsString mScope; + +public: + StartUnregisterRunnable(PromiseWorkerProxy* aProxy, + const nsAString& aScope) + : mPromiseWorkerProxy(aProxy) + , mScope(aScope) + { + MOZ_ASSERT(aProxy); + } + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + + // XXXnsm: There is a rare chance of this failing if the worker gets + // destroyed. In that case, unregister() called from a SW is no longer + // guaranteed to run. We should fix this by having a main thread proxy + // maintain a strongref to ServiceWorkerRegistrationInfo and use its + // principal. Can that be trusted? + nsCOMPtr<nsIPrincipal> principal; + { + MutexAutoLock lock(mPromiseWorkerProxy->Lock()); + if (mPromiseWorkerProxy->CleanedUp()) { + return NS_OK; + } + + WorkerPrivate* worker = mPromiseWorkerProxy->GetWorkerPrivate(); + MOZ_ASSERT(worker); + principal = worker->GetPrincipal(); + } + MOZ_ASSERT(principal); + + RefPtr<WorkerUnregisterCallback> cb = + new WorkerUnregisterCallback(mPromiseWorkerProxy); + nsCOMPtr<nsIServiceWorkerManager> swm = + mozilla::services::GetServiceWorkerManager(); + nsresult rv = swm->Unregister(principal, cb, mScope); + if (NS_WARN_IF(NS_FAILED(rv))) { + cb->UnregisterFailed(); + } + + return NS_OK; + } +}; +} // namespace + +already_AddRefed<Promise> +ServiceWorkerRegistrationMainThread::Update(ErrorResult& aRv) +{ + AssertIsOnMainThread(); + nsCOMPtr<nsIGlobalObject> go = do_QueryInterface(GetOwner()); + if (!go) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(go, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + nsCOMPtr<nsIDocument> doc = GetOwner()->GetExtantDoc(); + MOZ_ASSERT(doc); + + RefPtr<MainThreadUpdateCallback> cb = + new MainThreadUpdateCallback(promise); + UpdateInternal(doc->NodePrincipal(), mScope, cb); + + return promise.forget(); +} + +already_AddRefed<Promise> +ServiceWorkerRegistrationMainThread::Unregister(ErrorResult& aRv) +{ + AssertIsOnMainThread(); + nsCOMPtr<nsIGlobalObject> go = do_QueryInterface(GetOwner()); + if (!go) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + // Although the spec says that the same-origin checks should also be done + // asynchronously, we do them in sync because the Promise created by the + // WebIDL infrastructure due to a returned error will be resolved + // asynchronously. We aren't making any internal state changes in these + // checks, so ordering of multiple calls is not affected. + nsCOMPtr<nsIDocument> document = GetOwner()->GetExtantDoc(); + if (!document) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsCOMPtr<nsIURI> scopeURI; + nsCOMPtr<nsIURI> baseURI = document->GetBaseURI(); + // "If the origin of scope is not client's origin..." + nsresult rv = NS_NewURI(getter_AddRefs(scopeURI), mScope, nullptr, baseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + nsCOMPtr<nsIPrincipal> documentPrincipal = document->NodePrincipal(); + rv = documentPrincipal->CheckMayLoad(scopeURI, true /* report */, + false /* allowIfInheritsPrinciple */); + if (NS_FAILED(rv)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + nsAutoCString uriSpec; + aRv = scopeURI->GetSpecIgnoringRef(uriSpec); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + nsCOMPtr<nsIServiceWorkerManager> swm = + mozilla::services::GetServiceWorkerManager(); + + RefPtr<Promise> promise = Promise::Create(go, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + RefPtr<UnregisterCallback> cb = new UnregisterCallback(promise); + + NS_ConvertUTF8toUTF16 scope(uriSpec); + aRv = swm->Unregister(documentPrincipal, cb, scope); + if (aRv.Failed()) { + return nullptr; + } + + return promise.forget(); +} + +// Notification API extension. +already_AddRefed<Promise> +ServiceWorkerRegistrationMainThread::ShowNotification(JSContext* aCx, + const nsAString& aTitle, + const NotificationOptions& aOptions, + ErrorResult& aRv) +{ + AssertIsOnMainThread(); + nsCOMPtr<nsPIDOMWindowInner> window = GetOwner(); + if (NS_WARN_IF(!window)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + nsCOMPtr<nsIDocument> doc = window->GetExtantDoc(); + if (NS_WARN_IF(!doc)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<ServiceWorker> worker = GetActive(); + if (!worker) { + aRv.ThrowTypeError<MSG_NO_ACTIVE_WORKER>(mScope); + return nullptr; + } + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(window); + RefPtr<Promise> p = + Notification::ShowPersistentNotification(aCx, global, mScope, aTitle, + aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return p.forget(); +} + +already_AddRefed<Promise> +ServiceWorkerRegistrationMainThread::GetNotifications(const GetNotificationOptions& aOptions, ErrorResult& aRv) +{ + AssertIsOnMainThread(); + nsCOMPtr<nsPIDOMWindowInner> window = GetOwner(); + if (NS_WARN_IF(!window)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + return Notification::Get(window, aOptions, mScope, aRv); +} + +already_AddRefed<PushManager> +ServiceWorkerRegistrationMainThread::GetPushManager(JSContext* aCx, + ErrorResult& aRv) +{ + AssertIsOnMainThread(); + + if (!mPushManager) { + nsCOMPtr<nsIGlobalObject> globalObject = do_QueryInterface(GetOwner()); + + if (!globalObject) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + GlobalObject global(aCx, globalObject->GetGlobalJSObject()); + mPushManager = PushManager::Constructor(global, mScope, aRv); + if (aRv.Failed()) { + return nullptr; + } + } + + RefPtr<PushManager> ret = mPushManager; + return ret.forget(); +} + +//////////////////////////////////////////////////// +// Worker Thread implementation + +class ServiceWorkerRegistrationWorkerThread final : public ServiceWorkerRegistration + , public WorkerHolder +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ServiceWorkerRegistrationWorkerThread, + ServiceWorkerRegistration) + + ServiceWorkerRegistrationWorkerThread(WorkerPrivate* aWorkerPrivate, + const nsAString& aScope); + + already_AddRefed<Promise> + Update(ErrorResult& aRv) override; + + already_AddRefed<Promise> + Unregister(ErrorResult& aRv) override; + + // Partial interface from Notification API. + already_AddRefed<Promise> + ShowNotification(JSContext* aCx, + const nsAString& aTitle, + const NotificationOptions& aOptions, + ErrorResult& aRv) override; + + already_AddRefed<Promise> + GetNotifications(const GetNotificationOptions& aOptions, + ErrorResult& aRv) override; + + already_AddRefed<ServiceWorker> + GetInstalling() override; + + already_AddRefed<ServiceWorker> + GetWaiting() override; + + already_AddRefed<ServiceWorker> + GetActive() override; + + void + GetScope(nsAString& aScope) const override + { + aScope = mScope; + } + + bool + Notify(Status aStatus) override; + + already_AddRefed<PushManager> + GetPushManager(JSContext* aCx, ErrorResult& aRv) override; + +private: + ~ServiceWorkerRegistrationWorkerThread(); + + void + InitListener(); + + void + ReleaseListener(); + + WorkerPrivate* mWorkerPrivate; + RefPtr<WorkerListener> mListener; + + RefPtr<PushManager> mPushManager; +}; + +class WorkerListener final : public ServiceWorkerRegistrationListener +{ + // Accessed on the main thread. + WorkerPrivate* mWorkerPrivate; + nsString mScope; + bool mListeningForEvents; + + // Accessed on the worker thread. + ServiceWorkerRegistrationWorkerThread* mRegistration; + +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WorkerListener, override) + + WorkerListener(WorkerPrivate* aWorkerPrivate, + ServiceWorkerRegistrationWorkerThread* aReg) + : mWorkerPrivate(aWorkerPrivate) + , mListeningForEvents(false) + , mRegistration(aReg) + { + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(mRegistration); + // Copy scope so we can return it on the main thread. + mRegistration->GetScope(mScope); + } + + void + StartListeningForEvents() + { + AssertIsOnMainThread(); + MOZ_ASSERT(!mListeningForEvents); + MOZ_ASSERT(mWorkerPrivate); + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + // FIXME(nsm): Maybe the function shouldn't take an explicit scope. + swm->AddRegistrationEventListener(mScope, this); + mListeningForEvents = true; + } + } + + void + StopListeningForEvents() + { + AssertIsOnMainThread(); + + MOZ_ASSERT(mListeningForEvents); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + + // We aren't going to need this anymore and we shouldn't hold on since the + // worker will go away soon. + mWorkerPrivate = nullptr; + + if (swm) { + // FIXME(nsm): Maybe the function shouldn't take an explicit scope. + swm->RemoveRegistrationEventListener(mScope, this); + mListeningForEvents = false; + } + } + + // ServiceWorkerRegistrationListener + void + UpdateFound() override; + + void + InvalidateWorkers(WhichServiceWorker aWhichOnes) override + { + AssertIsOnMainThread(); + // FIXME(nsm); + } + + void + RegistrationRemoved() override + { + AssertIsOnMainThread(); + } + + void + GetScope(nsAString& aScope) const override + { + aScope = mScope; + } + + ServiceWorkerRegistrationWorkerThread* + GetRegistration() const + { + if (mWorkerPrivate) { + mWorkerPrivate->AssertIsOnWorkerThread(); + } + return mRegistration; + } + + void + ClearRegistration() + { + if (mWorkerPrivate) { + mWorkerPrivate->AssertIsOnWorkerThread(); + } + mRegistration = nullptr; + } + +private: + ~WorkerListener() + { + MOZ_ASSERT(!mListeningForEvents); + } +}; + +NS_IMPL_ADDREF_INHERITED(ServiceWorkerRegistrationWorkerThread, ServiceWorkerRegistration) +NS_IMPL_RELEASE_INHERITED(ServiceWorkerRegistrationWorkerThread, ServiceWorkerRegistration) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(ServiceWorkerRegistrationWorkerThread) +NS_INTERFACE_MAP_END_INHERITING(ServiceWorkerRegistration) + +// Expanded macros since we need special behaviour to release the proxy. +NS_IMPL_CYCLE_COLLECTION_CLASS(ServiceWorkerRegistrationWorkerThread) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(ServiceWorkerRegistrationWorkerThread, + ServiceWorkerRegistration) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPushManager) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(ServiceWorkerRegistrationWorkerThread, + ServiceWorkerRegistration) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPushManager) + tmp->ReleaseListener(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +ServiceWorkerRegistrationWorkerThread::ServiceWorkerRegistrationWorkerThread(WorkerPrivate* aWorkerPrivate, + const nsAString& aScope) + : ServiceWorkerRegistration(nullptr, aScope) + , mWorkerPrivate(aWorkerPrivate) +{ + InitListener(); +} + +ServiceWorkerRegistrationWorkerThread::~ServiceWorkerRegistrationWorkerThread() +{ + ReleaseListener(); + MOZ_ASSERT(!mListener); +} + +already_AddRefed<workers::ServiceWorker> +ServiceWorkerRegistrationWorkerThread::GetInstalling() +{ + // FIXME(nsm): Will be implemented after Bug 1113522. + return nullptr; +} + +already_AddRefed<ServiceWorker> +ServiceWorkerRegistrationWorkerThread::GetWaiting() +{ + // FIXME(nsm): Will be implemented after Bug 1113522. + return nullptr; +} + +already_AddRefed<ServiceWorker> +ServiceWorkerRegistrationWorkerThread::GetActive() +{ + // FIXME(nsm): Will be implemented after Bug 1113522. + return nullptr; +} + +already_AddRefed<Promise> +ServiceWorkerRegistrationWorkerThread::Update(ErrorResult& aRv) +{ + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + worker->AssertIsOnWorkerThread(); + + RefPtr<Promise> promise = Promise::Create(worker->GlobalScope(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Avoid infinite update loops by ignoring update() calls during top + // level script evaluation. See: + // https://github.com/slightlyoff/ServiceWorker/issues/800 + if (worker->LoadScriptAsPartOfLoadingServiceWorkerScript()) { + promise->MaybeResolveWithUndefined(); + return promise.forget(); + } + + RefPtr<PromiseWorkerProxy> proxy = PromiseWorkerProxy::Create(worker, promise); + if (!proxy) { + aRv.Throw(NS_ERROR_DOM_ABORT_ERR); + return nullptr; + } + + RefPtr<UpdateRunnable> r = new UpdateRunnable(proxy, mScope); + MOZ_ALWAYS_SUCCEEDS(worker->DispatchToMainThread(r.forget())); + + return promise.forget(); +} + +already_AddRefed<Promise> +ServiceWorkerRegistrationWorkerThread::Unregister(ErrorResult& aRv) +{ + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + worker->AssertIsOnWorkerThread(); + + if (!worker->IsServiceWorker()) { + // For other workers, the registration probably originated from + // getRegistration(), so we may have to validate origin etc. Let's do this + // this later. + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(worker->GlobalScope(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<PromiseWorkerProxy> proxy = PromiseWorkerProxy::Create(worker, promise); + if (!proxy) { + aRv.Throw(NS_ERROR_DOM_ABORT_ERR); + return nullptr; + } + + RefPtr<StartUnregisterRunnable> r = new StartUnregisterRunnable(proxy, mScope); + MOZ_ALWAYS_SUCCEEDS(worker->DispatchToMainThread(r.forget())); + + return promise.forget(); +} + +void +ServiceWorkerRegistrationWorkerThread::InitListener() +{ + MOZ_ASSERT(!mListener); + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + worker->AssertIsOnWorkerThread(); + + mListener = new WorkerListener(worker, this); + if (!HoldWorker(worker, Closing)) { + mListener = nullptr; + NS_WARNING("Could not add feature"); + return; + } + + nsCOMPtr<nsIRunnable> r = + NewRunnableMethod(mListener, &WorkerListener::StartListeningForEvents); + MOZ_ALWAYS_SUCCEEDS(worker->DispatchToMainThread(r.forget())); +} + +void +ServiceWorkerRegistrationWorkerThread::ReleaseListener() +{ + if (!mListener) { + return; + } + + // We can assert worker here, because: + // 1) We always HoldWorker, so if the worker has shutdown already, we'll + // have received Notify and removed it. If HoldWorker had failed, + // mListener will be null and we won't reach here. + // 2) Otherwise, worker is still around even if we are going away. + mWorkerPrivate->AssertIsOnWorkerThread(); + ReleaseWorker(); + + mListener->ClearRegistration(); + + nsCOMPtr<nsIRunnable> r = + NewRunnableMethod(mListener, &WorkerListener::StopListeningForEvents); + MOZ_ALWAYS_SUCCEEDS(mWorkerPrivate->DispatchToMainThread(r.forget())); + + mListener = nullptr; + mWorkerPrivate = nullptr; +} + +bool +ServiceWorkerRegistrationWorkerThread::Notify(Status aStatus) +{ + ReleaseListener(); + return true; +} + +class FireUpdateFoundRunnable final : public WorkerRunnable +{ + RefPtr<WorkerListener> mListener; +public: + FireUpdateFoundRunnable(WorkerPrivate* aWorkerPrivate, + WorkerListener* aListener) + : WorkerRunnable(aWorkerPrivate) + , mListener(aListener) + { + // Need this assertion for now since runnables which modify busy count can + // only be dispatched from parent thread to worker thread and we don't deal + // with nested workers. SW threads can't be nested. + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + ServiceWorkerRegistrationWorkerThread* reg = mListener->GetRegistration(); + if (reg) { + reg->DispatchTrustedEvent(NS_LITERAL_STRING("updatefound")); + } + return true; + } +}; + +void +WorkerListener::UpdateFound() +{ + AssertIsOnMainThread(); + if (mWorkerPrivate) { + RefPtr<FireUpdateFoundRunnable> r = + new FireUpdateFoundRunnable(mWorkerPrivate, this); + Unused << NS_WARN_IF(!r->Dispatch()); + } +} + +// Notification API extension. +already_AddRefed<Promise> +ServiceWorkerRegistrationWorkerThread::ShowNotification(JSContext* aCx, + const nsAString& aTitle, + const NotificationOptions& aOptions, + ErrorResult& aRv) +{ + // Until Bug 1131324 exposes ServiceWorkerContainer on workers, + // ShowPersistentNotification() checks for valid active worker while it is + // also verifying scope so that we block the worker on the main thread only + // once. + RefPtr<Promise> p = + Notification::ShowPersistentNotification(aCx, mWorkerPrivate->GlobalScope(), + mScope, aTitle, aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return p.forget(); +} + +already_AddRefed<Promise> +ServiceWorkerRegistrationWorkerThread::GetNotifications(const GetNotificationOptions& aOptions, + ErrorResult& aRv) +{ + return Notification::WorkerGet(mWorkerPrivate, aOptions, mScope, aRv); +} + +already_AddRefed<PushManager> +ServiceWorkerRegistrationWorkerThread::GetPushManager(JSContext* aCx, ErrorResult& aRv) +{ + if (!mPushManager) { + mPushManager = new PushManager(mScope); + } + + RefPtr<PushManager> ret = mPushManager; + return ret.forget(); +} + +//////////////////////////////////////////////////// +// Base class implementation + +NS_IMPL_ADDREF_INHERITED(ServiceWorkerRegistration, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(ServiceWorkerRegistration, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(ServiceWorkerRegistration) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +ServiceWorkerRegistration::ServiceWorkerRegistration(nsPIDOMWindowInner* aWindow, + const nsAString& aScope) + : DOMEventTargetHelper(aWindow) + , mScope(aScope) +{} + +JSObject* +ServiceWorkerRegistration::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) +{ + return ServiceWorkerRegistrationBinding::Wrap(aCx, this, aGivenProto); +} + +/* static */ already_AddRefed<ServiceWorkerRegistration> +ServiceWorkerRegistration::CreateForMainThread(nsPIDOMWindowInner* aWindow, + const nsAString& aScope) +{ + MOZ_ASSERT(aWindow); + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<ServiceWorkerRegistration> registration = + new ServiceWorkerRegistrationMainThread(aWindow, aScope); + + return registration.forget(); +} + +/* static */ already_AddRefed<ServiceWorkerRegistration> +ServiceWorkerRegistration::CreateForWorker(workers::WorkerPrivate* aWorkerPrivate, + const nsAString& aScope) +{ + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<ServiceWorkerRegistration> registration = + new ServiceWorkerRegistrationWorkerThread(aWorkerPrivate, aScope); + + return registration.forget(); +} + +} // dom namespace +} // mozilla namespace diff --git a/dom/workers/ServiceWorkerRegistration.h b/dom/workers/ServiceWorkerRegistration.h new file mode 100644 index 000000000..5b0847e7b --- /dev/null +++ b/dom/workers/ServiceWorkerRegistration.h @@ -0,0 +1,124 @@ +/* -*- 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_ServiceWorkerRegistration_h +#define mozilla_dom_ServiceWorkerRegistration_h + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/ServiceWorkerBinding.h" +#include "mozilla/dom/ServiceWorkerCommon.h" +#include "mozilla/dom/workers/bindings/WorkerHolder.h" +#include "nsContentUtils.h" // Required for nsContentUtils::PushEnabled + +// Support for Notification API extension. +#include "mozilla/dom/NotificationBinding.h" + +class nsPIDOMWindowInner; + +namespace mozilla { +namespace dom { + +class Promise; +class PushManager; +class WorkerListener; + +namespace workers { +class ServiceWorker; +class WorkerPrivate; +} // namespace workers + +// Used by ServiceWorkerManager to notify ServiceWorkerRegistrations of +// updatefound event and invalidating ServiceWorker instances. +class ServiceWorkerRegistrationListener +{ +public: + NS_IMETHOD_(MozExternalRefCountType) AddRef() = 0; + NS_IMETHOD_(MozExternalRefCountType) Release() = 0; + + virtual void + UpdateFound() = 0; + + virtual void + InvalidateWorkers(WhichServiceWorker aWhichOnes) = 0; + + virtual void + RegistrationRemoved() = 0; + + virtual void + GetScope(nsAString& aScope) const = 0; +}; + +class ServiceWorkerRegistration : public DOMEventTargetHelper +{ +public: + NS_DECL_ISUPPORTS_INHERITED + + IMPL_EVENT_HANDLER(updatefound) + + static bool + Visible(JSContext* aCx, JSObject* aObj); + + static bool + NotificationAPIVisible(JSContext* aCx, JSObject* aObj); + + + static already_AddRefed<ServiceWorkerRegistration> + CreateForMainThread(nsPIDOMWindowInner* aWindow, + const nsAString& aScope); + + static already_AddRefed<ServiceWorkerRegistration> + CreateForWorker(workers::WorkerPrivate* aWorkerPrivate, + const nsAString& aScope); + + JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + virtual already_AddRefed<workers::ServiceWorker> + GetInstalling() = 0; + + virtual already_AddRefed<workers::ServiceWorker> + GetWaiting() = 0; + + virtual already_AddRefed<workers::ServiceWorker> + GetActive() = 0; + + virtual void + GetScope(nsAString& aScope) const = 0; + + virtual already_AddRefed<Promise> + Update(ErrorResult& aRv) = 0; + + virtual already_AddRefed<Promise> + Unregister(ErrorResult& aRv) = 0; + + virtual already_AddRefed<PushManager> + GetPushManager(JSContext* aCx, ErrorResult& aRv) = 0; + + virtual already_AddRefed<Promise> + ShowNotification(JSContext* aCx, + const nsAString& aTitle, + const NotificationOptions& aOptions, + ErrorResult& aRv) = 0; + + virtual already_AddRefed<Promise> + GetNotifications(const GetNotificationOptions& aOptions, + ErrorResult& aRv) = 0; + +protected: + ServiceWorkerRegistration(nsPIDOMWindowInner* aWindow, + const nsAString& aScope); + + virtual ~ServiceWorkerRegistration() + { } + + const nsString mScope; +}; + + +} // namespace dom +} // namespace mozilla + +#endif /* mozilla_dom_ServiceWorkerRegistration_h */ diff --git a/dom/workers/ServiceWorkerRegistrationInfo.cpp b/dom/workers/ServiceWorkerRegistrationInfo.cpp new file mode 100644 index 000000000..26ad74bda --- /dev/null +++ b/dom/workers/ServiceWorkerRegistrationInfo.cpp @@ -0,0 +1,546 @@ +/* -*- 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 "ServiceWorkerRegistrationInfo.h" + +BEGIN_WORKERS_NAMESPACE + +namespace { + +class ContinueActivateRunnable final : public LifeCycleEventCallback +{ + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration; + bool mSuccess; + +public: + explicit ContinueActivateRunnable(const nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration) + : mRegistration(aRegistration) + , mSuccess(false) + { + AssertIsOnMainThread(); + } + + void + SetResult(bool aResult) override + { + mSuccess = aResult; + } + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + mRegistration->FinishActivate(mSuccess); + mRegistration = nullptr; + return NS_OK; + } +}; + +} // anonymous namespace + +void +ServiceWorkerRegistrationInfo::Clear() +{ + if (mEvaluatingWorker) { + mEvaluatingWorker = nullptr; + } + + if (mInstallingWorker) { + mInstallingWorker->UpdateState(ServiceWorkerState::Redundant); + mInstallingWorker->WorkerPrivate()->NoteDeadServiceWorkerInfo(); + mInstallingWorker = nullptr; + // FIXME(nsm): Abort any inflight requests from installing worker. + } + + if (mWaitingWorker) { + mWaitingWorker->UpdateState(ServiceWorkerState::Redundant); + mWaitingWorker->WorkerPrivate()->NoteDeadServiceWorkerInfo(); + mWaitingWorker = nullptr; + } + + if (mActiveWorker) { + mActiveWorker->UpdateState(ServiceWorkerState::Redundant); + mActiveWorker->WorkerPrivate()->NoteDeadServiceWorkerInfo(); + mActiveWorker = nullptr; + } + + NotifyListenersOnChange(WhichServiceWorker::INSTALLING_WORKER | + WhichServiceWorker::WAITING_WORKER | + WhichServiceWorker::ACTIVE_WORKER); +} + +ServiceWorkerRegistrationInfo::ServiceWorkerRegistrationInfo(const nsACString& aScope, + nsIPrincipal* aPrincipal) + : mControlledDocumentsCounter(0) + , mUpdateState(NoUpdate) + , mLastUpdateCheckTime(0) + , mScope(aScope) + , mPrincipal(aPrincipal) + , mPendingUninstall(false) +{} + +ServiceWorkerRegistrationInfo::~ServiceWorkerRegistrationInfo() +{ + if (IsControllingDocuments()) { + NS_WARNING("ServiceWorkerRegistrationInfo is still controlling documents. This can be a bug or a leak in ServiceWorker API or in any other API that takes the document alive."); + } +} + +NS_IMPL_ISUPPORTS(ServiceWorkerRegistrationInfo, nsIServiceWorkerRegistrationInfo) + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetPrincipal(nsIPrincipal** aPrincipal) +{ + AssertIsOnMainThread(); + NS_ADDREF(*aPrincipal = mPrincipal); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetScope(nsAString& aScope) +{ + AssertIsOnMainThread(); + CopyUTF8toUTF16(mScope, aScope); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetScriptSpec(nsAString& aScriptSpec) +{ + AssertIsOnMainThread(); + RefPtr<ServiceWorkerInfo> newest = Newest(); + if (newest) { + CopyUTF8toUTF16(newest->ScriptSpec(), aScriptSpec); + } + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetInstallingWorker(nsIServiceWorkerInfo **aResult) +{ + AssertIsOnMainThread(); + nsCOMPtr<nsIServiceWorkerInfo> info = do_QueryInterface(mInstallingWorker); + info.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetWaitingWorker(nsIServiceWorkerInfo **aResult) +{ + AssertIsOnMainThread(); + nsCOMPtr<nsIServiceWorkerInfo> info = do_QueryInterface(mWaitingWorker); + info.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetActiveWorker(nsIServiceWorkerInfo **aResult) +{ + AssertIsOnMainThread(); + nsCOMPtr<nsIServiceWorkerInfo> info = do_QueryInterface(mActiveWorker); + info.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::GetWorkerByID(uint64_t aID, nsIServiceWorkerInfo **aResult) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aResult); + + RefPtr<ServiceWorkerInfo> info = GetServiceWorkerInfoById(aID); + // It is ok to return null for a missing service worker info. + info.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::AddListener( + nsIServiceWorkerRegistrationInfoListener *aListener) +{ + AssertIsOnMainThread(); + + if (!aListener || mListeners.Contains(aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mListeners.AppendElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerRegistrationInfo::RemoveListener( + nsIServiceWorkerRegistrationInfoListener *aListener) +{ + AssertIsOnMainThread(); + + if (!aListener || !mListeners.Contains(aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mListeners.RemoveElement(aListener); + + return NS_OK; +} + +already_AddRefed<ServiceWorkerInfo> +ServiceWorkerRegistrationInfo::GetServiceWorkerInfoById(uint64_t aId) +{ + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerInfo> serviceWorker; + if (mEvaluatingWorker && mEvaluatingWorker->ID() == aId) { + serviceWorker = mEvaluatingWorker; + } else if (mInstallingWorker && mInstallingWorker->ID() == aId) { + serviceWorker = mInstallingWorker; + } else if (mWaitingWorker && mWaitingWorker->ID() == aId) { + serviceWorker = mWaitingWorker; + } else if (mActiveWorker && mActiveWorker->ID() == aId) { + serviceWorker = mActiveWorker; + } + + return serviceWorker.forget(); +} + +void +ServiceWorkerRegistrationInfo::TryToActivateAsync() +{ + MOZ_ALWAYS_SUCCEEDS( + NS_DispatchToMainThread(NewRunnableMethod(this, + &ServiceWorkerRegistrationInfo::TryToActivate))); +} + +/* + * TryToActivate should not be called directly, use TryToActivateAsync instead. + */ +void +ServiceWorkerRegistrationInfo::TryToActivate() +{ + AssertIsOnMainThread(); + bool controlling = IsControllingDocuments(); + bool skipWaiting = mWaitingWorker && mWaitingWorker->SkipWaitingFlag(); + bool idle = IsIdle(); + if (idle && (!controlling || skipWaiting)) { + Activate(); + } +} + +void +ServiceWorkerRegistrationInfo::Activate() +{ + if (!mWaitingWorker) { + return; + } + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown began during async activation step + return; + } + + TransitionWaitingToActive(); + + // FIXME(nsm): Unlink appcache if there is one. + + swm->CheckPendingReadyPromises(); + + // "Queue a task to fire a simple event named controllerchange..." + nsCOMPtr<nsIRunnable> controllerChangeRunnable = + NewRunnableMethod<RefPtr<ServiceWorkerRegistrationInfo>>( + swm, &ServiceWorkerManager::FireControllerChange, this); + NS_DispatchToMainThread(controllerChangeRunnable); + + nsCOMPtr<nsIRunnable> failRunnable = + NewRunnableMethod<bool>(this, + &ServiceWorkerRegistrationInfo::FinishActivate, + false /* success */); + + nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> handle( + new nsMainThreadPtrHolder<ServiceWorkerRegistrationInfo>(this)); + RefPtr<LifeCycleEventCallback> callback = new ContinueActivateRunnable(handle); + + ServiceWorkerPrivate* workerPrivate = mActiveWorker->WorkerPrivate(); + MOZ_ASSERT(workerPrivate); + nsresult rv = workerPrivate->SendLifeCycleEvent(NS_LITERAL_STRING("activate"), + callback, failRunnable); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(failRunnable)); + return; + } +} + +void +ServiceWorkerRegistrationInfo::FinishActivate(bool aSuccess) +{ + if (mPendingUninstall || !mActiveWorker || + mActiveWorker->State() != ServiceWorkerState::Activating) { + return; + } + + // Activation never fails, so aSuccess is ignored. + mActiveWorker->UpdateState(ServiceWorkerState::Activated); + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown started during async activation completion step + return; + } + swm->StoreRegistration(mPrincipal, this); +} + +void +ServiceWorkerRegistrationInfo::RefreshLastUpdateCheckTime() +{ + AssertIsOnMainThread(); + mLastUpdateCheckTime = PR_IntervalNow() / PR_MSEC_PER_SEC; +} + +bool +ServiceWorkerRegistrationInfo::IsLastUpdateCheckTimeOverOneDay() const +{ + AssertIsOnMainThread(); + + // For testing. + if (Preferences::GetBool("dom.serviceWorkers.testUpdateOverOneDay")) { + return true; + } + + const uint64_t kSecondsPerDay = 86400; + const uint64_t now = PR_IntervalNow() / PR_MSEC_PER_SEC; + + if ((now - mLastUpdateCheckTime) > kSecondsPerDay) { + return true; + } + return false; +} + +void +ServiceWorkerRegistrationInfo::NotifyListenersOnChange(WhichServiceWorker aChangedWorkers) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aChangedWorkers & (WhichServiceWorker::INSTALLING_WORKER | + WhichServiceWorker::WAITING_WORKER | + WhichServiceWorker::ACTIVE_WORKER)); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown started + return; + } + + swm->InvalidateServiceWorkerRegistrationWorker(this, aChangedWorkers); + + nsTArray<nsCOMPtr<nsIServiceWorkerRegistrationInfoListener>> listeners(mListeners); + for (size_t index = 0; index < listeners.Length(); ++index) { + listeners[index]->OnChange(); + } +} + +void +ServiceWorkerRegistrationInfo::MaybeScheduleTimeCheckAndUpdate() +{ + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // shutting down, do nothing + return; + } + + if (mUpdateState == NoUpdate) { + mUpdateState = NeedTimeCheckAndUpdate; + } + + swm->ScheduleUpdateTimer(mPrincipal, mScope); +} + +void +ServiceWorkerRegistrationInfo::MaybeScheduleUpdate() +{ + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // shutting down, do nothing + return; + } + + mUpdateState = NeedUpdate; + + swm->ScheduleUpdateTimer(mPrincipal, mScope); +} + +bool +ServiceWorkerRegistrationInfo::CheckAndClearIfUpdateNeeded() +{ + AssertIsOnMainThread(); + + bool result = mUpdateState == NeedUpdate || + (mUpdateState == NeedTimeCheckAndUpdate && + IsLastUpdateCheckTimeOverOneDay()); + + mUpdateState = NoUpdate; + + return result; +} + +ServiceWorkerInfo* +ServiceWorkerRegistrationInfo::GetEvaluating() const +{ + AssertIsOnMainThread(); + return mEvaluatingWorker; +} + +ServiceWorkerInfo* +ServiceWorkerRegistrationInfo::GetInstalling() const +{ + AssertIsOnMainThread(); + return mInstallingWorker; +} + +ServiceWorkerInfo* +ServiceWorkerRegistrationInfo::GetWaiting() const +{ + AssertIsOnMainThread(); + return mWaitingWorker; +} + +ServiceWorkerInfo* +ServiceWorkerRegistrationInfo::GetActive() const +{ + AssertIsOnMainThread(); + return mActiveWorker; +} + +void +ServiceWorkerRegistrationInfo::SetEvaluating(ServiceWorkerInfo* aServiceWorker) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aServiceWorker); + MOZ_ASSERT(!mEvaluatingWorker); + MOZ_ASSERT(!mInstallingWorker); + MOZ_ASSERT(mWaitingWorker != aServiceWorker); + MOZ_ASSERT(mActiveWorker != aServiceWorker); + + mEvaluatingWorker = aServiceWorker; +} + +void +ServiceWorkerRegistrationInfo::ClearEvaluating() +{ + AssertIsOnMainThread(); + + if (!mEvaluatingWorker) { + return; + } + + mEvaluatingWorker->UpdateState(ServiceWorkerState::Redundant); + mEvaluatingWorker = nullptr; +} + +void +ServiceWorkerRegistrationInfo::ClearInstalling() +{ + AssertIsOnMainThread(); + + if (!mInstallingWorker) { + return; + } + + mInstallingWorker->UpdateState(ServiceWorkerState::Redundant); + mInstallingWorker = nullptr; + NotifyListenersOnChange(WhichServiceWorker::INSTALLING_WORKER); +} + +void +ServiceWorkerRegistrationInfo::TransitionEvaluatingToInstalling() +{ + AssertIsOnMainThread(); + MOZ_ASSERT(mEvaluatingWorker); + MOZ_ASSERT(!mInstallingWorker); + + mInstallingWorker = mEvaluatingWorker.forget(); + mInstallingWorker->UpdateState(ServiceWorkerState::Installing); + NotifyListenersOnChange(WhichServiceWorker::INSTALLING_WORKER); +} + +void +ServiceWorkerRegistrationInfo::TransitionInstallingToWaiting() +{ + AssertIsOnMainThread(); + MOZ_ASSERT(mInstallingWorker); + + if (mWaitingWorker) { + MOZ_ASSERT(mInstallingWorker->CacheName() != mWaitingWorker->CacheName()); + mWaitingWorker->UpdateState(ServiceWorkerState::Redundant); + } + + mWaitingWorker = mInstallingWorker.forget(); + mWaitingWorker->UpdateState(ServiceWorkerState::Installed); + NotifyListenersOnChange(WhichServiceWorker::INSTALLING_WORKER | + WhichServiceWorker::WAITING_WORKER); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + // browser shutdown began + return; + } + swm->StoreRegistration(mPrincipal, this); +} + +void +ServiceWorkerRegistrationInfo::SetActive(ServiceWorkerInfo* aServiceWorker) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aServiceWorker); + + // TODO: Assert installing, waiting, and active are nullptr once the SWM + // moves to the parent process. After that happens this code will + // only run for browser initialization and not for cross-process + // overrides. + MOZ_ASSERT(mInstallingWorker != aServiceWorker); + MOZ_ASSERT(mWaitingWorker != aServiceWorker); + MOZ_ASSERT(mActiveWorker != aServiceWorker); + + if (mActiveWorker) { + MOZ_ASSERT(aServiceWorker->CacheName() != mActiveWorker->CacheName()); + mActiveWorker->UpdateState(ServiceWorkerState::Redundant); + } + + // The active worker is being overriden due to initial load or + // another process activating a worker. Move straight to the + // Activated state. + mActiveWorker = aServiceWorker; + mActiveWorker->SetActivateStateUncheckedWithoutEvent(ServiceWorkerState::Activated); + NotifyListenersOnChange(WhichServiceWorker::ACTIVE_WORKER); +} + +void +ServiceWorkerRegistrationInfo::TransitionWaitingToActive() +{ + AssertIsOnMainThread(); + MOZ_ASSERT(mWaitingWorker); + + if (mActiveWorker) { + MOZ_ASSERT(mWaitingWorker->CacheName() != mActiveWorker->CacheName()); + mActiveWorker->UpdateState(ServiceWorkerState::Redundant); + } + + // We are transitioning from waiting to active normally, so go to + // the activating state. + mActiveWorker = mWaitingWorker.forget(); + mActiveWorker->UpdateState(ServiceWorkerState::Activating); + NotifyListenersOnChange(WhichServiceWorker::WAITING_WORKER | + WhichServiceWorker::ACTIVE_WORKER); +} + +bool +ServiceWorkerRegistrationInfo::IsIdle() const +{ + return !mActiveWorker || mActiveWorker->WorkerPrivate()->IsIdle(); +} + +END_WORKERS_NAMESPACE diff --git a/dom/workers/ServiceWorkerRegistrationInfo.h b/dom/workers/ServiceWorkerRegistrationInfo.h new file mode 100644 index 000000000..d2d217be0 --- /dev/null +++ b/dom/workers/ServiceWorkerRegistrationInfo.h @@ -0,0 +1,184 @@ +/* -*- 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_workers_serviceworkerregistrationinfo_h +#define mozilla_dom_workers_serviceworkerregistrationinfo_h + +#include "mozilla/dom/workers/ServiceWorkerInfo.h" + +namespace mozilla { +namespace dom { +namespace workers { + +class ServiceWorkerRegistrationInfo final + : public nsIServiceWorkerRegistrationInfo +{ + uint32_t mControlledDocumentsCounter; + + enum + { + NoUpdate, + NeedTimeCheckAndUpdate, + NeedUpdate + } mUpdateState; + + uint64_t mLastUpdateCheckTime; + + RefPtr<ServiceWorkerInfo> mEvaluatingWorker; + RefPtr<ServiceWorkerInfo> mActiveWorker; + RefPtr<ServiceWorkerInfo> mWaitingWorker; + RefPtr<ServiceWorkerInfo> mInstallingWorker; + + virtual ~ServiceWorkerRegistrationInfo(); + +public: + NS_DECL_ISUPPORTS + NS_DECL_NSISERVICEWORKERREGISTRATIONINFO + + const nsCString mScope; + + nsCOMPtr<nsIPrincipal> mPrincipal; + + nsTArray<nsCOMPtr<nsIServiceWorkerRegistrationInfoListener>> mListeners; + + // When unregister() is called on a registration, it is not immediately + // removed since documents may be controlled. It is marked as + // pendingUninstall and when all controlling documents go away, removed. + bool mPendingUninstall; + + ServiceWorkerRegistrationInfo(const nsACString& aScope, + nsIPrincipal* aPrincipal); + + already_AddRefed<ServiceWorkerInfo> + Newest() const + { + RefPtr<ServiceWorkerInfo> newest; + if (mInstallingWorker) { + newest = mInstallingWorker; + } else if (mWaitingWorker) { + newest = mWaitingWorker; + } else { + newest = mActiveWorker; + } + + return newest.forget(); + } + + already_AddRefed<ServiceWorkerInfo> + GetServiceWorkerInfoById(uint64_t aId); + + void + StartControllingADocument() + { + ++mControlledDocumentsCounter; + } + + void + StopControllingADocument() + { + MOZ_ASSERT(mControlledDocumentsCounter); + --mControlledDocumentsCounter; + } + + bool + IsControllingDocuments() const + { + return mActiveWorker && mControlledDocumentsCounter; + } + + void + Clear(); + + void + TryToActivateAsync(); + + void + TryToActivate(); + + void + Activate(); + + void + FinishActivate(bool aSuccess); + + void + RefreshLastUpdateCheckTime(); + + bool + IsLastUpdateCheckTimeOverOneDay() const; + + void + NotifyListenersOnChange(WhichServiceWorker aChangedWorkers); + + void + MaybeScheduleTimeCheckAndUpdate(); + + void + MaybeScheduleUpdate(); + + bool + CheckAndClearIfUpdateNeeded(); + + ServiceWorkerInfo* + GetEvaluating() const; + + ServiceWorkerInfo* + GetInstalling() const; + + ServiceWorkerInfo* + GetWaiting() const; + + ServiceWorkerInfo* + GetActive() const; + + // Set the given worker as the evaluating service worker. The worker + // state is not changed. + void + SetEvaluating(ServiceWorkerInfo* aServiceWorker); + + // Remove an existing evaluating worker, if present. The worker will + // be transitioned to the Redundant state. + void + ClearEvaluating(); + + // Remove an existing installing worker, if present. The worker will + // be transitioned to the Redundant state. + void + ClearInstalling(); + + // Transition the current evaluating worker to be the installing worker. The + // worker's state is update to Installing. + void + TransitionEvaluatingToInstalling(); + + // Transition the current installing worker to be the waiting worker. The + // worker's state is updated to Installed. + void + TransitionInstallingToWaiting(); + + // Override the current active worker. This is used during browser + // initialization to load persisted workers. Its also used to propagate + // active workers across child processes in e10s. This second use will + // go away once the ServiceWorkerManager moves to the parent process. + // The worker is transitioned to the Activated state. + void + SetActive(ServiceWorkerInfo* aServiceWorker); + + // Transition the current waiting worker to be the new active worker. The + // worker is updated to the Activating state. + void + TransitionWaitingToActive(); + + // Determine if the registration is actively performing work. + bool + IsIdle() const; +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_serviceworkerregistrationinfo_h 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 diff --git a/dom/workers/ServiceWorkerScriptCache.h b/dom/workers/ServiceWorkerScriptCache.h new file mode 100644 index 000000000..113ed0983 --- /dev/null +++ b/dom/workers/ServiceWorkerScriptCache.h @@ -0,0 +1,59 @@ +/* -*- 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_workers_ServiceWorkerScriptCache_h +#define mozilla_dom_workers_ServiceWorkerScriptCache_h + +#include "nsString.h" + +class nsILoadGroup; +class nsIPrincipal; + +namespace mozilla { +namespace dom { +namespace workers { + +class ServiceWorkerRegistrationInfo; + +namespace serviceWorkerScriptCache { + +nsresult +PurgeCache(nsIPrincipal* aPrincipal, const nsAString& aCacheName); + +nsresult +GenerateCacheName(nsAString& aName); + +class CompareCallback +{ +public: + /* + * If there is an error, ignore aInCacheAndEqual and aNewCacheName. + * On success, if the cached result and network result matched, + * aInCacheAndEqual will be true and no new cache name is passed, otherwise + * use the new cache name to load the ServiceWorker. + */ + virtual void + ComparisonResult(nsresult aStatus, + bool aInCacheAndEqual, + const nsAString& aNewCacheName, + const nsACString& aMaxScope) = 0; + + NS_IMETHOD_(MozExternalRefCountType) AddRef() = 0; + NS_IMETHOD_(MozExternalRefCountType) Release() = 0; +}; + +nsresult +Compare(ServiceWorkerRegistrationInfo* aRegistration, + nsIPrincipal* aPrincipal, const nsAString& aCacheName, + const nsAString& aURL, CompareCallback* aCallback, nsILoadGroup* aLoadGroup); + +} // namespace serviceWorkerScriptCache + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_ServiceWorkerScriptCache_h diff --git a/dom/workers/ServiceWorkerUnregisterJob.cpp b/dom/workers/ServiceWorkerUnregisterJob.cpp new file mode 100644 index 000000000..8fd76b63d --- /dev/null +++ b/dom/workers/ServiceWorkerUnregisterJob.cpp @@ -0,0 +1,151 @@ +/* -*- 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 "ServiceWorkerUnregisterJob.h" + +#include "nsIPushService.h" + +namespace mozilla { +namespace dom { +namespace workers { + +class ServiceWorkerUnregisterJob::PushUnsubscribeCallback final : + public nsIUnsubscribeResultCallback +{ +public: + NS_DECL_ISUPPORTS + + explicit PushUnsubscribeCallback(ServiceWorkerUnregisterJob* aJob) + : mJob(aJob) + { + AssertIsOnMainThread(); + } + + NS_IMETHOD + OnUnsubscribe(nsresult aStatus, bool) override + { + // Warn if unsubscribing fails, but don't prevent the worker from + // unregistering. + Unused << NS_WARN_IF(NS_FAILED(aStatus)); + mJob->Unregister(); + return NS_OK; + } + +private: + ~PushUnsubscribeCallback() + { + } + + RefPtr<ServiceWorkerUnregisterJob> mJob; +}; + +NS_IMPL_ISUPPORTS(ServiceWorkerUnregisterJob::PushUnsubscribeCallback, + nsIUnsubscribeResultCallback) + +ServiceWorkerUnregisterJob::ServiceWorkerUnregisterJob(nsIPrincipal* aPrincipal, + const nsACString& aScope, + bool aSendToParent) + : ServiceWorkerJob(Type::Unregister, aPrincipal, aScope, EmptyCString()) + , mResult(false) + , mSendToParent(aSendToParent) +{ +} + +bool +ServiceWorkerUnregisterJob::GetResult() const +{ + AssertIsOnMainThread(); + return mResult; +} + +ServiceWorkerUnregisterJob::~ServiceWorkerUnregisterJob() +{ +} + +void +ServiceWorkerUnregisterJob::AsyncExecute() +{ + AssertIsOnMainThread(); + + if (Canceled()) { + Finish(NS_ERROR_DOM_ABORT_ERR); + return; + } + + // Push API, section 5: "When a service worker registration is unregistered, + // any associated push subscription must be deactivated." To ensure the + // service worker registration isn't cleared as we're unregistering, we + // unsubscribe first. + nsCOMPtr<nsIPushService> pushService = + do_GetService("@mozilla.org/push/Service;1"); + if (NS_WARN_IF(!pushService)) { + Unregister(); + return; + } + nsCOMPtr<nsIUnsubscribeResultCallback> unsubscribeCallback = + new PushUnsubscribeCallback(this); + nsresult rv = pushService->Unsubscribe(NS_ConvertUTF8toUTF16(mScope), + mPrincipal, unsubscribeCallback); + if (NS_WARN_IF(NS_FAILED(rv))) { + Unregister(); + } +} + +void +ServiceWorkerUnregisterJob::Unregister() +{ + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (Canceled() || !swm) { + Finish(NS_ERROR_DOM_ABORT_ERR); + return; + } + + // Step 1 of the Unregister algorithm requires checking that the + // client origin matches the scope's origin. We perform this in + // registration->update() method directly since we don't have that + // client information available here. + + // "Let registration be the result of running [[Get Registration]] + // algorithm passing scope as the argument." + RefPtr<ServiceWorkerRegistrationInfo> registration = + swm->GetRegistration(mPrincipal, mScope); + if (!registration) { + // "If registration is null, then, resolve promise with false." + Finish(NS_OK); + return; + } + + // Note, we send the message to remove the registration from disk now even + // though we may only set the mPendingUninstall flag below. This is + // necessary to ensure the registration is removed if the controlled + // clients are closed by shutting down the browser. If the registration + // is resurrected by clearing mPendingUninstall then it should be saved + // to disk again. + if (mSendToParent && !registration->mPendingUninstall) { + swm->MaybeSendUnregister(mPrincipal, mScope); + } + + // "Set registration's uninstalling flag." + registration->mPendingUninstall = true; + + // "Resolve promise with true" + mResult = true; + InvokeResultCallbacks(NS_OK); + + // "If no service worker client is using registration..." + if (!registration->IsControllingDocuments() && registration->IsIdle()) { + // "Invoke [[Clear Registration]]..." + swm->RemoveRegistration(registration); + } + + Finish(NS_OK); +} + +} // namespace workers +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/ServiceWorkerUnregisterJob.h b/dom/workers/ServiceWorkerUnregisterJob.h new file mode 100644 index 000000000..976f15a2c --- /dev/null +++ b/dom/workers/ServiceWorkerUnregisterJob.h @@ -0,0 +1,45 @@ +/* -*- 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_workers_serviceworkerunregisterjob_h +#define mozilla_dom_workers_serviceworkerunregisterjob_h + +#include "ServiceWorkerJob.h" + +namespace mozilla { +namespace dom { +namespace workers { + +class ServiceWorkerUnregisterJob final : public ServiceWorkerJob +{ +public: + ServiceWorkerUnregisterJob(nsIPrincipal* aPrincipal, + const nsACString& aScope, + bool aSendToParent); + + bool + GetResult() const; + +private: + class PushUnsubscribeCallback; + + virtual ~ServiceWorkerUnregisterJob(); + + virtual void + AsyncExecute() override; + + void + Unregister(); + + bool mResult; + bool mSendToParent; +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_serviceworkerunregisterjob_h diff --git a/dom/workers/ServiceWorkerUpdateJob.cpp b/dom/workers/ServiceWorkerUpdateJob.cpp new file mode 100644 index 000000000..614fe4de5 --- /dev/null +++ b/dom/workers/ServiceWorkerUpdateJob.cpp @@ -0,0 +1,552 @@ +/* -*- 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 "ServiceWorkerUpdateJob.h" + +#include "nsIScriptError.h" +#include "nsIURL.h" +#include "ServiceWorkerScriptCache.h" +#include "Workers.h" + +namespace mozilla { +namespace dom { +namespace workers { + +namespace { + +/** + * The spec mandates slightly different behaviors for computing the scope + * prefix string in case a Service-Worker-Allowed header is specified versus + * when it's not available. + * + * With the header: + * "Set maxScopeString to "/" concatenated with the strings in maxScope's + * path (including empty strings), separated from each other by "/"." + * Without the header: + * "Set maxScopeString to "/" concatenated with the strings, except the last + * string that denotes the script's file name, in registration's registering + * script url's path (including empty strings), separated from each other by + * "/"." + * + * In simpler terms, if the header is not present, we should only use the + * "directory" part of the pathname, and otherwise the entire pathname should be + * used. ScopeStringPrefixMode allows the caller to specify the desired + * behavior. + */ +enum ScopeStringPrefixMode { + eUseDirectory, + eUsePath +}; + +nsresult +GetRequiredScopeStringPrefix(nsIURI* aScriptURI, nsACString& aPrefix, + ScopeStringPrefixMode aPrefixMode) +{ + nsresult rv = aScriptURI->GetPrePath(aPrefix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (aPrefixMode == eUseDirectory) { + nsCOMPtr<nsIURL> scriptURL(do_QueryInterface(aScriptURI)); + if (NS_WARN_IF(!scriptURL)) { + return NS_ERROR_FAILURE; + } + + nsAutoCString dir; + rv = scriptURL->GetDirectory(dir); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aPrefix.Append(dir); + } else if (aPrefixMode == eUsePath) { + nsAutoCString path; + rv = aScriptURI->GetPath(path); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + aPrefix.Append(path); + } else { + MOZ_ASSERT_UNREACHABLE("Invalid value for aPrefixMode"); + } + return NS_OK; +} + +} // anonymous namespace + +class ServiceWorkerUpdateJob::CompareCallback final : public serviceWorkerScriptCache::CompareCallback +{ + RefPtr<ServiceWorkerUpdateJob> mJob; + + ~CompareCallback() + { + } + +public: + explicit CompareCallback(ServiceWorkerUpdateJob* aJob) + : mJob(aJob) + { + MOZ_ASSERT(mJob); + } + + virtual void + ComparisonResult(nsresult aStatus, + bool aInCacheAndEqual, + const nsAString& aNewCacheName, + const nsACString& aMaxScope) override + { + mJob->ComparisonResult(aStatus, aInCacheAndEqual, aNewCacheName, aMaxScope); + } + + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerUpdateJob::CompareCallback, override) +}; + +class ServiceWorkerUpdateJob::ContinueUpdateRunnable final : public LifeCycleEventCallback +{ + nsMainThreadPtrHandle<ServiceWorkerUpdateJob> mJob; + bool mSuccess; + +public: + explicit ContinueUpdateRunnable(const nsMainThreadPtrHandle<ServiceWorkerUpdateJob>& aJob) + : mJob(aJob) + , mSuccess(false) + { + AssertIsOnMainThread(); + } + + void + SetResult(bool aResult) override + { + mSuccess = aResult; + } + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + mJob->ContinueUpdateAfterScriptEval(mSuccess); + mJob = nullptr; + return NS_OK; + } +}; + +class ServiceWorkerUpdateJob::ContinueInstallRunnable final : public LifeCycleEventCallback +{ + nsMainThreadPtrHandle<ServiceWorkerUpdateJob> mJob; + bool mSuccess; + +public: + explicit ContinueInstallRunnable(const nsMainThreadPtrHandle<ServiceWorkerUpdateJob>& aJob) + : mJob(aJob) + , mSuccess(false) + { + AssertIsOnMainThread(); + } + + void + SetResult(bool aResult) override + { + mSuccess = aResult; + } + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + mJob->ContinueAfterInstallEvent(mSuccess); + mJob = nullptr; + return NS_OK; + } +}; + +ServiceWorkerUpdateJob::ServiceWorkerUpdateJob(nsIPrincipal* aPrincipal, + const nsACString& aScope, + const nsACString& aScriptSpec, + nsILoadGroup* aLoadGroup) + : ServiceWorkerJob(Type::Update, aPrincipal, aScope, aScriptSpec) + , mLoadGroup(aLoadGroup) +{ +} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerUpdateJob::GetRegistration() const +{ + AssertIsOnMainThread(); + RefPtr<ServiceWorkerRegistrationInfo> ref = mRegistration; + return ref.forget(); +} + +ServiceWorkerUpdateJob::ServiceWorkerUpdateJob(Type aType, + nsIPrincipal* aPrincipal, + const nsACString& aScope, + const nsACString& aScriptSpec, + nsILoadGroup* aLoadGroup) + : ServiceWorkerJob(aType, aPrincipal, aScope, aScriptSpec) + , mLoadGroup(aLoadGroup) +{ +} + +ServiceWorkerUpdateJob::~ServiceWorkerUpdateJob() +{ +} + +void +ServiceWorkerUpdateJob::FailUpdateJob(ErrorResult& aRv) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aRv.Failed()); + + // Cleanup after a failed installation. This essentially implements + // step 12 of the Install algorithm. + // + // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#installation-algorithm + // + // The spec currently only runs this after an install event fails, + // but we must handle many more internal errors. So we check for + // cleanup on every non-successful exit. + if (mRegistration) { + mRegistration->ClearEvaluating(); + mRegistration->ClearInstalling(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->MaybeRemoveRegistration(mRegistration); + } + } + + mRegistration = nullptr; + + Finish(aRv); +} + +void +ServiceWorkerUpdateJob::FailUpdateJob(nsresult aRv) +{ + ErrorResult rv(aRv); + FailUpdateJob(rv); +} + +void +ServiceWorkerUpdateJob::AsyncExecute() +{ + AssertIsOnMainThread(); + MOZ_ASSERT(GetType() == Type::Update); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (Canceled() || !swm) { + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } + + // Begin step 1 of the Update algorithm. + // + // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#update-algorithm + + RefPtr<ServiceWorkerRegistrationInfo> registration = + swm->GetRegistration(mPrincipal, mScope); + + if (!registration || registration->mPendingUninstall) { + ErrorResult rv; + rv.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(NS_ConvertUTF8toUTF16(mScope), + NS_LITERAL_STRING("uninstalled")); + FailUpdateJob(rv); + return; + } + + // If a Register job with a new script executed ahead of us in the job queue, + // then our update for the old script no longer makes sense. Simply abort + // in this case. + RefPtr<ServiceWorkerInfo> newest = registration->Newest(); + if (newest && !mScriptSpec.Equals(newest->ScriptSpec())) { + ErrorResult rv; + rv.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(NS_ConvertUTF8toUTF16(mScope), + NS_LITERAL_STRING("changed")); + FailUpdateJob(rv); + return; + } + + SetRegistration(registration); + Update(); +} + +void +ServiceWorkerUpdateJob::SetRegistration(ServiceWorkerRegistrationInfo* aRegistration) +{ + AssertIsOnMainThread(); + + MOZ_ASSERT(!mRegistration); + MOZ_ASSERT(aRegistration); + mRegistration = aRegistration; +} + +void +ServiceWorkerUpdateJob::Update() +{ + AssertIsOnMainThread(); + MOZ_ASSERT(!Canceled()); + + // SetRegistration() must be called before Update(). + MOZ_ASSERT(mRegistration); + MOZ_ASSERT(!mRegistration->GetInstalling()); + + // Begin the script download and comparison steps starting at step 5 + // of the Update algorithm. + + RefPtr<ServiceWorkerInfo> workerInfo = mRegistration->Newest(); + nsAutoString cacheName; + + // If the script has not changed, we need to perform a byte-for-byte + // comparison. + if (workerInfo && workerInfo->ScriptSpec().Equals(mScriptSpec)) { + cacheName = workerInfo->CacheName(); + } + + RefPtr<CompareCallback> callback = new CompareCallback(this); + + nsresult rv = + serviceWorkerScriptCache::Compare(mRegistration, mPrincipal, cacheName, + NS_ConvertUTF8toUTF16(mScriptSpec), + callback, mLoadGroup); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(rv); + return; + } +} + +void +ServiceWorkerUpdateJob::ComparisonResult(nsresult aStatus, + bool aInCacheAndEqual, + const nsAString& aNewCacheName, + const nsACString& aMaxScope) +{ + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (NS_WARN_IF(Canceled() || !swm)) { + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } + + // Handle failure of the download or comparison. This is part of Update + // step 5 as "If the algorithm asynchronously completes with null, then:". + if (NS_WARN_IF(NS_FAILED(aStatus))) { + FailUpdateJob(aStatus); + return; + } + + // The spec validates the response before performing the byte-for-byte check. + // Here we perform the comparison in another module and then validate the + // script URL and scope. Make sure to do this validation before accepting + // an byte-for-byte match since the service-worker-allowed header might have + // changed since the last time it was installed. + + // This is step 2 the "validate response" section of Update algorithm step 5. + // Step 1 is performed in the serviceWorkerScriptCache code. + + nsCOMPtr<nsIURI> scriptURI; + nsresult rv = NS_NewURI(getter_AddRefs(scriptURI), mScriptSpec); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsCOMPtr<nsIURI> maxScopeURI; + if (!aMaxScope.IsEmpty()) { + rv = NS_NewURI(getter_AddRefs(maxScopeURI), aMaxScope, + nullptr, scriptURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); + return; + } + } + + nsAutoCString defaultAllowedPrefix; + rv = GetRequiredScopeStringPrefix(scriptURI, defaultAllowedPrefix, + eUseDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsAutoCString maxPrefix(defaultAllowedPrefix); + if (maxScopeURI) { + rv = GetRequiredScopeStringPrefix(maxScopeURI, maxPrefix, eUsePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); + return; + } + } + + if (!StringBeginsWith(mRegistration->mScope, maxPrefix)) { + nsXPIDLString message; + NS_ConvertUTF8toUTF16 reportScope(mRegistration->mScope); + NS_ConvertUTF8toUTF16 reportMaxPrefix(maxPrefix); + const char16_t* params[] = { reportScope.get(), reportMaxPrefix.get() }; + + rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES, + "ServiceWorkerScopePathMismatch", + params, message); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to format localized string"); + swm->ReportToAllClients(mScope, + message, + EmptyString(), + EmptyString(), 0, 0, + nsIScriptError::errorFlag); + FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + // The response has been validated, so now we can consider if its a + // byte-for-byte match. This is step 6 of the Update algorithm. + if (aInCacheAndEqual) { + Finish(NS_OK); + return; + } + + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_UPDATED, 1); + + // Begin step 7 of the Update algorithm to evaluate the new script. + + RefPtr<ServiceWorkerInfo> sw = + new ServiceWorkerInfo(mRegistration->mPrincipal, + mRegistration->mScope, + mScriptSpec, aNewCacheName); + + mRegistration->SetEvaluating(sw); + + nsMainThreadPtrHandle<ServiceWorkerUpdateJob> handle( + new nsMainThreadPtrHolder<ServiceWorkerUpdateJob>(this)); + RefPtr<LifeCycleEventCallback> callback = new ContinueUpdateRunnable(handle); + + ServiceWorkerPrivate* workerPrivate = sw->WorkerPrivate(); + MOZ_ASSERT(workerPrivate); + rv = workerPrivate->CheckScriptEvaluation(callback); + + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } +} + +void +ServiceWorkerUpdateJob::ContinueUpdateAfterScriptEval(bool aScriptEvaluationResult) +{ + AssertIsOnMainThread(); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (Canceled() || !swm) { + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } + + // Step 7.5 of the Update algorithm verifying that the script evaluated + // successfully. + + if (NS_WARN_IF(!aScriptEvaluationResult)) { + ErrorResult error; + + NS_ConvertUTF8toUTF16 scriptSpec(mScriptSpec); + NS_ConvertUTF8toUTF16 scope(mRegistration->mScope); + error.ThrowTypeError<MSG_SW_SCRIPT_THREW>(scriptSpec, scope); + FailUpdateJob(error); + return; + } + + Install(swm); +} + +void +ServiceWorkerUpdateJob::Install(ServiceWorkerManager* aSWM) +{ + AssertIsOnMainThread(); + MOZ_DIAGNOSTIC_ASSERT(!Canceled()); + MOZ_DIAGNOSTIC_ASSERT(aSWM); + + MOZ_ASSERT(!mRegistration->GetInstalling()); + + // Begin step 2 of the Install algorithm. + // + // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#installation-algorithm + + mRegistration->TransitionEvaluatingToInstalling(); + + // Step 6 of the Install algorithm resolving the job promise. + InvokeResultCallbacks(NS_OK); + + // The job promise cannot be rejected after this point, but the job can + // still fail; e.g. if the install event handler throws, etc. + + // fire the updatefound event + nsCOMPtr<nsIRunnable> upr = + NewRunnableMethod<RefPtr<ServiceWorkerRegistrationInfo>>( + aSWM, + &ServiceWorkerManager::FireUpdateFoundOnServiceWorkerRegistrations, + mRegistration); + NS_DispatchToMainThread(upr); + + // Call ContinueAfterInstallEvent(false) on main thread if the SW + // script fails to load. + nsCOMPtr<nsIRunnable> failRunnable = NewRunnableMethod<bool> + (this, &ServiceWorkerUpdateJob::ContinueAfterInstallEvent, false); + + nsMainThreadPtrHandle<ServiceWorkerUpdateJob> handle( + new nsMainThreadPtrHolder<ServiceWorkerUpdateJob>(this)); + RefPtr<LifeCycleEventCallback> callback = new ContinueInstallRunnable(handle); + + // Send the install event to the worker thread + ServiceWorkerPrivate* workerPrivate = + mRegistration->GetInstalling()->WorkerPrivate(); + nsresult rv = workerPrivate->SendLifeCycleEvent(NS_LITERAL_STRING("install"), + callback, failRunnable); + if (NS_WARN_IF(NS_FAILED(rv))) { + ContinueAfterInstallEvent(false /* aSuccess */); + } +} + +void +ServiceWorkerUpdateJob::ContinueAfterInstallEvent(bool aInstallEventSuccess) +{ + if (Canceled()) { + return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + } + + // If we haven't been canceled we should have a registration. There appears + // to be a path where it gets cleared before we call into here. Assert + // to try to catch this condition, but don't crash in release. + MOZ_DIAGNOSTIC_ASSERT(mRegistration); + if (!mRegistration) { + return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + } + + // Continue executing the Install algorithm at step 12. + + // "If installFailed is true" + if (NS_WARN_IF(!aInstallEventSuccess)) { + // The installing worker is cleaned up by FailUpdateJob(). + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } + + MOZ_DIAGNOSTIC_ASSERT(mRegistration->GetInstalling()); + mRegistration->TransitionInstallingToWaiting(); + + Finish(NS_OK); + + // Step 20 calls for explicitly waiting for queued event tasks to fire. Instead, + // we simply queue a runnable to execute Activate. This ensures the events are + // flushed from the queue before proceeding. + + // Step 22 of the Install algorithm. Activate is executed after the completion + // of this job. The controlling client and skipWaiting checks are performed + // in TryToActivate(). + mRegistration->TryToActivateAsync(); +} + +} // namespace workers +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/ServiceWorkerUpdateJob.h b/dom/workers/ServiceWorkerUpdateJob.h new file mode 100644 index 000000000..77adb2212 --- /dev/null +++ b/dom/workers/ServiceWorkerUpdateJob.h @@ -0,0 +1,104 @@ +/* -*- 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_workers_serviceworkerupdatejob_h +#define mozilla_dom_workers_serviceworkerupdatejob_h + +#include "ServiceWorkerJob.h" + +namespace mozilla { +namespace dom { +namespace workers { + +class ServiceWorkerManager; + +// A job class that performs the Update and Install algorithms from the +// service worker spec. This class is designed to be inherited and customized +// as a different job type. This is necessary because the register job +// performs largely the same operations as the update job, but has a few +// different starting steps. +class ServiceWorkerUpdateJob : public ServiceWorkerJob +{ +public: + // Construct an update job to be used only for updates. + ServiceWorkerUpdateJob(nsIPrincipal* aPrincipal, + const nsACString& aScope, + const nsACString& aScriptSpec, + nsILoadGroup* aLoadGroup); + + already_AddRefed<ServiceWorkerRegistrationInfo> + GetRegistration() const; + +protected: + // Construct an update job that is overriden as another job type. + ServiceWorkerUpdateJob(Type aType, + nsIPrincipal* aPrincipal, + const nsACString& aScope, + const nsACString& aScriptSpec, + nsILoadGroup* aLoadGroup); + + virtual ~ServiceWorkerUpdateJob(); + + // FailUpdateJob() must be called if an update job needs Finish() with + // an error. + void + FailUpdateJob(ErrorResult& aRv); + + void + FailUpdateJob(nsresult aRv); + + // The entry point when the update job is being used directly. Job + // types overriding this class should override this method to + // customize behavior. + virtual void + AsyncExecute() override; + + // Set the registration to be operated on by Update() or to be immediately + // returned as a result of the job. This must be called before Update(). + void + SetRegistration(ServiceWorkerRegistrationInfo* aRegistration); + + // Execute the bulk of the update job logic using the registration defined + // by a previous SetRegistration() call. This can be called by the overriden + // AsyncExecute() to complete the job. The Update() method will always call + // Finish(). This method corresponds to the spec Update algorithm. + void + Update(); + +private: + class CompareCallback; + class ContinueUpdateRunnable; + class ContinueInstallRunnable; + + // Utility method called after a script is loaded and compared to + // our current cached script. + void + ComparisonResult(nsresult aStatus, + bool aInCacheAndEqual, + const nsAString& aNewCacheName, + const nsACString& aMaxScope); + + // Utility method called after evaluating the worker script. + void + ContinueUpdateAfterScriptEval(bool aScriptEvaluationResult); + + // Utility method corresponding to the spec Install algorithm. + void + Install(ServiceWorkerManager* aSWM); + + // Utility method called after the install event is handled. + void + ContinueAfterInstallEvent(bool aInstallEventSuccess); + + nsCOMPtr<nsILoadGroup> mLoadGroup; + RefPtr<ServiceWorkerRegistrationInfo> mRegistration; +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_serviceworkerupdatejob_h diff --git a/dom/workers/ServiceWorkerWindowClient.cpp b/dom/workers/ServiceWorkerWindowClient.cpp new file mode 100644 index 000000000..2ce0603cf --- /dev/null +++ b/dom/workers/ServiceWorkerWindowClient.cpp @@ -0,0 +1,558 @@ +/* -*- 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 "ServiceWorkerWindowClient.h" + +#include "js/Value.h" +#include "mozilla/Mutex.h" +#include "mozilla/dom/ClientBinding.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseWorkerProxy.h" +#include "mozilla/UniquePtr.h" +#include "nsContentUtils.h" +#include "nsGlobalWindow.h" +#include "nsIDocShell.h" +#include "nsIDocShellLoadInfo.h" +#include "nsIDocument.h" +#include "nsIGlobalObject.h" +#include "nsIPrincipal.h" +#include "nsIScriptSecurityManager.h" +#include "nsIWebNavigation.h" +#include "nsIWebProgress.h" +#include "nsIWebProgressListener.h" +#include "nsString.h" +#include "nsWeakReference.h" +#include "ServiceWorker.h" +#include "ServiceWorkerInfo.h" +#include "ServiceWorkerManager.h" +#include "WorkerPrivate.h" +#include "WorkerScope.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::dom::workers; + +using mozilla::UniquePtr; + +JSObject* +ServiceWorkerWindowClient::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return WindowClientBinding::Wrap(aCx, this, aGivenProto); +} + +namespace { + +class ResolveOrRejectPromiseRunnable final : public WorkerRunnable +{ + RefPtr<PromiseWorkerProxy> mPromiseProxy; + UniquePtr<ServiceWorkerClientInfo> mClientInfo; + nsresult mRv; + +public: + // Passing a null clientInfo will resolve the promise with a null value. + ResolveOrRejectPromiseRunnable( + WorkerPrivate* aWorkerPrivate, PromiseWorkerProxy* aPromiseProxy, + UniquePtr<ServiceWorkerClientInfo>&& aClientInfo) + : WorkerRunnable(aWorkerPrivate) + , mPromiseProxy(aPromiseProxy) + , mClientInfo(Move(aClientInfo)) + , mRv(NS_OK) + { + AssertIsOnMainThread(); + } + + // Reject the promise with passed nsresult. + ResolveOrRejectPromiseRunnable(WorkerPrivate* aWorkerPrivate, + PromiseWorkerProxy* aPromiseProxy, + nsresult aRv) + : WorkerRunnable(aWorkerPrivate) + , mPromiseProxy(aPromiseProxy) + , mClientInfo(nullptr) + , mRv(aRv) + { + MOZ_ASSERT(NS_FAILED(aRv)); + AssertIsOnMainThread(); + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<Promise> promise = mPromiseProxy->WorkerPromise(); + MOZ_ASSERT(promise); + + if (NS_WARN_IF(NS_FAILED(mRv))) { + promise->MaybeReject(mRv); + } else if (mClientInfo) { + RefPtr<ServiceWorkerWindowClient> client = + new ServiceWorkerWindowClient(promise->GetParentObject(), *mClientInfo); + promise->MaybeResolve(client); + } else { + promise->MaybeResolve(JS::NullHandleValue); + } + + // Release the reference on the worker thread. + mPromiseProxy->CleanUp(); + + return true; + } +}; + +class ClientFocusRunnable final : public Runnable +{ + uint64_t mWindowId; + RefPtr<PromiseWorkerProxy> mPromiseProxy; + +public: + ClientFocusRunnable(uint64_t aWindowId, PromiseWorkerProxy* aPromiseProxy) + : mWindowId(aWindowId) + , mPromiseProxy(aPromiseProxy) + { + MOZ_ASSERT(mPromiseProxy); + } + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + nsGlobalWindow* window = nsGlobalWindow::GetInnerWindowWithId(mWindowId); + UniquePtr<ServiceWorkerClientInfo> clientInfo; + + if (window) { + nsCOMPtr<nsIDocument> doc = window->GetDocument(); + if (doc) { + nsContentUtils::DispatchFocusChromeEvent(window->GetOuterWindow()); + clientInfo.reset(new ServiceWorkerClientInfo(doc)); + } + } + + DispatchResult(Move(clientInfo)); + return NS_OK; + } + +private: + void + DispatchResult(UniquePtr<ServiceWorkerClientInfo>&& aClientInfo) + { + AssertIsOnMainThread(); + MutexAutoLock lock(mPromiseProxy->Lock()); + if (mPromiseProxy->CleanedUp()) { + return; + } + + RefPtr<ResolveOrRejectPromiseRunnable> resolveRunnable; + if (aClientInfo) { + resolveRunnable = new ResolveOrRejectPromiseRunnable( + mPromiseProxy->GetWorkerPrivate(), mPromiseProxy, Move(aClientInfo)); + } else { + resolveRunnable = new ResolveOrRejectPromiseRunnable( + mPromiseProxy->GetWorkerPrivate(), mPromiseProxy, + NS_ERROR_DOM_INVALID_ACCESS_ERR); + } + + resolveRunnable->Dispatch(); + } +}; + +} // namespace + +already_AddRefed<Promise> +ServiceWorkerWindowClient::Focus(ErrorResult& aRv) const +{ + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + workerPrivate->AssertIsOnWorkerThread(); + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject()); + MOZ_ASSERT(global); + + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (workerPrivate->GlobalScope()->WindowInteractionAllowed()) { + RefPtr<PromiseWorkerProxy> promiseProxy = + PromiseWorkerProxy::Create(workerPrivate, promise); + if (promiseProxy) { + RefPtr<ClientFocusRunnable> r = new ClientFocusRunnable(mWindowId, + promiseProxy); + MOZ_ALWAYS_SUCCEEDS(workerPrivate->DispatchToMainThread(r.forget())); + } else { + promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + } + + } else { + promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR); + } + + return promise.forget(); +} + +class WebProgressListener final : public nsIWebProgressListener, + public nsSupportsWeakReference +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(WebProgressListener, + nsIWebProgressListener) + + WebProgressListener(PromiseWorkerProxy* aPromiseProxy, + ServiceWorkerPrivate* aServiceWorkerPrivate, + nsPIDOMWindowOuter* aWindow, nsIURI* aBaseURI) + : mPromiseProxy(aPromiseProxy) + , mServiceWorkerPrivate(aServiceWorkerPrivate) + , mWindow(aWindow) + , mBaseURI(aBaseURI) + { + MOZ_ASSERT(aPromiseProxy); + MOZ_ASSERT(aServiceWorkerPrivate); + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aWindow->IsOuterWindow()); + MOZ_ASSERT(aBaseURI); + AssertIsOnMainThread(); + + mServiceWorkerPrivate->StoreISupports(static_cast<nsIWebProgressListener*>(this)); + } + + NS_IMETHOD + OnStateChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + uint32_t aStateFlags, nsresult aStatus) override + { + if (!(aStateFlags & STATE_IS_DOCUMENT) || + !(aStateFlags & (STATE_STOP | STATE_TRANSFERRING))) { + return NS_OK; + } + + // This is safe because our caller holds a strong ref. + mServiceWorkerPrivate->RemoveISupports(static_cast<nsIWebProgressListener*>(this)); + aWebProgress->RemoveProgressListener(this); + + WorkerPrivate* workerPrivate; + + { + MutexAutoLock lock(mPromiseProxy->Lock()); + if (mPromiseProxy->CleanedUp()) { + return NS_OK; + } + + workerPrivate = mPromiseProxy->GetWorkerPrivate(); + } + + nsCOMPtr<nsIDocument> doc = mWindow->GetExtantDoc(); + + RefPtr<ResolveOrRejectPromiseRunnable> resolveRunnable; + UniquePtr<ServiceWorkerClientInfo> clientInfo; + if (!doc) { + resolveRunnable = new ResolveOrRejectPromiseRunnable( + workerPrivate, mPromiseProxy, NS_ERROR_TYPE_ERR); + resolveRunnable->Dispatch(); + + return NS_OK; + } + + // Check same origin. + nsCOMPtr<nsIScriptSecurityManager> securityManager = + nsContentUtils::GetSecurityManager(); + nsresult rv = securityManager->CheckSameOriginURI(doc->GetOriginalURI(), + mBaseURI, false); + + if (NS_SUCCEEDED(rv)) { + nsContentUtils::DispatchFocusChromeEvent(mWindow->GetOuterWindow()); + clientInfo.reset(new ServiceWorkerClientInfo(doc)); + } + + resolveRunnable = new ResolveOrRejectPromiseRunnable( + workerPrivate, mPromiseProxy, Move(clientInfo)); + resolveRunnable->Dispatch(); + + return NS_OK; + } + + NS_IMETHOD + OnProgressChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + int32_t aCurSelfProgress, int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) override + { + MOZ_CRASH("Unexpected notification."); + return NS_OK; + } + + NS_IMETHOD + OnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + nsIURI* aLocation, uint32_t aFlags) override + { + MOZ_CRASH("Unexpected notification."); + return NS_OK; + } + + NS_IMETHOD + OnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + nsresult aStatus, const char16_t* aMessage) override + { + MOZ_CRASH("Unexpected notification."); + return NS_OK; + } + + NS_IMETHOD + OnSecurityChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + uint32_t aState) override + { + MOZ_CRASH("Unexpected notification."); + return NS_OK; + } + +private: + ~WebProgressListener() {} + + RefPtr<PromiseWorkerProxy> mPromiseProxy; + RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate; + nsCOMPtr<nsPIDOMWindowOuter> mWindow; + nsCOMPtr<nsIURI> mBaseURI; +}; + +NS_IMPL_CYCLE_COLLECTING_ADDREF(WebProgressListener) +NS_IMPL_CYCLE_COLLECTING_RELEASE(WebProgressListener) +NS_IMPL_CYCLE_COLLECTION(WebProgressListener, mPromiseProxy, + mServiceWorkerPrivate, mWindow) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebProgressListener) + NS_INTERFACE_MAP_ENTRY(nsIWebProgressListener) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END + +class ClientNavigateRunnable final : public Runnable +{ + uint64_t mWindowId; + nsString mUrl; + nsCString mBaseUrl; + nsString mScope; + RefPtr<PromiseWorkerProxy> mPromiseProxy; + MOZ_INIT_OUTSIDE_CTOR WorkerPrivate* mWorkerPrivate; + +public: + ClientNavigateRunnable(uint64_t aWindowId, const nsAString& aUrl, + const nsAString& aScope, + PromiseWorkerProxy* aPromiseProxy) + : mWindowId(aWindowId) + , mUrl(aUrl) + , mScope(aScope) + , mPromiseProxy(aPromiseProxy) + , mWorkerPrivate(nullptr) + { + MOZ_ASSERT(aPromiseProxy); + MOZ_ASSERT(aPromiseProxy->GetWorkerPrivate()); + aPromiseProxy->GetWorkerPrivate()->AssertIsOnWorkerThread(); + } + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + + nsCOMPtr<nsIPrincipal> principal; + + { + MutexAutoLock lock(mPromiseProxy->Lock()); + if (mPromiseProxy->CleanedUp()) { + return NS_OK; + } + + mWorkerPrivate = mPromiseProxy->GetWorkerPrivate(); + WorkerPrivate::LocationInfo& info = mWorkerPrivate->GetLocationInfo(); + mBaseUrl = info.mHref; + principal = mWorkerPrivate->GetPrincipal(); + MOZ_DIAGNOSTIC_ASSERT(principal); + } + + nsCOMPtr<nsIURI> baseUrl; + nsCOMPtr<nsIURI> url; + nsresult rv = ParseUrl(getter_AddRefs(baseUrl), getter_AddRefs(url)); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return RejectPromise(NS_ERROR_TYPE_ERR); + } + + rv = principal->CheckMayLoad(url, true, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return RejectPromise(rv); + } + + nsGlobalWindow* window; + rv = Navigate(url, principal, &window); + if (NS_WARN_IF(NS_FAILED(rv))) { + return RejectPromise(rv); + } + + nsCOMPtr<nsIDocShell> docShell = window->GetDocShell(); + nsCOMPtr<nsIWebProgress> webProgress = do_GetInterface(docShell); + if (NS_WARN_IF(!webProgress)) { + return NS_ERROR_FAILURE; + } + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (!swm) { + return NS_ERROR_FAILURE; + } + + RefPtr<ServiceWorkerRegistrationInfo> registration = + swm->GetRegistration(principal, NS_ConvertUTF16toUTF8(mScope)); + if (NS_WARN_IF(!registration)) { + return NS_ERROR_FAILURE; + } + RefPtr<ServiceWorkerInfo> serviceWorkerInfo = + registration->GetServiceWorkerInfoById(mWorkerPrivate->ServiceWorkerID()); + if (NS_WARN_IF(!serviceWorkerInfo)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIWebProgressListener> listener = + new WebProgressListener(mPromiseProxy, serviceWorkerInfo->WorkerPrivate(), + window->GetOuterWindow(), baseUrl); + + rv = webProgress->AddProgressListener( + listener, nsIWebProgress::NOTIFY_STATE_DOCUMENT); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return RejectPromise(rv); + } + + return NS_OK; + } + +private: + nsresult + RejectPromise(nsresult aRv) + { + MOZ_ASSERT(mWorkerPrivate); + RefPtr<ResolveOrRejectPromiseRunnable> resolveRunnable = + new ResolveOrRejectPromiseRunnable(mWorkerPrivate, mPromiseProxy, aRv); + + resolveRunnable->Dispatch(); + return NS_OK; + } + + nsresult + ResolvePromise(UniquePtr<ServiceWorkerClientInfo>&& aClientInfo) + { + MOZ_ASSERT(mWorkerPrivate); + RefPtr<ResolveOrRejectPromiseRunnable> resolveRunnable = + new ResolveOrRejectPromiseRunnable(mWorkerPrivate, mPromiseProxy, + Move(aClientInfo)); + + resolveRunnable->Dispatch(); + return NS_OK; + } + + nsresult + ParseUrl(nsIURI** aBaseUrl, nsIURI** aUrl) + { + MOZ_ASSERT(aBaseUrl); + MOZ_ASSERT(aUrl); + AssertIsOnMainThread(); + + nsCOMPtr<nsIURI> baseUrl; + nsresult rv = NS_NewURI(getter_AddRefs(baseUrl), mBaseUrl); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> url; + rv = NS_NewURI(getter_AddRefs(url), mUrl, nullptr, baseUrl); + NS_ENSURE_SUCCESS(rv, rv); + + baseUrl.forget(aBaseUrl); + url.forget(aUrl); + + return NS_OK; + } + + nsresult + Navigate(nsIURI* aUrl, nsIPrincipal* aPrincipal, nsGlobalWindow** aWindow) + { + MOZ_ASSERT(aWindow); + + nsGlobalWindow* window = nsGlobalWindow::GetInnerWindowWithId(mWindowId); + if (NS_WARN_IF(!window)) { + return NS_ERROR_TYPE_ERR; + } + + nsCOMPtr<nsIDocument> doc = window->GetDocument(); + if (NS_WARN_IF(!doc)) { + return NS_ERROR_TYPE_ERR; + } + + if (NS_WARN_IF(!doc->IsActive())) { + return NS_ERROR_TYPE_ERR; + } + + nsCOMPtr<nsIDocShell> docShell = window->GetDocShell(); + if (NS_WARN_IF(!docShell)) { + return NS_ERROR_TYPE_ERR; + } + + nsCOMPtr<nsIDocShellLoadInfo> loadInfo; + nsresult rv = docShell->CreateLoadInfo(getter_AddRefs(loadInfo)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + loadInfo->SetTriggeringPrincipal(aPrincipal); + loadInfo->SetReferrer(doc->GetOriginalURI()); + loadInfo->SetReferrerPolicy(doc->GetReferrerPolicy()); + loadInfo->SetLoadType(nsIDocShellLoadInfo::loadStopContentAndReplace); + loadInfo->SetSourceDocShell(docShell); + rv = + docShell->LoadURI(aUrl, loadInfo, nsIWebNavigation::LOAD_FLAGS_NONE, true); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *aWindow = window; + return NS_OK; + } +}; + +already_AddRefed<Promise> +ServiceWorkerWindowClient::Navigate(const nsAString& aUrl, ErrorResult& aRv) +{ + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + workerPrivate->AssertIsOnWorkerThread(); + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject()); + MOZ_ASSERT(global); + + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (aUrl.EqualsLiteral("about:blank")) { + promise->MaybeReject(NS_ERROR_TYPE_ERR); + return promise.forget(); + } + + ServiceWorkerGlobalScope* globalScope = + static_cast<ServiceWorkerGlobalScope*>(workerPrivate->GlobalScope()); + nsString scope; + globalScope->GetScope(scope); + + RefPtr<PromiseWorkerProxy> promiseProxy = + PromiseWorkerProxy::Create(workerPrivate, promise); + if (promiseProxy) { + RefPtr<ClientNavigateRunnable> r = + new ClientNavigateRunnable(mWindowId, aUrl, scope, promiseProxy); + MOZ_ALWAYS_SUCCEEDS(workerPrivate->DispatchToMainThread(r.forget())); + } else { + promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + } + + return promise.forget(); +} diff --git a/dom/workers/ServiceWorkerWindowClient.h b/dom/workers/ServiceWorkerWindowClient.h new file mode 100644 index 000000000..0e2441294 --- /dev/null +++ b/dom/workers/ServiceWorkerWindowClient.h @@ -0,0 +1,64 @@ +/* -*- 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_workers_serviceworkerwindowclient_h +#define mozilla_dom_workers_serviceworkerwindowclient_h + +#include "ServiceWorkerClient.h" + +namespace mozilla { +namespace dom { + +class Promise; + +namespace workers { + +class ServiceWorkerWindowClient final : public ServiceWorkerClient +{ +public: + ServiceWorkerWindowClient(nsISupports* aOwner, + const ServiceWorkerClientInfo& aClientInfo) + : ServiceWorkerClient(aOwner, aClientInfo), + mVisibilityState(aClientInfo.mVisibilityState), + mFocused(aClientInfo.mFocused) + { + } + + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + mozilla::dom::VisibilityState + VisibilityState() const + { + return mVisibilityState; + } + + bool + Focused() const + { + return mFocused; + } + + already_AddRefed<Promise> + Focus(ErrorResult& aRv) const; + + already_AddRefed<Promise> + Navigate(const nsAString& aUrl, ErrorResult& aRv); + +private: + ~ServiceWorkerWindowClient() + { } + + mozilla::dom::VisibilityState mVisibilityState; + bool mFocused; +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_serviceworkerwindowclient_h diff --git a/dom/workers/SharedWorker.cpp b/dom/workers/SharedWorker.cpp new file mode 100644 index 000000000..b0eed2def --- /dev/null +++ b/dom/workers/SharedWorker.cpp @@ -0,0 +1,207 @@ +/* -*- 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 "SharedWorker.h" + +#include "nsPIDOMWindow.h" + +#include "mozilla/EventDispatcher.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/dom/SharedWorkerBinding.h" +#include "mozilla/Telemetry.h" +#include "nsContentUtils.h" +#include "nsIClassInfoImpl.h" +#include "nsIDOMEvent.h" + +#include "RuntimeService.h" +#include "WorkerPrivate.h" + +using mozilla::dom::Optional; +using mozilla::dom::Sequence; +using mozilla::dom::MessagePort; +using namespace mozilla; + +USING_WORKERS_NAMESPACE + +SharedWorker::SharedWorker(nsPIDOMWindowInner* aWindow, + WorkerPrivate* aWorkerPrivate, + MessagePort* aMessagePort) + : DOMEventTargetHelper(aWindow) + , mWorkerPrivate(aWorkerPrivate) + , mMessagePort(aMessagePort) + , mFrozen(false) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aMessagePort); +} + +SharedWorker::~SharedWorker() +{ + AssertIsOnMainThread(); +} + +// static +already_AddRefed<SharedWorker> +SharedWorker::Constructor(const GlobalObject& aGlobal, JSContext* aCx, + const nsAString& aScriptURL, + const mozilla::dom::Optional<nsAString>& aName, + ErrorResult& aRv) +{ + AssertIsOnMainThread(); + + RuntimeService* rts = RuntimeService::GetOrCreateService(); + if (!rts) { + aRv = NS_ERROR_NOT_AVAILABLE; + return nullptr; + } + + nsCString name; + if (aName.WasPassed()) { + name = NS_ConvertUTF16toUTF8(aName.Value()); + } + + RefPtr<SharedWorker> sharedWorker; + nsresult rv = rts->CreateSharedWorker(aGlobal, aScriptURL, name, + getter_AddRefs(sharedWorker)); + if (NS_FAILED(rv)) { + aRv = rv; + return nullptr; + } + + Telemetry::Accumulate(Telemetry::SHARED_WORKER_COUNT, 1); + + return sharedWorker.forget(); +} + +MessagePort* +SharedWorker::Port() +{ + AssertIsOnMainThread(); + return mMessagePort; +} + +void +SharedWorker::Freeze() +{ + AssertIsOnMainThread(); + MOZ_ASSERT(!IsFrozen()); + + mFrozen = true; +} + +void +SharedWorker::Thaw() +{ + AssertIsOnMainThread(); + MOZ_ASSERT(IsFrozen()); + + mFrozen = false; + + if (!mFrozenEvents.IsEmpty()) { + nsTArray<nsCOMPtr<nsIDOMEvent>> events; + mFrozenEvents.SwapElements(events); + + for (uint32_t index = 0; index < events.Length(); index++) { + nsCOMPtr<nsIDOMEvent>& event = events[index]; + MOZ_ASSERT(event); + + nsCOMPtr<nsIDOMEventTarget> target; + if (NS_SUCCEEDED(event->GetTarget(getter_AddRefs(target)))) { + bool ignored; + if (NS_FAILED(target->DispatchEvent(event, &ignored))) { + NS_WARNING("Failed to dispatch event!"); + } + } else { + NS_WARNING("Failed to get target!"); + } + } + } +} + +void +SharedWorker::QueueEvent(nsIDOMEvent* aEvent) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aEvent); + MOZ_ASSERT(IsFrozen()); + + mFrozenEvents.AppendElement(aEvent); +} + +void +SharedWorker::Close() +{ + AssertIsOnMainThread(); + + if (mMessagePort) { + mMessagePort->Close(); + } +} + +void +SharedWorker::PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + ErrorResult& aRv) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(mWorkerPrivate); + MOZ_ASSERT(mMessagePort); + + mMessagePort->PostMessage(aCx, aMessage, aTransferable, aRv); +} + +NS_IMPL_ADDREF_INHERITED(SharedWorker, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(SharedWorker, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(SharedWorker) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_CYCLE_COLLECTION_CLASS(SharedWorker) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(SharedWorker, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMessagePort) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFrozenEvents) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(SharedWorker, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mMessagePort) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFrozenEvents) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +JSObject* +SharedWorker::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + AssertIsOnMainThread(); + + return SharedWorkerBinding::Wrap(aCx, this, aGivenProto); +} + +nsresult +SharedWorker::PreHandleEvent(EventChainPreVisitor& aVisitor) +{ + AssertIsOnMainThread(); + + if (IsFrozen()) { + nsCOMPtr<nsIDOMEvent> event = aVisitor.mDOMEvent; + if (!event) { + event = EventDispatcher::CreateEvent(aVisitor.mEvent->mOriginalTarget, + aVisitor.mPresContext, + aVisitor.mEvent, EmptyString()); + } + + QueueEvent(event); + + aVisitor.mCanHandle = false; + aVisitor.mParentTarget = nullptr; + return NS_OK; + } + + return DOMEventTargetHelper::PreHandleEvent(aVisitor); +} diff --git a/dom/workers/SharedWorker.h b/dom/workers/SharedWorker.h new file mode 100644 index 000000000..220436e4d --- /dev/null +++ b/dom/workers/SharedWorker.h @@ -0,0 +1,105 @@ +/* -*- 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_workers_sharedworker_h__ +#define mozilla_dom_workers_sharedworker_h__ + +#include "Workers.h" + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/DOMEventTargetHelper.h" + +class nsIDOMEvent; +class nsPIDOMWindowInner; + +namespace mozilla { +class EventChainPreVisitor; + +namespace dom { +class MessagePort; +} +} // namespace mozilla + +BEGIN_WORKERS_NAMESPACE + +class RuntimeService; +class WorkerPrivate; + +class SharedWorker final : public DOMEventTargetHelper +{ + friend class RuntimeService; + + typedef mozilla::ErrorResult ErrorResult; + typedef mozilla::dom::GlobalObject GlobalObject; + + RefPtr<WorkerPrivate> mWorkerPrivate; + RefPtr<MessagePort> mMessagePort; + nsTArray<nsCOMPtr<nsIDOMEvent>> mFrozenEvents; + bool mFrozen; + +public: + static already_AddRefed<SharedWorker> + Constructor(const GlobalObject& aGlobal, JSContext* aCx, + const nsAString& aScriptURL, const Optional<nsAString>& aName, + ErrorResult& aRv); + + MessagePort* + Port(); + + bool + IsFrozen() const + { + return mFrozen; + } + + void + Freeze(); + + void + Thaw(); + + void + QueueEvent(nsIDOMEvent* aEvent); + + void + Close(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(SharedWorker, DOMEventTargetHelper) + + IMPL_EVENT_HANDLER(error) + + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + virtual nsresult + PreHandleEvent(EventChainPreVisitor& aVisitor) override; + + WorkerPrivate* + GetWorkerPrivate() const + { + return mWorkerPrivate; + } + +private: + // This class can only be created from the RuntimeService. + SharedWorker(nsPIDOMWindowInner* aWindow, + WorkerPrivate* aWorkerPrivate, + MessagePort* aMessagePort); + + // This class is reference-counted and will be destroyed from Release(). + ~SharedWorker(); + + // Only called by MessagePort. + void + PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + ErrorResult& aRv); +}; + +END_WORKERS_NAMESPACE + +#endif // mozilla_dom_workers_sharedworker_h__ diff --git a/dom/workers/WorkerDebuggerManager.cpp b/dom/workers/WorkerDebuggerManager.cpp new file mode 100644 index 000000000..dfd7e5acc --- /dev/null +++ b/dom/workers/WorkerDebuggerManager.cpp @@ -0,0 +1,361 @@ +/* -*- 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 "WorkerDebuggerManager.h" + +#include "nsISimpleEnumerator.h" + +#include "mozilla/ClearOnShutdown.h" + +#include "WorkerPrivate.h" + +USING_WORKERS_NAMESPACE + +namespace { + +class RegisterDebuggerMainThreadRunnable final : public mozilla::Runnable +{ + WorkerPrivate* mWorkerPrivate; + bool mNotifyListeners; + +public: + RegisterDebuggerMainThreadRunnable(WorkerPrivate* aWorkerPrivate, + bool aNotifyListeners) + : mWorkerPrivate(aWorkerPrivate), + mNotifyListeners(aNotifyListeners) + { } + +private: + ~RegisterDebuggerMainThreadRunnable() + { } + + NS_IMETHOD + Run() override + { + WorkerDebuggerManager* manager = WorkerDebuggerManager::Get(); + MOZ_ASSERT(manager); + + manager->RegisterDebuggerMainThread(mWorkerPrivate, mNotifyListeners); + return NS_OK; + } +}; + +class UnregisterDebuggerMainThreadRunnable final : public mozilla::Runnable +{ + WorkerPrivate* mWorkerPrivate; + +public: + explicit UnregisterDebuggerMainThreadRunnable(WorkerPrivate* aWorkerPrivate) + : mWorkerPrivate(aWorkerPrivate) + { } + +private: + ~UnregisterDebuggerMainThreadRunnable() + { } + + NS_IMETHOD + Run() override + { + WorkerDebuggerManager* manager = WorkerDebuggerManager::Get(); + MOZ_ASSERT(manager); + + manager->UnregisterDebuggerMainThread(mWorkerPrivate); + return NS_OK; + } +}; + +// Does not hold an owning reference. +static WorkerDebuggerManager* gWorkerDebuggerManager; + +} /* anonymous namespace */ + +BEGIN_WORKERS_NAMESPACE + +class WorkerDebuggerEnumerator final : public nsISimpleEnumerator +{ + nsTArray<RefPtr<WorkerDebugger>> mDebuggers; + uint32_t mIndex; + +public: + explicit WorkerDebuggerEnumerator( + const nsTArray<RefPtr<WorkerDebugger>>& aDebuggers) + : mDebuggers(aDebuggers), mIndex(0) + { + } + + NS_DECL_ISUPPORTS + NS_DECL_NSISIMPLEENUMERATOR + +private: + ~WorkerDebuggerEnumerator() {} +}; + +NS_IMPL_ISUPPORTS(WorkerDebuggerEnumerator, nsISimpleEnumerator); + +NS_IMETHODIMP +WorkerDebuggerEnumerator::HasMoreElements(bool* aResult) +{ + *aResult = mIndex < mDebuggers.Length(); + return NS_OK; +}; + +NS_IMETHODIMP +WorkerDebuggerEnumerator::GetNext(nsISupports** aResult) +{ + if (mIndex == mDebuggers.Length()) { + return NS_ERROR_FAILURE; + } + + mDebuggers.ElementAt(mIndex++).forget(aResult); + return NS_OK; +}; + +WorkerDebuggerManager::WorkerDebuggerManager() +: mMutex("WorkerDebuggerManager::mMutex") +{ + AssertIsOnMainThread(); +} + +WorkerDebuggerManager::~WorkerDebuggerManager() +{ + AssertIsOnMainThread(); +} + +// static +already_AddRefed<WorkerDebuggerManager> +WorkerDebuggerManager::GetInstance() +{ + RefPtr<WorkerDebuggerManager> manager = WorkerDebuggerManager::GetOrCreate(); + return manager.forget(); +} + +// static +WorkerDebuggerManager* +WorkerDebuggerManager::GetOrCreate() +{ + AssertIsOnMainThread(); + + if (!gWorkerDebuggerManager) { + // The observer service now owns us until shutdown. + gWorkerDebuggerManager = new WorkerDebuggerManager(); + if (NS_FAILED(gWorkerDebuggerManager->Init())) { + NS_WARNING("Failed to initialize worker debugger manager!"); + gWorkerDebuggerManager = nullptr; + return nullptr; + } + } + + return gWorkerDebuggerManager; +} + +WorkerDebuggerManager* +WorkerDebuggerManager::Get() +{ + MOZ_ASSERT(gWorkerDebuggerManager); + return gWorkerDebuggerManager; +} + +NS_IMPL_ISUPPORTS(WorkerDebuggerManager, nsIObserver, nsIWorkerDebuggerManager); + +NS_IMETHODIMP +WorkerDebuggerManager::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) +{ + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + Shutdown(); + return NS_OK; + } + + NS_NOTREACHED("Unknown observer topic!"); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebuggerManager::GetWorkerDebuggerEnumerator( + nsISimpleEnumerator** aResult) +{ + AssertIsOnMainThread(); + + RefPtr<WorkerDebuggerEnumerator> enumerator = + new WorkerDebuggerEnumerator(mDebuggers); + enumerator.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebuggerManager::AddListener(nsIWorkerDebuggerManagerListener* aListener) +{ + AssertIsOnMainThread(); + + MutexAutoLock lock(mMutex); + + if (mListeners.Contains(aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mListeners.AppendElement(aListener); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebuggerManager::RemoveListener( + nsIWorkerDebuggerManagerListener* aListener) +{ + AssertIsOnMainThread(); + + MutexAutoLock lock(mMutex); + + if (!mListeners.Contains(aListener)) { + return NS_OK; + } + + mListeners.RemoveElement(aListener); + return NS_OK; +} + +nsresult +WorkerDebuggerManager::Init() +{ + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + NS_ENSURE_TRUE(obs, NS_ERROR_FAILURE); + + nsresult rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +void +WorkerDebuggerManager::Shutdown() +{ + AssertIsOnMainThread(); + + MutexAutoLock lock(mMutex); + + mListeners.Clear(); +} + +void +WorkerDebuggerManager::RegisterDebugger(WorkerPrivate* aWorkerPrivate) +{ + aWorkerPrivate->AssertIsOnParentThread(); + + if (NS_IsMainThread()) { + // When the parent thread is the main thread, it will always block until all + // register liseners have been called, since it cannot continue until the + // call to RegisterDebuggerMainThread returns. + // + // In this case, it is always safe to notify all listeners on the main + // thread, even if there were no listeners at the time this method was + // called, so we can always pass true for the value of aNotifyListeners. + // This avoids having to lock mMutex to check whether mListeners is empty. + RegisterDebuggerMainThread(aWorkerPrivate, true); + } else { + // We guarantee that if any register listeners are called, the worker does + // not start running until all register listeners have been called. To + // guarantee this, the parent thread should block until all register + // listeners have been called. + // + // However, to avoid overhead when the debugger is not being used, the + // parent thread will only block if there were any listeners at the time + // this method was called. As a result, we should not notify any listeners + // on the main thread if there were no listeners at the time this method was + // called, because the parent will not be blocking in that case. + bool hasListeners = false; + { + MutexAutoLock lock(mMutex); + + hasListeners = !mListeners.IsEmpty(); + } + + nsCOMPtr<nsIRunnable> runnable = + new RegisterDebuggerMainThreadRunnable(aWorkerPrivate, hasListeners); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable, NS_DISPATCH_NORMAL)); + + if (hasListeners) { + aWorkerPrivate->WaitForIsDebuggerRegistered(true); + } + } +} + +void +WorkerDebuggerManager::UnregisterDebugger(WorkerPrivate* aWorkerPrivate) +{ + aWorkerPrivate->AssertIsOnParentThread(); + + if (NS_IsMainThread()) { + UnregisterDebuggerMainThread(aWorkerPrivate); + } else { + nsCOMPtr<nsIRunnable> runnable = + new UnregisterDebuggerMainThreadRunnable(aWorkerPrivate); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable, NS_DISPATCH_NORMAL)); + + aWorkerPrivate->WaitForIsDebuggerRegistered(false); + } +} + +void +WorkerDebuggerManager::RegisterDebuggerMainThread(WorkerPrivate* aWorkerPrivate, + bool aNotifyListeners) +{ + AssertIsOnMainThread(); + + RefPtr<WorkerDebugger> debugger = new WorkerDebugger(aWorkerPrivate); + mDebuggers.AppendElement(debugger); + + aWorkerPrivate->SetDebugger(debugger); + + if (aNotifyListeners) { + nsTArray<nsCOMPtr<nsIWorkerDebuggerManagerListener>> listeners; + { + MutexAutoLock lock(mMutex); + + listeners = mListeners; + } + + for (size_t index = 0; index < listeners.Length(); ++index) { + listeners[index]->OnRegister(debugger); + } + } + + aWorkerPrivate->SetIsDebuggerRegistered(true); +} + +void +WorkerDebuggerManager::UnregisterDebuggerMainThread( + WorkerPrivate* aWorkerPrivate) +{ + AssertIsOnMainThread(); + + // There is nothing to do here if the debugger was never succesfully + // registered. We need to check this on the main thread because the worker + // does not wait for the registration to complete if there were no listeners + // installed when it started. + if (!aWorkerPrivate->IsDebuggerRegistered()) { + return; + } + + RefPtr<WorkerDebugger> debugger = aWorkerPrivate->Debugger(); + mDebuggers.RemoveElement(debugger); + + aWorkerPrivate->SetDebugger(nullptr); + + nsTArray<nsCOMPtr<nsIWorkerDebuggerManagerListener>> listeners; + { + MutexAutoLock lock(mMutex); + + listeners = mListeners; + } + + for (size_t index = 0; index < listeners.Length(); ++index) { + listeners[index]->OnUnregister(debugger); + } + + debugger->Close(); + aWorkerPrivate->SetIsDebuggerRegistered(false); +} + +END_WORKERS_NAMESPACE diff --git a/dom/workers/WorkerDebuggerManager.h b/dom/workers/WorkerDebuggerManager.h new file mode 100644 index 000000000..6c4c943b1 --- /dev/null +++ b/dom/workers/WorkerDebuggerManager.h @@ -0,0 +1,121 @@ +/* -*- 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_workers_workerdebuggermanager_h +#define mozilla_dom_workers_workerdebuggermanager_h + +#include "Workers.h" + +#include "nsIObserver.h" +#include "nsIWorkerDebuggerManager.h" + +#include "nsServiceManagerUtils.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" + +#define WORKERDEBUGGERMANAGER_CID \ + { 0x62ec8731, 0x55ad, 0x4246, \ + { 0xb2, 0xea, 0xf2, 0x6c, 0x1f, 0xe1, 0x9d, 0x2d } } +#define WORKERDEBUGGERMANAGER_CONTRACTID \ + "@mozilla.org/dom/workers/workerdebuggermanager;1" + +BEGIN_WORKERS_NAMESPACE + +class WorkerDebugger; + +class WorkerDebuggerManager final : public nsIObserver, + public nsIWorkerDebuggerManager +{ + Mutex mMutex; + + // Protected by mMutex. + nsTArray<nsCOMPtr<nsIWorkerDebuggerManagerListener>> mListeners; + + // Only touched on the main thread. + nsTArray<RefPtr<WorkerDebugger>> mDebuggers; + +public: + static already_AddRefed<WorkerDebuggerManager> + GetInstance(); + + static WorkerDebuggerManager* + GetOrCreate(); + + static WorkerDebuggerManager* + Get(); + + WorkerDebuggerManager(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + NS_DECL_NSIWORKERDEBUGGERMANAGER + + nsresult + Init(); + + void + Shutdown(); + + void + RegisterDebugger(WorkerPrivate* aWorkerPrivate); + + void + RegisterDebuggerMainThread(WorkerPrivate* aWorkerPrivate, + bool aNotifyListeners); + + void + UnregisterDebugger(WorkerPrivate* aWorkerPrivate); + + void + UnregisterDebuggerMainThread(WorkerPrivate* aWorkerPrivate); + +private: + virtual ~WorkerDebuggerManager(); +}; + +inline nsresult +RegisterWorkerDebugger(WorkerPrivate* aWorkerPrivate) +{ + WorkerDebuggerManager* manager; + + if (NS_IsMainThread()) { + manager = WorkerDebuggerManager::GetOrCreate(); + if (!manager) { + NS_WARNING("Failed to create worker debugger manager!"); + return NS_ERROR_FAILURE; + } + } + else { + manager = WorkerDebuggerManager::Get(); + } + + manager->RegisterDebugger(aWorkerPrivate); + return NS_OK; +} + +inline nsresult +UnregisterWorkerDebugger(WorkerPrivate* aWorkerPrivate) +{ + WorkerDebuggerManager* manager; + + if (NS_IsMainThread()) { + manager = WorkerDebuggerManager::GetOrCreate(); + if (!manager) { + NS_WARNING("Failed to create worker debugger manager!"); + return NS_ERROR_FAILURE; + } + } + else { + manager = WorkerDebuggerManager::Get(); + } + + manager->UnregisterDebugger(aWorkerPrivate); + return NS_OK; +} + +END_WORKERS_NAMESPACE + +#endif // mozilla_dom_workers_workerdebuggermanager_h diff --git a/dom/workers/WorkerHolder.cpp b/dom/workers/WorkerHolder.cpp new file mode 100644 index 000000000..6383ae5a1 --- /dev/null +++ b/dom/workers/WorkerHolder.cpp @@ -0,0 +1,60 @@ +/* -*- 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 "WorkerHolder.h" +#include "WorkerPrivate.h" + +BEGIN_WORKERS_NAMESPACE + +WorkerHolder::WorkerHolder() + : mWorkerPrivate(nullptr) +{ +} + +WorkerHolder::~WorkerHolder() +{ + NS_ASSERT_OWNINGTHREAD(WorkerHolder); + ReleaseWorkerInternal(); + MOZ_ASSERT(mWorkerPrivate == nullptr); +} + +bool +WorkerHolder::HoldWorker(WorkerPrivate* aWorkerPrivate, Status aFailStatus) +{ + NS_ASSERT_OWNINGTHREAD(WorkerHolder); + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + if (!aWorkerPrivate->AddHolder(this, aFailStatus)) { + return false; + } + + mWorkerPrivate = aWorkerPrivate; + return true; +} + +void +WorkerHolder::ReleaseWorker() +{ + NS_ASSERT_OWNINGTHREAD(WorkerHolder); + MOZ_ASSERT(mWorkerPrivate); + + ReleaseWorkerInternal(); +} + +void +WorkerHolder::ReleaseWorkerInternal() +{ + NS_ASSERT_OWNINGTHREAD(WorkerHolder); + + if (mWorkerPrivate) { + mWorkerPrivate->AssertIsOnWorkerThread(); + mWorkerPrivate->RemoveHolder(this); + mWorkerPrivate = nullptr; + } +} + +END_WORKERS_NAMESPACE diff --git a/dom/workers/WorkerHolder.h b/dom/workers/WorkerHolder.h new file mode 100644 index 000000000..d0eb22f29 --- /dev/null +++ b/dom/workers/WorkerHolder.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/. */ + +#ifndef mozilla_dom_workers_WorkerHolder_h +#define mozilla_dom_workers_WorkerHolder_h + +#include "mozilla/dom/workers/Workers.h" + +BEGIN_WORKERS_NAMESPACE + +/** + * Use this chart to help figure out behavior during each of the closing + * statuses. Details below. + * + * +==============================================================+ + * | Closing Statuses | + * +=============+=============+=================+================+ + * | status | clear queue | abort execution | close handler | + * +=============+=============+=================+================+ + * | Closing | yes | no | no timeout | + * +-------------+-------------+-----------------+----------------+ + * | Terminating | yes | yes | no timeout | + * +-------------+-------------+-----------------+----------------+ + * | Canceling | yes | yes | short duration | + * +-------------+-------------+-----------------+----------------+ + * | Killing | yes | yes | doesn't run | + * +-------------+-------------+-----------------+----------------+ + */ + +#ifdef Status +/* Xlib headers insist on this for some reason... Nuke it because + it'll override our member name */ +#undef Status +#endif +enum Status +{ + // Not yet scheduled. + Pending = 0, + + // This status means that the close handler has not yet been scheduled. + Running, + + // Inner script called close() on the worker global scope. Setting this + // status causes the worker to clear its queue of events but does not abort + // the currently running script. The close handler is also scheduled with + // no expiration time. + Closing, + + // Outer script called terminate() on the worker or the worker object was + // garbage collected in its outer script. Setting this status causes the + // worker to abort immediately, clear its queue of events, and schedules the + // close handler with no expiration time. + Terminating, + + // Either the user navigated away from the owning page or the owning page fell + // out of bfcache. Setting this status causes the worker to abort immediately + // and schedules the close handler with a short expiration time. Since the + // page has gone away the worker may not post any messages. + Canceling, + + // The application is shutting down. Setting this status causes the worker to + // abort immediately and the close handler is never scheduled. + Killing, + + // The close handler has run and the worker is effectively dead. + Dead +}; + +class WorkerHolder +{ +public: + NS_DECL_OWNINGTHREAD + + WorkerHolder(); + virtual ~WorkerHolder(); + + bool HoldWorker(WorkerPrivate* aWorkerPrivate, Status aFailStatus); + void ReleaseWorker(); + + virtual bool Notify(Status aStatus) = 0; + +protected: + void ReleaseWorkerInternal(); + + WorkerPrivate* MOZ_NON_OWNING_REF mWorkerPrivate; + +private: + void AssertIsOwningThread() const; +}; + +END_WORKERS_NAMESPACE + +#endif /* mozilla_dom_workers_WorkerHolder_h */ diff --git a/dom/workers/WorkerInlines.h b/dom/workers/WorkerInlines.h new file mode 100644 index 000000000..4fd7fd9a4 --- /dev/null +++ b/dom/workers/WorkerInlines.h @@ -0,0 +1,25 @@ +/* -*- 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/. */ + +BEGIN_WORKERS_NAMESPACE + +inline +void +SetJSPrivateSafeish(JSObject* aObj, PrivatizableBase* aBase) +{ + JS_SetPrivate(aObj, aBase); +} + +template <class Derived> +inline +Derived* +GetJSPrivateSafeish(JSObject* aObj) +{ + return static_cast<Derived*>( + static_cast<PrivatizableBase*>(JS_GetPrivate(aObj))); +} + +END_WORKERS_NAMESPACE diff --git a/dom/workers/WorkerLocation.cpp b/dom/workers/WorkerLocation.cpp new file mode 100644 index 000000000..469bf84bf --- /dev/null +++ b/dom/workers/WorkerLocation.cpp @@ -0,0 +1,43 @@ +/* -*- 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/WorkerLocation.h" + +#include "mozilla/dom/WorkerLocationBinding.h" + +namespace mozilla { +namespace dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(WorkerLocation) + +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(WorkerLocation, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(WorkerLocation, Release) + +/* static */ already_AddRefed<WorkerLocation> +WorkerLocation::Create(workers::WorkerPrivate::LocationInfo& aInfo) +{ + RefPtr<WorkerLocation> location = + new WorkerLocation(NS_ConvertUTF8toUTF16(aInfo.mHref), + NS_ConvertUTF8toUTF16(aInfo.mProtocol), + NS_ConvertUTF8toUTF16(aInfo.mHost), + NS_ConvertUTF8toUTF16(aInfo.mHostname), + NS_ConvertUTF8toUTF16(aInfo.mPort), + NS_ConvertUTF8toUTF16(aInfo.mPathname), + NS_ConvertUTF8toUTF16(aInfo.mSearch), + NS_ConvertUTF8toUTF16(aInfo.mHash), + aInfo.mOrigin); + + return location.forget(); +} + +JSObject* +WorkerLocation::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return WorkerLocationBinding::Wrap(aCx, this, aGivenProto); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/WorkerLocation.h b/dom/workers/WorkerLocation.h new file mode 100644 index 000000000..73137efff --- /dev/null +++ b/dom/workers/WorkerLocation.h @@ -0,0 +1,116 @@ +/* -*- 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_location_h__ +#define mozilla_dom_location_h__ + +#include "Workers.h" +#include "WorkerPrivate.h" +#include "nsWrapperCache.h" + +namespace mozilla { +namespace dom { + +class WorkerLocation final : public nsWrapperCache +{ + nsString mHref; + nsString mProtocol; + nsString mHost; + nsString mHostname; + nsString mPort; + nsString mPathname; + nsString mSearch; + nsString mHash; + nsString mOrigin; + + WorkerLocation(const nsAString& aHref, + const nsAString& aProtocol, + const nsAString& aHost, + const nsAString& aHostname, + const nsAString& aPort, + const nsAString& aPathname, + const nsAString& aSearch, + const nsAString& aHash, + const nsAString& aOrigin) + : mHref(aHref) + , mProtocol(aProtocol) + , mHost(aHost) + , mHostname(aHostname) + , mPort(aPort) + , mPathname(aPathname) + , mSearch(aSearch) + , mHash(aHash) + , mOrigin(aOrigin) + { + MOZ_COUNT_CTOR(WorkerLocation); + } + + ~WorkerLocation() + { + MOZ_COUNT_DTOR(WorkerLocation); + } + +public: + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(WorkerLocation) + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(WorkerLocation) + + static already_AddRefed<WorkerLocation> + Create(workers::WorkerPrivate::LocationInfo& aInfo); + + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + nsISupports* GetParentObject() const { + return nullptr; + } + + void Stringify(nsString& aHref) const + { + aHref = mHref; + } + void GetHref(nsString& aHref) const + { + aHref = mHref; + } + void GetProtocol(nsString& aProtocol) const + { + aProtocol = mProtocol; + } + void GetHost(nsString& aHost) const + { + aHost = mHost; + } + void GetHostname(nsString& aHostname) const + { + aHostname = mHostname; + } + void GetPort(nsString& aPort) const + { + aPort = mPort; + } + void GetPathname(nsString& aPathname) const + { + aPathname = mPathname; + } + void GetSearch(nsString& aSearch) const + { + aSearch = mSearch; + } + void GetHash(nsString& aHash) const + { + aHash = mHash; + } + void GetOrigin(nsString& aOrigin) const + { + aOrigin = mOrigin; + } +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_location_h__ diff --git a/dom/workers/WorkerNavigator.cpp b/dom/workers/WorkerNavigator.cpp new file mode 100644 index 000000000..682c7a22c --- /dev/null +++ b/dom/workers/WorkerNavigator.cpp @@ -0,0 +1,184 @@ +/* -*- 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/BindingUtils.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseWorkerProxy.h" +#include "mozilla/dom/StorageManager.h" +#include "mozilla/dom/WorkerNavigator.h" +#include "mozilla/dom/WorkerNavigatorBinding.h" + +#include "nsProxyRelease.h" +#include "RuntimeService.h" + +#include "nsIDocument.h" + +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" +#include "WorkerScope.h" + +#include "mozilla/dom/Navigator.h" + +namespace mozilla { +namespace dom { + +using namespace mozilla::dom::workers; + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(WorkerNavigator, mStorageManager); +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(WorkerNavigator, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(WorkerNavigator, Release) + +/* static */ already_AddRefed<WorkerNavigator> +WorkerNavigator::Create(bool aOnLine) +{ + RuntimeService* rts = RuntimeService::GetService(); + MOZ_ASSERT(rts); + + const RuntimeService::NavigatorProperties& properties = + rts->GetNavigatorProperties(); + + RefPtr<WorkerNavigator> navigator = + new WorkerNavigator(properties, aOnLine); + + return navigator.forget(); +} + +JSObject* +WorkerNavigator::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return WorkerNavigatorBinding::Wrap(aCx, this, aGivenProto); +} + +void +WorkerNavigator::SetLanguages(const nsTArray<nsString>& aLanguages) +{ + WorkerNavigatorBinding::ClearCachedLanguagesValue(this); + mProperties.mLanguages = aLanguages; +} + +void +WorkerNavigator::GetAppName(nsString& aAppName) const +{ + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + if (!mProperties.mAppNameOverridden.IsEmpty() && + !workerPrivate->UsesSystemPrincipal()) { + aAppName = mProperties.mAppNameOverridden; + } else { + aAppName = mProperties.mAppName; + } +} + +void +WorkerNavigator::GetAppVersion(nsString& aAppVersion) const +{ + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + if (!mProperties.mAppVersionOverridden.IsEmpty() && + !workerPrivate->UsesSystemPrincipal()) { + aAppVersion = mProperties.mAppVersionOverridden; + } else { + aAppVersion = mProperties.mAppVersion; + } +} + +void +WorkerNavigator::GetPlatform(nsString& aPlatform) const +{ + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + if (!mProperties.mPlatformOverridden.IsEmpty() && + !workerPrivate->UsesSystemPrincipal()) { + aPlatform = mProperties.mPlatformOverridden; + } else { + aPlatform = mProperties.mPlatform; + } +} + +namespace { + +class GetUserAgentRunnable final : public WorkerMainThreadRunnable +{ + nsString& mUA; + +public: + GetUserAgentRunnable(WorkerPrivate* aWorkerPrivate, nsString& aUA) + : WorkerMainThreadRunnable(aWorkerPrivate, + NS_LITERAL_CSTRING("UserAgent getter")) + , mUA(aUA) + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + virtual bool MainThreadRun() override + { + AssertIsOnMainThread(); + + nsCOMPtr<nsPIDOMWindowInner> window = mWorkerPrivate->GetWindow(); + nsCOMPtr<nsIURI> uri; + if (window && window->GetDocShell()) { + nsIDocument* doc = window->GetExtantDoc(); + if (doc) { + doc->NodePrincipal()->GetURI(getter_AddRefs(uri)); + } + } + + bool isCallerChrome = mWorkerPrivate->UsesSystemPrincipal(); + nsresult rv = dom::Navigator::GetUserAgent(window, uri, + isCallerChrome, mUA); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to retrieve user-agent from the worker thread."); + } + + return true; + } +}; + +} // namespace + +void +WorkerNavigator::GetUserAgent(nsString& aUserAgent, ErrorResult& aRv) const +{ + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + RefPtr<GetUserAgentRunnable> runnable = + new GetUserAgentRunnable(workerPrivate, aUserAgent); + + runnable->Dispatch(aRv); +} + +uint64_t +WorkerNavigator::HardwareConcurrency() const +{ + RuntimeService* rts = RuntimeService::GetService(); + MOZ_ASSERT(rts); + + return rts->ClampedHardwareConcurrency(); +} + +StorageManager* +WorkerNavigator::Storage() +{ + if (!mStorageManager) { + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + RefPtr<nsIGlobalObject> global = workerPrivate->GlobalScope(); + MOZ_ASSERT(global); + + mStorageManager = new StorageManager(global); + } + + return mStorageManager; +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/WorkerNavigator.h b/dom/workers/WorkerNavigator.h new file mode 100644 index 000000000..64338e46b --- /dev/null +++ b/dom/workers/WorkerNavigator.h @@ -0,0 +1,114 @@ +/* -*- 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_workernavigator_h__ +#define mozilla_dom_workernavigator_h__ + +#include "Workers.h" +#include "RuntimeService.h" +#include "nsString.h" +#include "nsWrapperCache.h" +#include "mozilla/dom/StorageManager.h" + +namespace mozilla { +namespace dom { +class Promise; +class StorageManager; + +class WorkerNavigator final : public nsWrapperCache +{ + typedef struct workers::RuntimeService::NavigatorProperties NavigatorProperties; + + NavigatorProperties mProperties; + RefPtr<StorageManager> mStorageManager; + bool mOnline; + + WorkerNavigator(const NavigatorProperties& aProperties, + bool aOnline) + : mProperties(aProperties) + , mOnline(aOnline) + { + MOZ_COUNT_CTOR(WorkerNavigator); + } + + ~WorkerNavigator() + { + MOZ_COUNT_DTOR(WorkerNavigator); + } + +public: + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(WorkerNavigator) + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(WorkerNavigator) + + static already_AddRefed<WorkerNavigator> + Create(bool aOnLine); + + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + nsISupports* GetParentObject() const { + return nullptr; + } + + void GetAppCodeName(nsString& aAppCodeName) const + { + aAppCodeName.AssignLiteral("Mozilla"); + } + void GetAppName(nsString& aAppName) const; + + void GetAppVersion(nsString& aAppVersion) const; + + void GetPlatform(nsString& aPlatform) const; + + void GetProduct(nsString& aProduct) const + { + aProduct.AssignLiteral("Gecko"); + } + + bool TaintEnabled() const + { + return false; + } + + void GetLanguage(nsString& aLanguage) const + { + if (mProperties.mLanguages.Length() >= 1) { + aLanguage.Assign(mProperties.mLanguages[0]); + } else { + aLanguage.Truncate(); + } + } + + void GetLanguages(nsTArray<nsString>& aLanguages) const + { + aLanguages = mProperties.mLanguages; + } + + void GetUserAgent(nsString& aUserAgent, ErrorResult& aRv) const; + + bool OnLine() const + { + return mOnline; + } + + // Worker thread only! + void SetOnLine(bool aOnline) + { + mOnline = aOnline; + } + + void SetLanguages(const nsTArray<nsString>& aLanguages); + + uint64_t HardwareConcurrency() const; + + StorageManager* Storage(); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workernavigator_h__ diff --git a/dom/workers/WorkerPrefs.h b/dom/workers/WorkerPrefs.h new file mode 100644 index 000000000..c9b605a84 --- /dev/null +++ b/dom/workers/WorkerPrefs.h @@ -0,0 +1,49 @@ +/* -*- 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/. */ + +// This is the list of the preferences that are exposed to workers. +// The format is as follows: +// +// WORKER_SIMPLE_PREF("foo.bar", FooBar, FOO_BAR, UpdaterFunction) +// +// * First argument is the name of the pref. +// * The name of the getter function. This defines a FindName() +// function that returns the value of the pref on WorkerPrivate. +// * The macro version of the name. This defines a WORKERPREF_FOO_BAR +// macro in Workers.h. +// * The name of the function that updates the new value of a pref. +// +// WORKER_PREF("foo.bar", UpdaterFunction) +// +// * First argument is the name of the pref. +// * The name of the function that updates the new value of a pref. + +#if !(defined(DEBUG) || defined(MOZ_ENABLE_JS_DUMP)) +WORKER_SIMPLE_PREF("browser.dom.window.dump.enabled", DumpEnabled, DUMP) +#endif +WORKER_SIMPLE_PREF("canvas.imagebitmap_extensions.enabled", ImageBitmapExtensionsEnabled, IMAGEBITMAP_EXTENSIONS_ENABLED) +WORKER_SIMPLE_PREF("dom.caches.enabled", DOMCachesEnabled, DOM_CACHES) +WORKER_SIMPLE_PREF("dom.caches.testing.enabled", DOMCachesTestingEnabled, DOM_CACHES_TESTING) +WORKER_SIMPLE_PREF("dom.performance.enable_user_timing_logging", PerformanceLoggingEnabled, PERFORMANCE_LOGGING_ENABLED) +WORKER_SIMPLE_PREF("dom.webnotifications.enabled", DOMWorkerNotificationEnabled, DOM_WORKERNOTIFICATION) +WORKER_SIMPLE_PREF("dom.webnotifications.serviceworker.enabled", DOMServiceWorkerNotificationEnabled, DOM_SERVICEWORKERNOTIFICATION) +WORKER_SIMPLE_PREF("dom.webnotifications.requireinteraction.enabled", DOMWorkerNotificationRIEnabled, DOM_WORKERNOTIFICATIONRI) +WORKER_SIMPLE_PREF("dom.serviceWorkers.enabled", ServiceWorkersEnabled, SERVICEWORKERS_ENABLED) +WORKER_SIMPLE_PREF("dom.serviceWorkers.testing.enabled", ServiceWorkersTestingEnabled, SERVICEWORKERS_TESTING_ENABLED) +WORKER_SIMPLE_PREF("dom.serviceWorkers.openWindow.enabled", OpenWindowEnabled, OPEN_WINDOW_ENABLED) +WORKER_SIMPLE_PREF("dom.storageManager.enabled", StorageManagerEnabled, STORAGEMANAGER_ENABLED) +WORKER_SIMPLE_PREF("dom.push.enabled", PushEnabled, PUSH_ENABLED) +WORKER_SIMPLE_PREF("dom.requestcontext.enabled", RequestContextEnabled, REQUESTCONTEXT_ENABLED) +WORKER_SIMPLE_PREF("gfx.offscreencanvas.enabled", OffscreenCanvasEnabled, OFFSCREENCANVAS_ENABLED) +WORKER_SIMPLE_PREF("dom.webkitBlink.dirPicker.enabled", WebkitBlinkDirectoryPickerEnabled, DOM_WEBKITBLINK_DIRPICKER_WEBKITBLINK) +WORKER_PREF("dom.workers.latestJSVersion", JSVersionChanged) +WORKER_PREF("intl.accept_languages", PrefLanguagesChanged) +WORKER_PREF("general.appname.override", AppNameOverrideChanged) +WORKER_PREF("general.appversion.override", AppVersionOverrideChanged) +WORKER_PREF("general.platform.override", PlatformOverrideChanged) +#ifdef JS_GC_ZEAL +WORKER_PREF("dom.workers.options.gcZeal", LoadGCZealOptions) +#endif diff --git a/dom/workers/WorkerPrivate.cpp b/dom/workers/WorkerPrivate.cpp new file mode 100644 index 000000000..1df4e5551 --- /dev/null +++ b/dom/workers/WorkerPrivate.cpp @@ -0,0 +1,6752 @@ +/* -*- 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 "WorkerPrivate.h" + +#include "amIAddonManager.h" +#include "nsIClassInfo.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIConsoleService.h" +#include "nsIDOMDOMException.h" +#include "nsIDOMEvent.h" +#include "nsIDocument.h" +#include "nsIDocShell.h" +#include "nsIInterfaceRequestor.h" +#include "nsIMemoryReporter.h" +#include "nsINetworkInterceptController.h" +#include "nsIPermissionManager.h" +#include "nsIScriptError.h" +#include "nsIScriptGlobalObject.h" +#include "nsIScriptSecurityManager.h" +#include "nsIScriptTimeoutHandler.h" +#include "nsITabChild.h" +#include "nsITextToSubURI.h" +#include "nsIThreadInternal.h" +#include "nsITimer.h" +#include "nsIURI.h" +#include "nsIURL.h" +#include "nsIWeakReferenceUtils.h" +#include "nsIWorkerDebugger.h" +#include "nsIXPConnect.h" +#include "nsPIDOMWindow.h" +#include "nsGlobalWindow.h" + +#include <algorithm> +#include "ImageContainer.h" +#include "jsfriendapi.h" +#include "js/MemoryMetrics.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/Likely.h" +#include "mozilla/LoadContext.h" +#include "mozilla/Move.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/Console.h" +#include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/ErrorEvent.h" +#include "mozilla/dom/ErrorEventBinding.h" +#include "mozilla/dom/Exceptions.h" +#include "mozilla/dom/ExtendableMessageEventBinding.h" +#include "mozilla/dom/FunctionBinding.h" +#include "mozilla/dom/IndexedDatabaseManager.h" +#include "mozilla/dom/MessageEvent.h" +#include "mozilla/dom/MessageEventBinding.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/dom/MessagePortBinding.h" +#include "mozilla/dom/Performance.h" +#include "mozilla/dom/PMessagePort.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseDebugging.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/SimpleGlobalObject.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/StructuredCloneHolder.h" +#include "mozilla/dom/TabChild.h" +#include "mozilla/dom/WorkerBinding.h" +#include "mozilla/dom/WorkerDebuggerGlobalScopeBinding.h" +#include "mozilla/dom/WorkerGlobalScopeBinding.h" +#include "mozilla/Preferences.h" +#include "mozilla/ThrottledEventQueue.h" +#include "mozilla/TimelineConsumers.h" +#include "mozilla/WorkerTimelineMarker.h" +#include "nsAlgorithm.h" +#include "nsContentUtils.h" +#include "nsCycleCollector.h" +#include "nsError.h" +#include "nsDOMJSUtils.h" +#include "nsHostObjectProtocolHandler.h" +#include "nsJSEnvironment.h" +#include "nsJSUtils.h" +#include "nsNetUtil.h" +#include "nsPrintfCString.h" +#include "nsProxyRelease.h" +#include "nsQueryObject.h" +#include "nsSandboxFlags.h" +#include "nsUTF8Utils.h" +#include "prthread.h" +#include "xpcpublic.h" + +#ifdef ANDROID +#include <android/log.h> +#endif + +#ifdef DEBUG +#include "nsThreadManager.h" +#endif + +#include "Navigator.h" +#include "Principal.h" +#include "RuntimeService.h" +#include "ScriptLoader.h" +#include "ServiceWorkerEvents.h" +#include "ServiceWorkerManager.h" +#include "ServiceWorkerWindowClient.h" +#include "SharedWorker.h" +#include "WorkerDebuggerManager.h" +#include "WorkerHolder.h" +#include "WorkerNavigator.h" +#include "WorkerRunnable.h" +#include "WorkerScope.h" +#include "WorkerThread.h" + +// JS_MaybeGC will run once every second during normal execution. +#define PERIODIC_GC_TIMER_DELAY_SEC 1 + +// A shrinking GC will run five seconds after the last event is processed. +#define IDLE_GC_TIMER_DELAY_SEC 5 + +#define PREF_WORKERS_ENABLED "dom.workers.enabled" + +static mozilla::LazyLogModule sWorkerPrivateLog("WorkerPrivate"); +static mozilla::LazyLogModule sWorkerTimeoutsLog("WorkerTimeouts"); + +mozilla::LogModule* +WorkerLog() +{ + return sWorkerPrivateLog; +} + +mozilla::LogModule* +TimeoutsLog() +{ + return sWorkerTimeoutsLog; +} + +#define LOG(log, _args) MOZ_LOG(log, LogLevel::Debug, _args); + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::ipc; + +USING_WORKERS_NAMESPACE + +MOZ_DEFINE_MALLOC_SIZE_OF(JsWorkerMallocSizeOf) + +#ifdef DEBUG + +BEGIN_WORKERS_NAMESPACE + +void +AssertIsOnMainThread() +{ + MOZ_ASSERT(NS_IsMainThread(), "Wrong thread!"); +} + +END_WORKERS_NAMESPACE + +#endif + +namespace { + +#ifdef DEBUG + +const nsIID kDEBUGWorkerEventTargetIID = { + 0xccaba3fa, 0x5be2, 0x4de2, { 0xba, 0x87, 0x3b, 0x3b, 0x5b, 0x1d, 0x5, 0xfb } +}; + +#endif + +template <class T> +class AutoPtrComparator +{ + typedef nsAutoPtr<T> A; + typedef T* B; + +public: + bool Equals(const A& a, const B& b) const { + return a && b ? *a == *b : !a && !b ? true : false; + } + bool LessThan(const A& a, const B& b) const { + return a && b ? *a < *b : b ? true : false; + } +}; + +template <class T> +inline AutoPtrComparator<T> +GetAutoPtrComparator(const nsTArray<nsAutoPtr<T> >&) +{ + return AutoPtrComparator<T>(); +} + +// Specialize this if there's some class that has multiple nsISupports bases. +template <class T> +struct ISupportsBaseInfo +{ + typedef T ISupportsBase; +}; + +template <template <class> class SmartPtr, class T> +inline void +SwapToISupportsArray(SmartPtr<T>& aSrc, + nsTArray<nsCOMPtr<nsISupports> >& aDest) +{ + nsCOMPtr<nsISupports>* dest = aDest.AppendElement(); + + T* raw = nullptr; + aSrc.swap(raw); + + nsISupports* rawSupports = + static_cast<typename ISupportsBaseInfo<T>::ISupportsBase*>(raw); + dest->swap(rawSupports); +} + +// This class is used to wrap any runnables that the worker receives via the +// nsIEventTarget::Dispatch() method (either from NS_DispatchToCurrentThread or +// from the worker's EventTarget). +class ExternalRunnableWrapper final : public WorkerRunnable +{ + nsCOMPtr<nsIRunnable> mWrappedRunnable; + +public: + ExternalRunnableWrapper(WorkerPrivate* aWorkerPrivate, + nsIRunnable* aWrappedRunnable) + : WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), + mWrappedRunnable(aWrappedRunnable) + { + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aWrappedRunnable); + } + + NS_DECL_ISUPPORTS_INHERITED + +private: + ~ExternalRunnableWrapper() + { } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + nsresult rv = mWrappedRunnable->Run(); + if (NS_FAILED(rv)) { + if (!JS_IsExceptionPending(aCx)) { + Throw(aCx, rv); + } + return false; + } + return true; + } + + nsresult + Cancel() override + { + nsresult rv; + nsCOMPtr<nsICancelableRunnable> cancelable = + do_QueryInterface(mWrappedRunnable); + MOZ_ASSERT(cancelable); // We checked this earlier! + rv = cancelable->Cancel(); + nsresult rv2 = WorkerRunnable::Cancel(); + return NS_FAILED(rv) ? rv : rv2; + } +}; + +struct WindowAction +{ + nsPIDOMWindowInner* mWindow; + bool mDefaultAction; + + MOZ_IMPLICIT WindowAction(nsPIDOMWindowInner* aWindow) + : mWindow(aWindow), mDefaultAction(true) + { } + + bool + operator==(const WindowAction& aOther) const + { + return mWindow == aOther.mWindow; + } +}; + +void +LogErrorToConsole(const nsAString& aMessage, + const nsAString& aFilename, + const nsAString& aLine, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aFlags, + uint64_t aInnerWindowId) +{ + AssertIsOnMainThread(); + + nsCOMPtr<nsIScriptError> scriptError = + do_CreateInstance(NS_SCRIPTERROR_CONTRACTID); + NS_WARNING_ASSERTION(scriptError, "Failed to create script error!"); + + if (scriptError) { + if (NS_FAILED(scriptError->InitWithWindowID(aMessage, aFilename, aLine, + aLineNumber, aColumnNumber, + aFlags, "Web Worker", + aInnerWindowId))) { + NS_WARNING("Failed to init script error!"); + scriptError = nullptr; + } + } + + nsCOMPtr<nsIConsoleService> consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + NS_WARNING_ASSERTION(consoleService, "Failed to get console service!"); + + if (consoleService) { + if (scriptError) { + if (NS_SUCCEEDED(consoleService->LogMessage(scriptError))) { + return; + } + NS_WARNING("LogMessage failed!"); + } else if (NS_SUCCEEDED(consoleService->LogStringMessage( + aMessage.BeginReading()))) { + return; + } + NS_WARNING("LogStringMessage failed!"); + } + + NS_ConvertUTF16toUTF8 msg(aMessage); + NS_ConvertUTF16toUTF8 filename(aFilename); + + static const char kErrorString[] = "JS error in Web Worker: %s [%s:%u]"; + +#ifdef ANDROID + __android_log_print(ANDROID_LOG_INFO, "Gecko", kErrorString, msg.get(), + filename.get(), aLineNumber); +#endif + + fprintf(stderr, kErrorString, msg.get(), filename.get(), aLineNumber); + fflush(stderr); +} + +class MainThreadReleaseRunnable final : public Runnable +{ + nsTArray<nsCOMPtr<nsISupports>> mDoomed; + nsCOMPtr<nsILoadGroup> mLoadGroupToCancel; + +public: + MainThreadReleaseRunnable(nsTArray<nsCOMPtr<nsISupports>>& aDoomed, + nsCOMPtr<nsILoadGroup>& aLoadGroupToCancel) + { + mDoomed.SwapElements(aDoomed); + mLoadGroupToCancel.swap(aLoadGroupToCancel); + } + + NS_DECL_ISUPPORTS_INHERITED + + NS_IMETHOD + Run() override + { + if (mLoadGroupToCancel) { + mLoadGroupToCancel->Cancel(NS_BINDING_ABORTED); + mLoadGroupToCancel = nullptr; + } + + mDoomed.Clear(); + return NS_OK; + } + +private: + ~MainThreadReleaseRunnable() + { } +}; + +class WorkerFinishedRunnable final : public WorkerControlRunnable +{ + WorkerPrivate* mFinishedWorker; + +public: + WorkerFinishedRunnable(WorkerPrivate* aWorkerPrivate, + WorkerPrivate* aFinishedWorker) + : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), + mFinishedWorker(aFinishedWorker) + { } + +private: + virtual bool + PreDispatch(WorkerPrivate* aWorkerPrivate) override + { + // Silence bad assertions. + return true; + } + + virtual void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override + { + // Silence bad assertions. + } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + nsCOMPtr<nsILoadGroup> loadGroupToCancel; + mFinishedWorker->ForgetOverridenLoadGroup(loadGroupToCancel); + + nsTArray<nsCOMPtr<nsISupports>> doomed; + mFinishedWorker->ForgetMainThreadObjects(doomed); + + RefPtr<MainThreadReleaseRunnable> runnable = + new MainThreadReleaseRunnable(doomed, loadGroupToCancel); + if (NS_FAILED(mWorkerPrivate->DispatchToMainThread(runnable.forget()))) { + NS_WARNING("Failed to dispatch, going to leak!"); + } + + RuntimeService* runtime = RuntimeService::GetService(); + NS_ASSERTION(runtime, "This should never be null!"); + + mFinishedWorker->DisableDebugger(); + + runtime->UnregisterWorker(mFinishedWorker); + + mFinishedWorker->ClearSelfRef(); + return true; + } +}; + +class TopLevelWorkerFinishedRunnable final : public Runnable +{ + WorkerPrivate* mFinishedWorker; + +public: + explicit TopLevelWorkerFinishedRunnable(WorkerPrivate* aFinishedWorker) + : mFinishedWorker(aFinishedWorker) + { + aFinishedWorker->AssertIsOnWorkerThread(); + } + + NS_DECL_ISUPPORTS_INHERITED + +private: + ~TopLevelWorkerFinishedRunnable() {} + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + + RuntimeService* runtime = RuntimeService::GetService(); + MOZ_ASSERT(runtime); + + mFinishedWorker->DisableDebugger(); + + runtime->UnregisterWorker(mFinishedWorker); + + nsCOMPtr<nsILoadGroup> loadGroupToCancel; + mFinishedWorker->ForgetOverridenLoadGroup(loadGroupToCancel); + + nsTArray<nsCOMPtr<nsISupports> > doomed; + mFinishedWorker->ForgetMainThreadObjects(doomed); + + RefPtr<MainThreadReleaseRunnable> runnable = + new MainThreadReleaseRunnable(doomed, loadGroupToCancel); + if (NS_FAILED(NS_DispatchToCurrentThread(runnable))) { + NS_WARNING("Failed to dispatch, going to leak!"); + } + + mFinishedWorker->ClearSelfRef(); + return NS_OK; + } +}; + +class ModifyBusyCountRunnable final : public WorkerControlRunnable +{ + bool mIncrease; + +public: + ModifyBusyCountRunnable(WorkerPrivate* aWorkerPrivate, bool aIncrease) + : WorkerControlRunnable(aWorkerPrivate, ParentThreadUnchangedBusyCount), + mIncrease(aIncrease) + { } + +private: + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + return aWorkerPrivate->ModifyBusyCount(mIncrease); + } + + virtual void + PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aRunResult) + override + { + if (mIncrease) { + WorkerControlRunnable::PostRun(aCx, aWorkerPrivate, aRunResult); + return; + } + // Don't do anything here as it's possible that aWorkerPrivate has been + // deleted. + } +}; + +class CompileScriptRunnable final : public WorkerRunnable +{ + nsString mScriptURL; + +public: + explicit CompileScriptRunnable(WorkerPrivate* aWorkerPrivate, + const nsAString& aScriptURL) + : WorkerRunnable(aWorkerPrivate), + mScriptURL(aScriptURL) + { } + +private: + // We can't implement PreRun effectively, because at the point when that would + // run we have not yet done our load so don't know things like our final + // principal and whatnot. + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + aWorkerPrivate->AssertIsOnWorkerThread(); + + ErrorResult rv; + scriptloader::LoadMainScript(aWorkerPrivate, mScriptURL, WorkerScript, rv); + rv.WouldReportJSException(); + // Explicitly ignore NS_BINDING_ABORTED on rv. Or more precisely, still + // return false and don't SetWorkerScriptExecutedSuccessfully() in that + // case, but don't throw anything on aCx. The idea is to not dispatch error + // events if our load is canceled with that error code. + if (rv.ErrorCodeIs(NS_BINDING_ABORTED)) { + rv.SuppressException(); + return false; + } + + WorkerGlobalScope* globalScope = aWorkerPrivate->GlobalScope(); + if (NS_WARN_IF(!globalScope)) { + // We never got as far as calling GetOrCreateGlobalScope, or it failed. + // We have no way to enter a compartment, hence no sane way to report this + // error. :( + rv.SuppressException(); + return false; + } + + // Make sure to propagate exceptions from rv onto aCx, so that they will get + // reported after we return. We do this for all failures on rv, because now + // we're using rv to track all the state we care about. + // + // This is a little dumb, but aCx is in the null compartment here because we + // set it up that way in our Run(), since we had not created the global at + // that point yet. So we need to enter the compartment of our global, + // because setting a pending exception on aCx involves wrapping into its + // current compartment. Luckily we have a global now. + JSAutoCompartment ac(aCx, globalScope->GetGlobalJSObject()); + if (rv.MaybeSetPendingException(aCx)) { + return false; + } + + aWorkerPrivate->SetWorkerScriptExecutedSuccessfully(); + return true; + } +}; + +class CompileDebuggerScriptRunnable final : public WorkerDebuggerRunnable +{ + nsString mScriptURL; + +public: + CompileDebuggerScriptRunnable(WorkerPrivate* aWorkerPrivate, + const nsAString& aScriptURL) + : WorkerDebuggerRunnable(aWorkerPrivate), + mScriptURL(aScriptURL) + { } + +private: + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + aWorkerPrivate->AssertIsOnWorkerThread(); + + WorkerDebuggerGlobalScope* globalScope = + aWorkerPrivate->CreateDebuggerGlobalScope(aCx); + if (!globalScope) { + NS_WARNING("Failed to make global!"); + return false; + } + + JS::Rooted<JSObject*> global(aCx, globalScope->GetWrapper()); + + ErrorResult rv; + JSAutoCompartment ac(aCx, global); + scriptloader::LoadMainScript(aWorkerPrivate, mScriptURL, + DebuggerScript, rv); + rv.WouldReportJSException(); + // Explicitly ignore NS_BINDING_ABORTED on rv. Or more precisely, still + // return false and don't SetWorkerScriptExecutedSuccessfully() in that + // case, but don't throw anything on aCx. The idea is to not dispatch error + // events if our load is canceled with that error code. + if (rv.ErrorCodeIs(NS_BINDING_ABORTED)) { + rv.SuppressException(); + return false; + } + // Make sure to propagate exceptions from rv onto aCx, so that they will get + // reported after we return. We do this for all failures on rv, because now + // we're using rv to track all the state we care about. + if (rv.MaybeSetPendingException(aCx)) { + return false; + } + + return true; + } +}; + +class MessageEventRunnable final : public WorkerRunnable + , public StructuredCloneHolder +{ + // This is only used for messages dispatched to a service worker. + UniquePtr<ServiceWorkerClientInfo> mEventSource; + + RefPtr<PromiseNativeHandler> mHandler; + +public: + MessageEventRunnable(WorkerPrivate* aWorkerPrivate, + TargetAndBusyBehavior aBehavior) + : WorkerRunnable(aWorkerPrivate, aBehavior) + , StructuredCloneHolder(CloningSupported, TransferringSupported, + StructuredCloneScope::SameProcessDifferentThread) + { + } + + void + SetServiceWorkerData(UniquePtr<ServiceWorkerClientInfo>&& aSource, + PromiseNativeHandler* aHandler) + { + mEventSource = Move(aSource); + mHandler = aHandler; + } + + bool + DispatchDOMEvent(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + DOMEventTargetHelper* aTarget, bool aIsMainThread) + { + nsCOMPtr<nsIGlobalObject> parent = do_QueryInterface(aTarget->GetParentObject()); + + // For some workers without window, parent is null and we try to find it + // from the JS Context. + if (!parent) { + JS::Rooted<JSObject*> globalObject(aCx, JS::CurrentGlobalOrNull(aCx)); + if (NS_WARN_IF(!globalObject)) { + return false; + } + + parent = xpc::NativeGlobal(globalObject); + if (NS_WARN_IF(!parent)) { + return false; + } + } + + MOZ_ASSERT(parent); + + JS::Rooted<JS::Value> messageData(aCx); + ErrorResult rv; + + UniquePtr<AbstractTimelineMarker> start; + UniquePtr<AbstractTimelineMarker> end; + RefPtr<TimelineConsumers> timelines = TimelineConsumers::Get(); + bool isTimelineRecording = timelines && !timelines->IsEmpty(); + + if (isTimelineRecording) { + start = MakeUnique<WorkerTimelineMarker>(aIsMainThread + ? ProfileTimelineWorkerOperationType::DeserializeDataOnMainThread + : ProfileTimelineWorkerOperationType::DeserializeDataOffMainThread, + MarkerTracingType::START); + } + + Read(parent, aCx, &messageData, rv); + + if (isTimelineRecording) { + end = MakeUnique<WorkerTimelineMarker>(aIsMainThread + ? ProfileTimelineWorkerOperationType::DeserializeDataOnMainThread + : ProfileTimelineWorkerOperationType::DeserializeDataOffMainThread, + MarkerTracingType::END); + timelines->AddMarkerForAllObservedDocShells(start); + timelines->AddMarkerForAllObservedDocShells(end); + } + + if (NS_WARN_IF(rv.Failed())) { + xpc::Throw(aCx, rv.StealNSResult()); + return false; + } + + Sequence<OwningNonNull<MessagePort>> ports; + if (!TakeTransferredPortsAsSequence(ports)) { + return false; + } + + nsCOMPtr<nsIDOMEvent> domEvent; + RefPtr<ExtendableMessageEvent> extendableEvent; + // For messages dispatched to service worker, use ExtendableMessageEvent + // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#extendablemessage-event-section + if (mEventSource) { + RefPtr<ServiceWorkerClient> client = + new ServiceWorkerWindowClient(aTarget, *mEventSource); + + RootedDictionary<ExtendableMessageEventInit> init(aCx); + + init.mBubbles = false; + init.mCancelable = false; + + init.mData = messageData; + init.mPorts = ports; + init.mSource.SetValue().SetAsClient() = client; + + ErrorResult rv; + extendableEvent = ExtendableMessageEvent::Constructor( + aTarget, NS_LITERAL_STRING("message"), init, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + return false; + } + + domEvent = do_QueryObject(extendableEvent); + } else { + RefPtr<MessageEvent> event = new MessageEvent(aTarget, nullptr, nullptr); + event->InitMessageEvent(nullptr, + NS_LITERAL_STRING("message"), + false /* non-bubbling */, + false /* cancelable */, + messageData, + EmptyString(), + EmptyString(), + nullptr, + ports); + domEvent = do_QueryObject(event); + } + + domEvent->SetTrusted(true); + + nsEventStatus dummy = nsEventStatus_eIgnore; + aTarget->DispatchDOMEvent(nullptr, domEvent, nullptr, &dummy); + + if (extendableEvent && mHandler) { + RefPtr<Promise> waitUntilPromise = extendableEvent->GetPromise(); + if (!waitUntilPromise) { + waitUntilPromise = Promise::Resolve(parent, aCx, + JS::UndefinedHandleValue, rv); + MOZ_RELEASE_ASSERT(!rv.Failed()); + } + + MOZ_ASSERT(waitUntilPromise); + + waitUntilPromise->AppendNativeHandler(mHandler); + } + + return true; + } + +private: + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + if (mBehavior == ParentThreadUnchangedBusyCount) { + // Don't fire this event if the JS object has been disconnected from the + // private object. + if (!aWorkerPrivate->IsAcceptingEvents()) { + return true; + } + + if (aWorkerPrivate->IsFrozen() || + aWorkerPrivate->IsParentWindowPaused()) { + MOZ_ASSERT(!IsDebuggerRunnable()); + aWorkerPrivate->QueueRunnable(this); + return true; + } + + aWorkerPrivate->AssertInnerWindowIsCorrect(); + + return DispatchDOMEvent(aCx, aWorkerPrivate, aWorkerPrivate, + !aWorkerPrivate->GetParent()); + } + + MOZ_ASSERT(aWorkerPrivate == GetWorkerPrivateFromContext(aCx)); + + return DispatchDOMEvent(aCx, aWorkerPrivate, aWorkerPrivate->GlobalScope(), + false); + } +}; + +class DebuggerMessageEventRunnable : public WorkerDebuggerRunnable { + nsString mMessage; + +public: + DebuggerMessageEventRunnable(WorkerPrivate* aWorkerPrivate, + const nsAString& aMessage) + : WorkerDebuggerRunnable(aWorkerPrivate), + mMessage(aMessage) + { + } + +private: + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + WorkerDebuggerGlobalScope* globalScope = aWorkerPrivate->DebuggerGlobalScope(); + MOZ_ASSERT(globalScope); + + JS::Rooted<JSString*> message(aCx, JS_NewUCStringCopyN(aCx, mMessage.get(), + mMessage.Length())); + if (!message) { + return false; + } + JS::Rooted<JS::Value> data(aCx, JS::StringValue(message)); + + RefPtr<MessageEvent> event = new MessageEvent(globalScope, nullptr, + nullptr); + event->InitMessageEvent(nullptr, + NS_LITERAL_STRING("message"), + false, // canBubble + true, // cancelable + data, + EmptyString(), + EmptyString(), + nullptr, + Sequence<OwningNonNull<MessagePort>>()); + event->SetTrusted(true); + + nsCOMPtr<nsIDOMEvent> domEvent = do_QueryObject(event); + nsEventStatus status = nsEventStatus_eIgnore; + globalScope->DispatchDOMEvent(nullptr, domEvent, nullptr, &status); + return true; + } +}; + +class NotifyRunnable final : public WorkerControlRunnable +{ + Status mStatus; + +public: + NotifyRunnable(WorkerPrivate* aWorkerPrivate, Status aStatus) + : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), + mStatus(aStatus) + { + MOZ_ASSERT(aStatus == Closing || aStatus == Terminating || + aStatus == Canceling || aStatus == Killing); + } + +private: + virtual bool + PreDispatch(WorkerPrivate* aWorkerPrivate) override + { + aWorkerPrivate->AssertIsOnParentThread(); + return aWorkerPrivate->ModifyBusyCount(true); + } + + virtual void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override + { + aWorkerPrivate->AssertIsOnParentThread(); + if (!aDispatchResult) { + // We couldn't dispatch to the worker, which means it's already dead. + // Undo the busy count modification. + aWorkerPrivate->ModifyBusyCount(false); + } + } + + virtual void + PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aRunResult) + override + { + aWorkerPrivate->ModifyBusyCountFromWorker(false); + return; + } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + bool ok = aWorkerPrivate->NotifyInternal(aCx, mStatus); + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + return ok; + } +}; + +class CloseRunnable final : public WorkerControlRunnable +{ +public: + explicit CloseRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerControlRunnable(aWorkerPrivate, ParentThreadUnchangedBusyCount) + { } + +private: + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + return aWorkerPrivate->Close(); + } +}; + +class FreezeRunnable final : public WorkerControlRunnable +{ +public: + explicit FreezeRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) + { } + +private: + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + return aWorkerPrivate->FreezeInternal(); + } +}; + +class ThawRunnable final : public WorkerControlRunnable +{ +public: + explicit ThawRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) + { } + +private: + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + return aWorkerPrivate->ThawInternal(); + } +}; + +class ReportErrorToConsoleRunnable final : public WorkerRunnable +{ + const char* mMessage; + +public: + // aWorkerPrivate is the worker thread we're on (or the main thread, if null) + static void + Report(WorkerPrivate* aWorkerPrivate, const char* aMessage) + { + if (aWorkerPrivate) { + aWorkerPrivate->AssertIsOnWorkerThread(); + } else { + AssertIsOnMainThread(); + } + + // Now fire a runnable to do the same on the parent's thread if we can. + if (aWorkerPrivate) { + RefPtr<ReportErrorToConsoleRunnable> runnable = + new ReportErrorToConsoleRunnable(aWorkerPrivate, aMessage); + runnable->Dispatch(); + return; + } + + // Log a warning to the console. + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("DOM"), + nullptr, + nsContentUtils::eDOM_PROPERTIES, + aMessage); + } + +private: + ReportErrorToConsoleRunnable(WorkerPrivate* aWorkerPrivate, const char* aMessage) + : WorkerRunnable(aWorkerPrivate, ParentThreadUnchangedBusyCount), + mMessage(aMessage) + { } + + virtual void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override + { + aWorkerPrivate->AssertIsOnWorkerThread(); + + // Dispatch may fail if the worker was canceled, no need to report that as + // an error, so don't call base class PostDispatch. + } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + WorkerPrivate* parent = aWorkerPrivate->GetParent(); + MOZ_ASSERT_IF(!parent, NS_IsMainThread()); + Report(parent, mMessage); + return true; + } +}; + +class ReportErrorRunnable final : public WorkerRunnable +{ + nsString mMessage; + nsString mFilename; + nsString mLine; + uint32_t mLineNumber; + uint32_t mColumnNumber; + uint32_t mFlags; + uint32_t mErrorNumber; + JSExnType mExnType; + bool mMutedError; + +public: + // aWorkerPrivate is the worker thread we're on (or the main thread, if null) + // aTarget is the worker object that we are going to fire an error at + // (if any). + static void + ReportError(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + bool aFireAtScope, WorkerPrivate* aTarget, + const nsString& aMessage, const nsString& aFilename, + const nsString& aLine, uint32_t aLineNumber, + uint32_t aColumnNumber, uint32_t aFlags, + uint32_t aErrorNumber, JSExnType aExnType, + bool aMutedError, uint64_t aInnerWindowId, + JS::Handle<JS::Value> aException = JS::NullHandleValue) + { + if (aWorkerPrivate) { + aWorkerPrivate->AssertIsOnWorkerThread(); + } else { + AssertIsOnMainThread(); + } + + // We should not fire error events for warnings but instead make sure that + // they show up in the error console. + if (!JSREPORT_IS_WARNING(aFlags)) { + // First fire an ErrorEvent at the worker. + RootedDictionary<ErrorEventInit> init(aCx); + + if (aMutedError) { + init.mMessage.AssignLiteral("Script error."); + } else { + init.mMessage = aMessage; + init.mFilename = aFilename; + init.mLineno = aLineNumber; + init.mError = aException; + } + + init.mCancelable = true; + init.mBubbles = false; + + if (aTarget) { + RefPtr<ErrorEvent> event = + ErrorEvent::Constructor(aTarget, NS_LITERAL_STRING("error"), init); + event->SetTrusted(true); + + nsEventStatus status = nsEventStatus_eIgnore; + aTarget->DispatchDOMEvent(nullptr, event, nullptr, &status); + + if (status == nsEventStatus_eConsumeNoDefault) { + return; + } + } + + // Now fire an event at the global object, but don't do that if the error + // code is too much recursion and this is the same script threw the error. + // XXXbz the interaction of this with worker errors seems kinda broken. + // An overrecursion in the debugger or debugger sandbox will get turned + // into an error event on our parent worker! + // https://bugzilla.mozilla.org/show_bug.cgi?id=1271441 tracks making this + // better. + if (aFireAtScope && (aTarget || aErrorNumber != JSMSG_OVER_RECURSED)) { + JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx)); + NS_ASSERTION(global, "This should never be null!"); + + nsEventStatus status = nsEventStatus_eIgnore; + nsIScriptGlobalObject* sgo; + + if (aWorkerPrivate) { + WorkerGlobalScope* globalScope = nullptr; + UNWRAP_OBJECT(WorkerGlobalScope, &global, globalScope); + + if (!globalScope) { + WorkerDebuggerGlobalScope* globalScope = nullptr; + UNWRAP_OBJECT(WorkerDebuggerGlobalScope, &global, globalScope); + + MOZ_ASSERT_IF(globalScope, globalScope->GetWrapperPreserveColor() == global); + if (globalScope || IsDebuggerSandbox(global)) { + aWorkerPrivate->ReportErrorToDebugger(aFilename, aLineNumber, + aMessage); + return; + } + + MOZ_ASSERT(SimpleGlobalObject::SimpleGlobalType(global) == + SimpleGlobalObject::GlobalType::BindingDetail); + // XXXbz We should really log this to console, but unwinding out of + // this stuff without ending up firing any events is ... hard. Just + // return for now. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1271441 tracks + // making this better. + return; + } + + MOZ_ASSERT(globalScope->GetWrapperPreserveColor() == global); + nsIDOMEventTarget* target = static_cast<nsIDOMEventTarget*>(globalScope); + + RefPtr<ErrorEvent> event = + ErrorEvent::Constructor(aTarget, NS_LITERAL_STRING("error"), init); + event->SetTrusted(true); + + if (NS_FAILED(EventDispatcher::DispatchDOMEvent(target, nullptr, + event, nullptr, + &status))) { + NS_WARNING("Failed to dispatch worker thread error event!"); + status = nsEventStatus_eIgnore; + } + } + else if ((sgo = nsJSUtils::GetStaticScriptGlobal(global))) { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_FAILED(sgo->HandleScriptError(init, &status))) { + NS_WARNING("Failed to dispatch main thread error event!"); + status = nsEventStatus_eIgnore; + } + } + + // Was preventDefault() called? + if (status == nsEventStatus_eConsumeNoDefault) { + return; + } + } + } + + // Now fire a runnable to do the same on the parent's thread if we can. + if (aWorkerPrivate) { + RefPtr<ReportErrorRunnable> runnable = + new ReportErrorRunnable(aWorkerPrivate, aMessage, aFilename, aLine, + aLineNumber, aColumnNumber, aFlags, + aErrorNumber, aExnType, aMutedError); + runnable->Dispatch(); + return; + } + + // Otherwise log an error to the error console. + LogErrorToConsole(aMessage, aFilename, aLine, aLineNumber, aColumnNumber, + aFlags, aInnerWindowId); + } + +private: + ReportErrorRunnable(WorkerPrivate* aWorkerPrivate, const nsString& aMessage, + const nsString& aFilename, const nsString& aLine, + uint32_t aLineNumber, uint32_t aColumnNumber, + uint32_t aFlags, uint32_t aErrorNumber, + JSExnType aExnType, bool aMutedError) + : WorkerRunnable(aWorkerPrivate, ParentThreadUnchangedBusyCount), + mMessage(aMessage), mFilename(aFilename), mLine(aLine), + mLineNumber(aLineNumber), mColumnNumber(aColumnNumber), mFlags(aFlags), + mErrorNumber(aErrorNumber), mExnType(aExnType), mMutedError(aMutedError) + { } + + virtual void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override + { + aWorkerPrivate->AssertIsOnWorkerThread(); + + // Dispatch may fail if the worker was canceled, no need to report that as + // an error, so don't call base class PostDispatch. + } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + JS::Rooted<JSObject*> target(aCx, aWorkerPrivate->GetWrapper()); + + uint64_t innerWindowId; + bool fireAtScope = true; + + bool workerIsAcceptingEvents = aWorkerPrivate->IsAcceptingEvents(); + + WorkerPrivate* parent = aWorkerPrivate->GetParent(); + if (parent) { + innerWindowId = 0; + } + else { + AssertIsOnMainThread(); + + if (aWorkerPrivate->IsFrozen() || + aWorkerPrivate->IsParentWindowPaused()) { + MOZ_ASSERT(!IsDebuggerRunnable()); + aWorkerPrivate->QueueRunnable(this); + return true; + } + + if (aWorkerPrivate->IsSharedWorker()) { + aWorkerPrivate->BroadcastErrorToSharedWorkers(aCx, mMessage, mFilename, + mLine, mLineNumber, + mColumnNumber, mFlags); + return true; + } + + // Service workers do not have a main thread parent global, so normal + // worker error reporting will crash. Instead, pass the error to + // the ServiceWorkerManager to report on any controlled documents. + if (aWorkerPrivate->IsServiceWorker()) { + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->HandleError(aCx, aWorkerPrivate->GetPrincipal(), + aWorkerPrivate->WorkerName(), + aWorkerPrivate->ScriptURL(), + mMessage, + mFilename, mLine, mLineNumber, + mColumnNumber, mFlags, mExnType); + } + return true; + } + + // The innerWindowId is only required if we are going to ReportError + // below, which is gated on this condition. The inner window correctness + // check is only going to succeed when the worker is accepting events. + if (workerIsAcceptingEvents) { + aWorkerPrivate->AssertInnerWindowIsCorrect(); + innerWindowId = aWorkerPrivate->WindowID(); + } + } + + // Don't fire this event if the JS object has been disconnected from the + // private object. + if (!workerIsAcceptingEvents) { + return true; + } + + ReportError(aCx, parent, fireAtScope, aWorkerPrivate, mMessage, + mFilename, mLine, mLineNumber, mColumnNumber, mFlags, + mErrorNumber, mExnType, mMutedError, innerWindowId); + return true; + } +}; + +class TimerRunnable final : public WorkerRunnable, + public nsITimerCallback +{ +public: + NS_DECL_ISUPPORTS_INHERITED + + explicit TimerRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) + { } + +private: + ~TimerRunnable() {} + + virtual bool + PreDispatch(WorkerPrivate* aWorkerPrivate) override + { + // Silence bad assertions. + return true; + } + + virtual void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override + { + // Silence bad assertions. + } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + return aWorkerPrivate->RunExpiredTimeouts(aCx); + } + + NS_IMETHOD + Notify(nsITimer* aTimer) override + { + return Run(); + } +}; + +NS_IMPL_ISUPPORTS_INHERITED(TimerRunnable, WorkerRunnable, nsITimerCallback) + +class DebuggerImmediateRunnable : public WorkerRunnable +{ + RefPtr<dom::Function> mHandler; + +public: + explicit DebuggerImmediateRunnable(WorkerPrivate* aWorkerPrivate, + dom::Function& aHandler) + : WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), + mHandler(&aHandler) + { } + +private: + virtual bool + IsDebuggerRunnable() const override + { + return true; + } + + virtual bool + PreDispatch(WorkerPrivate* aWorkerPrivate) override + { + // Silence bad assertions. + return true; + } + + virtual void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override + { + // Silence bad assertions. + } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx)); + JS::Rooted<JS::Value> callable(aCx, JS::ObjectValue(*mHandler->Callable())); + JS::HandleValueArray args = JS::HandleValueArray::empty(); + JS::Rooted<JS::Value> rval(aCx); + if (!JS_CallFunctionValue(aCx, global, callable, args, &rval)) { + // Just return false; WorkerRunnable::Run will report the exception. + return false; + } + + return true; + } +}; + +void +DummyCallback(nsITimer* aTimer, void* aClosure) +{ + // Nothing! +} + +class UpdateContextOptionsRunnable final : public WorkerControlRunnable +{ + JS::ContextOptions mContextOptions; + +public: + UpdateContextOptionsRunnable(WorkerPrivate* aWorkerPrivate, + const JS::ContextOptions& aContextOptions) + : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), + mContextOptions(aContextOptions) + { } + +private: + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + aWorkerPrivate->UpdateContextOptionsInternal(aCx, mContextOptions); + return true; + } +}; + +class UpdatePreferenceRunnable final : public WorkerControlRunnable +{ + WorkerPreference mPref; + bool mValue; + +public: + UpdatePreferenceRunnable(WorkerPrivate* aWorkerPrivate, + WorkerPreference aPref, + bool aValue) + : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), + mPref(aPref), + mValue(aValue) + { } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + aWorkerPrivate->UpdatePreferenceInternal(mPref, mValue); + return true; + } +}; + +class UpdateLanguagesRunnable final : public WorkerRunnable +{ + nsTArray<nsString> mLanguages; + +public: + UpdateLanguagesRunnable(WorkerPrivate* aWorkerPrivate, + const nsTArray<nsString>& aLanguages) + : WorkerRunnable(aWorkerPrivate), + mLanguages(aLanguages) + { } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + aWorkerPrivate->UpdateLanguagesInternal(mLanguages); + return true; + } +}; + +class UpdateJSWorkerMemoryParameterRunnable final : + public WorkerControlRunnable +{ + uint32_t mValue; + JSGCParamKey mKey; + +public: + UpdateJSWorkerMemoryParameterRunnable(WorkerPrivate* aWorkerPrivate, + JSGCParamKey aKey, + uint32_t aValue) + : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), + mValue(aValue), mKey(aKey) + { } + +private: + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + aWorkerPrivate->UpdateJSWorkerMemoryParameterInternal(aCx, mKey, mValue); + return true; + } +}; + +#ifdef JS_GC_ZEAL +class UpdateGCZealRunnable final : public WorkerControlRunnable +{ + uint8_t mGCZeal; + uint32_t mFrequency; + +public: + UpdateGCZealRunnable(WorkerPrivate* aWorkerPrivate, + uint8_t aGCZeal, + uint32_t aFrequency) + : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), + mGCZeal(aGCZeal), mFrequency(aFrequency) + { } + +private: + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + aWorkerPrivate->UpdateGCZealInternal(aCx, mGCZeal, mFrequency); + return true; + } +}; +#endif + +class GarbageCollectRunnable final : public WorkerControlRunnable +{ + bool mShrinking; + bool mCollectChildren; + +public: + GarbageCollectRunnable(WorkerPrivate* aWorkerPrivate, bool aShrinking, + bool aCollectChildren) + : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), + mShrinking(aShrinking), mCollectChildren(aCollectChildren) + { } + +private: + virtual bool + PreDispatch(WorkerPrivate* aWorkerPrivate) override + { + // Silence bad assertions, this can be dispatched from either the main + // thread or the timer thread.. + return true; + } + + virtual void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override + { + // Silence bad assertions, this can be dispatched from either the main + // thread or the timer thread.. + } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + aWorkerPrivate->GarbageCollectInternal(aCx, mShrinking, mCollectChildren); + return true; + } +}; + +class CycleCollectRunnable : public WorkerControlRunnable +{ + bool mCollectChildren; + +public: + CycleCollectRunnable(WorkerPrivate* aWorkerPrivate, bool aCollectChildren) + : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), + mCollectChildren(aCollectChildren) + { } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) + { + aWorkerPrivate->CycleCollectInternal(mCollectChildren); + return true; + } +}; + +class OfflineStatusChangeRunnable : public WorkerRunnable +{ +public: + OfflineStatusChangeRunnable(WorkerPrivate* aWorkerPrivate, bool aIsOffline) + : WorkerRunnable(aWorkerPrivate), + mIsOffline(aIsOffline) + { + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) + { + aWorkerPrivate->OfflineStatusChangeEventInternal(mIsOffline); + return true; + } + +private: + bool mIsOffline; +}; + +class MemoryPressureRunnable : public WorkerControlRunnable +{ +public: + explicit MemoryPressureRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) + {} + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) + { + aWorkerPrivate->MemoryPressureInternal(); + return true; + } +}; + +#ifdef DEBUG +static bool +StartsWithExplicit(nsACString& s) +{ + return StringBeginsWith(s, NS_LITERAL_CSTRING("explicit/")); +} +#endif + +class MessagePortRunnable final : public WorkerRunnable +{ + MessagePortIdentifier mPortIdentifier; + +public: + MessagePortRunnable(WorkerPrivate* aWorkerPrivate, MessagePort* aPort) + : WorkerRunnable(aWorkerPrivate) + { + MOZ_ASSERT(aPort); + // In order to move the port from one thread to another one, we have to + // close and disentangle it. The output will be a MessagePortIdentifier that + // will be used to recreate a new MessagePort on the other thread. + aPort->CloneAndDisentangle(mPortIdentifier); + } + +private: + ~MessagePortRunnable() + { } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + return aWorkerPrivate->ConnectMessagePort(aCx, mPortIdentifier); + } + + nsresult + Cancel() override + { + MessagePort::ForceClose(mPortIdentifier); + return WorkerRunnable::Cancel(); + } +}; + +class DummyRunnable final + : public WorkerRunnable +{ +public: + explicit + DummyRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) + { + aWorkerPrivate->AssertIsOnWorkerThread(); + } + +private: + ~DummyRunnable() + { + mWorkerPrivate->AssertIsOnWorkerThread(); + } + + virtual bool + PreDispatch(WorkerPrivate* aWorkerPrivate) override + { + MOZ_ASSERT_UNREACHABLE("Should never call Dispatch on this!"); + return true; + } + + virtual void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override + { + MOZ_ASSERT_UNREACHABLE("Should never call Dispatch on this!"); + } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + // Do nothing. + return true; + } +}; + +PRThread* +PRThreadFromThread(nsIThread* aThread) +{ + MOZ_ASSERT(aThread); + + PRThread* result; + MOZ_ALWAYS_SUCCEEDS(aThread->GetPRThread(&result)); + MOZ_ASSERT(result); + + return result; +} + +class SimpleWorkerHolder final : public WorkerHolder +{ +public: + virtual bool Notify(Status aStatus) { return true; } +}; + +} /* anonymous namespace */ + +NS_IMPL_ISUPPORTS_INHERITED0(MainThreadReleaseRunnable, Runnable) + +NS_IMPL_ISUPPORTS_INHERITED0(TopLevelWorkerFinishedRunnable, Runnable) + +TimerThreadEventTarget::TimerThreadEventTarget(WorkerPrivate* aWorkerPrivate, + WorkerRunnable* aWorkerRunnable) + : mWorkerPrivate(aWorkerPrivate), mWorkerRunnable(aWorkerRunnable) +{ + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aWorkerRunnable); +} + +TimerThreadEventTarget::~TimerThreadEventTarget() +{ +} + +NS_IMETHODIMP +TimerThreadEventTarget::DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) +{ + nsCOMPtr<nsIRunnable> runnable(aRunnable); + return Dispatch(runnable.forget(), aFlags); +} + +NS_IMETHODIMP +TimerThreadEventTarget::Dispatch(already_AddRefed<nsIRunnable> aRunnable, uint32_t aFlags) +{ + // This should only happen on the timer thread. + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(aFlags == nsIEventTarget::DISPATCH_NORMAL); + + RefPtr<TimerThreadEventTarget> kungFuDeathGrip = this; + + // Run the runnable we're given now (should just call DummyCallback()), + // otherwise the timer thread will leak it... If we run this after + // dispatch running the event can race against resetting the timer. + nsCOMPtr<nsIRunnable> runnable(aRunnable); + runnable->Run(); + + // This can fail if we're racing to terminate or cancel, should be handled + // by the terminate or cancel code. + mWorkerRunnable->Dispatch(); + + return NS_OK; +} + +NS_IMETHODIMP +TimerThreadEventTarget::DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +TimerThreadEventTarget::IsOnCurrentThread(bool* aIsOnCurrentThread) +{ + MOZ_ASSERT(aIsOnCurrentThread); + + nsresult rv = mWorkerPrivate->IsOnCurrentThread(aIsOnCurrentThread); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(TimerThreadEventTarget, nsIEventTarget) + +WorkerLoadInfo::WorkerLoadInfo() + : mWindowID(UINT64_MAX) + , mServiceWorkerID(0) + , mReferrerPolicy(net::RP_Default) + , mFromWindow(false) + , mEvalAllowed(false) + , mReportCSPViolations(false) + , mXHRParamsAllowed(false) + , mPrincipalIsSystem(false) + , mStorageAllowed(false) + , mServiceWorkersTestingInWindow(false) +{ + MOZ_COUNT_CTOR(WorkerLoadInfo); +} + +WorkerLoadInfo::~WorkerLoadInfo() +{ + MOZ_COUNT_DTOR(WorkerLoadInfo); +} + +void +WorkerLoadInfo::StealFrom(WorkerLoadInfo& aOther) +{ + MOZ_ASSERT(!mBaseURI); + aOther.mBaseURI.swap(mBaseURI); + + MOZ_ASSERT(!mResolvedScriptURI); + aOther.mResolvedScriptURI.swap(mResolvedScriptURI); + + MOZ_ASSERT(!mPrincipal); + aOther.mPrincipal.swap(mPrincipal); + + MOZ_ASSERT(!mScriptContext); + aOther.mScriptContext.swap(mScriptContext); + + MOZ_ASSERT(!mWindow); + aOther.mWindow.swap(mWindow); + + MOZ_ASSERT(!mCSP); + aOther.mCSP.swap(mCSP); + + MOZ_ASSERT(!mChannel); + aOther.mChannel.swap(mChannel); + + MOZ_ASSERT(!mLoadGroup); + aOther.mLoadGroup.swap(mLoadGroup); + + MOZ_ASSERT(!mLoadFailedAsyncRunnable); + aOther.mLoadFailedAsyncRunnable.swap(mLoadFailedAsyncRunnable); + + MOZ_ASSERT(!mInterfaceRequestor); + aOther.mInterfaceRequestor.swap(mInterfaceRequestor); + + MOZ_ASSERT(!mPrincipalInfo); + mPrincipalInfo = aOther.mPrincipalInfo.forget(); + + mDomain = aOther.mDomain; + mServiceWorkerCacheName = aOther.mServiceWorkerCacheName; + mWindowID = aOther.mWindowID; + mServiceWorkerID = aOther.mServiceWorkerID; + mReferrerPolicy = aOther.mReferrerPolicy; + mFromWindow = aOther.mFromWindow; + mEvalAllowed = aOther.mEvalAllowed; + mReportCSPViolations = aOther.mReportCSPViolations; + mXHRParamsAllowed = aOther.mXHRParamsAllowed; + mPrincipalIsSystem = aOther.mPrincipalIsSystem; + mStorageAllowed = aOther.mStorageAllowed; + mServiceWorkersTestingInWindow = aOther.mServiceWorkersTestingInWindow; + mOriginAttributes = aOther.mOriginAttributes; +} + +template <class Derived> +class WorkerPrivateParent<Derived>::EventTarget final + : public nsIEventTarget +{ + // This mutex protects mWorkerPrivate and must be acquired *before* the + // WorkerPrivate's mutex whenever they must both be held. + mozilla::Mutex mMutex; + WorkerPrivate* mWorkerPrivate; + nsIEventTarget* mWeakNestedEventTarget; + nsCOMPtr<nsIEventTarget> mNestedEventTarget; + +public: + explicit EventTarget(WorkerPrivate* aWorkerPrivate) + : mMutex("WorkerPrivateParent::EventTarget::mMutex"), + mWorkerPrivate(aWorkerPrivate), mWeakNestedEventTarget(nullptr) + { + MOZ_ASSERT(aWorkerPrivate); + } + + EventTarget(WorkerPrivate* aWorkerPrivate, nsIEventTarget* aNestedEventTarget) + : mMutex("WorkerPrivateParent::EventTarget::mMutex"), + mWorkerPrivate(aWorkerPrivate), mWeakNestedEventTarget(aNestedEventTarget), + mNestedEventTarget(aNestedEventTarget) + { + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aNestedEventTarget); + } + + void + Disable() + { + nsCOMPtr<nsIEventTarget> nestedEventTarget; + { + MutexAutoLock lock(mMutex); + + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate = nullptr; + mNestedEventTarget.swap(nestedEventTarget); + } + } + + nsIEventTarget* + GetWeakNestedEventTarget() const + { + MOZ_ASSERT(mWeakNestedEventTarget); + return mWeakNestedEventTarget; + } + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIEVENTTARGET + +private: + ~EventTarget() + { } +}; + +WorkerLoadInfo:: +InterfaceRequestor::InterfaceRequestor(nsIPrincipal* aPrincipal, + nsILoadGroup* aLoadGroup) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + // Look for an existing LoadContext. This is optional and it's ok if + // we don't find one. + nsCOMPtr<nsILoadContext> baseContext; + if (aLoadGroup) { + nsCOMPtr<nsIInterfaceRequestor> callbacks; + aLoadGroup->GetNotificationCallbacks(getter_AddRefs(callbacks)); + if (callbacks) { + callbacks->GetInterface(NS_GET_IID(nsILoadContext), + getter_AddRefs(baseContext)); + } + mOuterRequestor = callbacks; + } + + mLoadContext = new LoadContext(aPrincipal, baseContext); +} + +void +WorkerLoadInfo:: +InterfaceRequestor::MaybeAddTabChild(nsILoadGroup* aLoadGroup) +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (!aLoadGroup) { + return; + } + + nsCOMPtr<nsIInterfaceRequestor> callbacks; + aLoadGroup->GetNotificationCallbacks(getter_AddRefs(callbacks)); + if (!callbacks) { + return; + } + + nsCOMPtr<nsITabChild> tabChild; + callbacks->GetInterface(NS_GET_IID(nsITabChild), getter_AddRefs(tabChild)); + if (!tabChild) { + return; + } + + // Use weak references to the tab child. Holding a strong reference will + // not prevent an ActorDestroy() from being called on the TabChild. + // Therefore, we should let the TabChild destroy itself as soon as possible. + mTabChildList.AppendElement(do_GetWeakReference(tabChild)); +} + +NS_IMETHODIMP +WorkerLoadInfo:: +InterfaceRequestor::GetInterface(const nsIID& aIID, void** aSink) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mLoadContext); + + if (aIID.Equals(NS_GET_IID(nsILoadContext))) { + nsCOMPtr<nsILoadContext> ref = mLoadContext; + ref.forget(aSink); + return NS_OK; + } + + // If we still have an active nsITabChild, then return it. Its possible, + // though, that all of the TabChild objects have been destroyed. In that + // case we return NS_NOINTERFACE. + if (aIID.Equals(NS_GET_IID(nsITabChild))) { + nsCOMPtr<nsITabChild> tabChild = GetAnyLiveTabChild(); + if (!tabChild) { + return NS_NOINTERFACE; + } + tabChild.forget(aSink); + return NS_OK; + } + + if (aIID.Equals(NS_GET_IID(nsINetworkInterceptController)) && + mOuterRequestor) { + // If asked for the network intercept controller, ask the outer requestor, + // which could be the docshell. + return mOuterRequestor->GetInterface(aIID, aSink); + } + + return NS_NOINTERFACE; +} + +already_AddRefed<nsITabChild> +WorkerLoadInfo:: +InterfaceRequestor::GetAnyLiveTabChild() +{ + MOZ_ASSERT(NS_IsMainThread()); + + // Search our list of known TabChild objects for one that still exists. + while (!mTabChildList.IsEmpty()) { + nsCOMPtr<nsITabChild> tabChild = + do_QueryReferent(mTabChildList.LastElement()); + + // Does this tab child still exist? If so, return it. We are done. If the + // PBrowser actor is no longer useful, don't bother returning this tab. + if (tabChild && !static_cast<TabChild*>(tabChild.get())->IsDestroyed()) { + return tabChild.forget(); + } + + // Otherwise remove the stale weak reference and check the next one + mTabChildList.RemoveElementAt(mTabChildList.Length() - 1); + } + + return nullptr; +} + +NS_IMPL_ADDREF(WorkerLoadInfo::InterfaceRequestor) +NS_IMPL_RELEASE(WorkerLoadInfo::InterfaceRequestor) +NS_IMPL_QUERY_INTERFACE(WorkerLoadInfo::InterfaceRequestor, nsIInterfaceRequestor) + +struct WorkerPrivate::TimeoutInfo +{ + TimeoutInfo() + : mId(0), mIsInterval(false), mCanceled(false) + { + MOZ_COUNT_CTOR(mozilla::dom::workers::WorkerPrivate::TimeoutInfo); + } + + ~TimeoutInfo() + { + MOZ_COUNT_DTOR(mozilla::dom::workers::WorkerPrivate::TimeoutInfo); + } + + bool operator==(const TimeoutInfo& aOther) + { + return mTargetTime == aOther.mTargetTime; + } + + bool operator<(const TimeoutInfo& aOther) + { + return mTargetTime < aOther.mTargetTime; + } + + nsCOMPtr<nsIScriptTimeoutHandler> mHandler; + mozilla::TimeStamp mTargetTime; + mozilla::TimeDuration mInterval; + int32_t mId; + bool mIsInterval; + bool mCanceled; +}; + +class WorkerJSContextStats final : public JS::RuntimeStats +{ + const nsCString mRtPath; + +public: + explicit WorkerJSContextStats(const nsACString& aRtPath) + : JS::RuntimeStats(JsWorkerMallocSizeOf), mRtPath(aRtPath) + { } + + ~WorkerJSContextStats() + { + for (size_t i = 0; i != zoneStatsVector.length(); i++) { + delete static_cast<xpc::ZoneStatsExtras*>(zoneStatsVector[i].extra); + } + + for (size_t i = 0; i != compartmentStatsVector.length(); i++) { + delete static_cast<xpc::CompartmentStatsExtras*>(compartmentStatsVector[i].extra); + } + } + + const nsCString& Path() const + { + return mRtPath; + } + + virtual void + initExtraZoneStats(JS::Zone* aZone, + JS::ZoneStats* aZoneStats) + override + { + MOZ_ASSERT(!aZoneStats->extra); + + // ReportJSRuntimeExplicitTreeStats expects that + // aZoneStats->extra is a xpc::ZoneStatsExtras pointer. + xpc::ZoneStatsExtras* extras = new xpc::ZoneStatsExtras; + extras->pathPrefix = mRtPath; + extras->pathPrefix += nsPrintfCString("zone(0x%p)/", (void *)aZone); + + MOZ_ASSERT(StartsWithExplicit(extras->pathPrefix)); + + aZoneStats->extra = extras; + } + + virtual void + initExtraCompartmentStats(JSCompartment* aCompartment, + JS::CompartmentStats* aCompartmentStats) + override + { + MOZ_ASSERT(!aCompartmentStats->extra); + + // ReportJSRuntimeExplicitTreeStats expects that + // aCompartmentStats->extra is a xpc::CompartmentStatsExtras pointer. + xpc::CompartmentStatsExtras* extras = new xpc::CompartmentStatsExtras; + + // This is the |jsPathPrefix|. Each worker has exactly two compartments: + // one for atoms, and one for everything else. + extras->jsPathPrefix.Assign(mRtPath); + extras->jsPathPrefix += nsPrintfCString("zone(0x%p)/", + (void *)js::GetCompartmentZone(aCompartment)); + extras->jsPathPrefix += js::IsAtomsCompartment(aCompartment) + ? NS_LITERAL_CSTRING("compartment(web-worker-atoms)/") + : NS_LITERAL_CSTRING("compartment(web-worker)/"); + + // This should never be used when reporting with workers (hence the "?!"). + extras->domPathPrefix.AssignLiteral("explicit/workers/?!/"); + + MOZ_ASSERT(StartsWithExplicit(extras->jsPathPrefix)); + MOZ_ASSERT(StartsWithExplicit(extras->domPathPrefix)); + + extras->location = nullptr; + + aCompartmentStats->extra = extras; + } +}; + +class WorkerPrivate::MemoryReporter final : public nsIMemoryReporter +{ + NS_DECL_THREADSAFE_ISUPPORTS + + friend class WorkerPrivate; + + SharedMutex mMutex; + WorkerPrivate* mWorkerPrivate; + bool mAlreadyMappedToAddon; + +public: + explicit MemoryReporter(WorkerPrivate* aWorkerPrivate) + : mMutex(aWorkerPrivate->mMutex), mWorkerPrivate(aWorkerPrivate), + mAlreadyMappedToAddon(false) + { + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + NS_IMETHOD + CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) override; + +private: + class FinishCollectRunnable; + + class CollectReportsRunnable final : public MainThreadWorkerControlRunnable + { + RefPtr<FinishCollectRunnable> mFinishCollectRunnable; + const bool mAnonymize; + + public: + CollectReportsRunnable( + WorkerPrivate* aWorkerPrivate, + nsIHandleReportCallback* aHandleReport, + nsISupports* aHandlerData, + bool aAnonymize, + const nsACString& aPath); + + private: + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override; + + ~CollectReportsRunnable() + { + if (NS_IsMainThread()) { + mFinishCollectRunnable->Run(); + return; + } + + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + MOZ_ALWAYS_SUCCEEDS( + workerPrivate->DispatchToMainThread(mFinishCollectRunnable.forget())); + } + }; + + class FinishCollectRunnable final : public Runnable + { + nsCOMPtr<nsIHandleReportCallback> mHandleReport; + nsCOMPtr<nsISupports> mHandlerData; + const bool mAnonymize; + bool mSuccess; + + public: + WorkerJSContextStats mCxStats; + + explicit FinishCollectRunnable( + nsIHandleReportCallback* aHandleReport, + nsISupports* aHandlerData, + bool aAnonymize, + const nsACString& aPath); + + NS_IMETHOD Run() override; + + void SetSuccess(bool success) + { + mSuccess = success; + } + + private: + ~FinishCollectRunnable() + { + // mHandleReport and mHandlerData are released on the main thread. + AssertIsOnMainThread(); + } + + FinishCollectRunnable(const FinishCollectRunnable&) = delete; + FinishCollectRunnable& operator=(const FinishCollectRunnable&) = delete; + FinishCollectRunnable& operator=(const FinishCollectRunnable&&) = delete; + }; + + ~MemoryReporter() + { + } + + void + Disable() + { + // Called from WorkerPrivate::DisableMemoryReporter. + mMutex.AssertCurrentThreadOwns(); + + NS_ASSERTION(mWorkerPrivate, "Disabled more than once!"); + mWorkerPrivate = nullptr; + } + + // Only call this from the main thread and under mMutex lock. + void + TryToMapAddon(nsACString &path); +}; + +NS_IMPL_ISUPPORTS(WorkerPrivate::MemoryReporter, nsIMemoryReporter) + +NS_IMETHODIMP +WorkerPrivate::MemoryReporter::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, + bool aAnonymize) +{ + AssertIsOnMainThread(); + + RefPtr<CollectReportsRunnable> runnable; + + { + MutexAutoLock lock(mMutex); + + if (!mWorkerPrivate) { + // This will effectively report 0 memory. + nsCOMPtr<nsIMemoryReporterManager> manager = + do_GetService("@mozilla.org/memory-reporter-manager;1"); + if (manager) { + manager->EndReport(); + } + return NS_OK; + } + + nsAutoCString path; + path.AppendLiteral("explicit/workers/workers("); + if (aAnonymize && !mWorkerPrivate->Domain().IsEmpty()) { + path.AppendLiteral("<anonymized-domain>)/worker(<anonymized-url>"); + } else { + nsAutoCString escapedDomain(mWorkerPrivate->Domain()); + if (escapedDomain.IsEmpty()) { + escapedDomain += "chrome"; + } else { + escapedDomain.ReplaceChar('/', '\\'); + } + path.Append(escapedDomain); + path.AppendLiteral(")/worker("); + NS_ConvertUTF16toUTF8 escapedURL(mWorkerPrivate->ScriptURL()); + escapedURL.ReplaceChar('/', '\\'); + path.Append(escapedURL); + } + path.AppendPrintf(", 0x%p)/", static_cast<void*>(mWorkerPrivate)); + + TryToMapAddon(path); + + runnable = + new CollectReportsRunnable(mWorkerPrivate, aHandleReport, aData, aAnonymize, path); + } + + if (!runnable->Dispatch()) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +void +WorkerPrivate::MemoryReporter::TryToMapAddon(nsACString &path) +{ + AssertIsOnMainThread(); + mMutex.AssertCurrentThreadOwns(); + + if (mAlreadyMappedToAddon || !mWorkerPrivate) { + return; + } + + nsCOMPtr<nsIURI> scriptURI; + if (NS_FAILED(NS_NewURI(getter_AddRefs(scriptURI), + mWorkerPrivate->ScriptURL()))) { + return; + } + + mAlreadyMappedToAddon = true; + + if (!XRE_IsParentProcess()) { + // Only try to access the service from the main process. + return; + } + + nsAutoCString addonId; + bool ok; + nsCOMPtr<amIAddonManager> addonManager = + do_GetService("@mozilla.org/addons/integration;1"); + + if (!addonManager || + NS_FAILED(addonManager->MapURIToAddonID(scriptURI, addonId, &ok)) || + !ok) { + return; + } + + static const size_t explicitLength = strlen("explicit/"); + addonId.Insert(NS_LITERAL_CSTRING("add-ons/"), 0); + addonId += "/"; + path.Insert(addonId, explicitLength); +} + +WorkerPrivate::MemoryReporter::CollectReportsRunnable::CollectReportsRunnable( + WorkerPrivate* aWorkerPrivate, + nsIHandleReportCallback* aHandleReport, + nsISupports* aHandlerData, + bool aAnonymize, + const nsACString& aPath) + : MainThreadWorkerControlRunnable(aWorkerPrivate), + mFinishCollectRunnable( + new FinishCollectRunnable(aHandleReport, aHandlerData, aAnonymize, aPath)), + mAnonymize(aAnonymize) +{ } + +bool +WorkerPrivate::MemoryReporter::CollectReportsRunnable::WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) +{ + aWorkerPrivate->AssertIsOnWorkerThread(); + + mFinishCollectRunnable->SetSuccess( + aWorkerPrivate->CollectRuntimeStats(&mFinishCollectRunnable->mCxStats, mAnonymize)); + + return true; +} + +WorkerPrivate::MemoryReporter::FinishCollectRunnable::FinishCollectRunnable( + nsIHandleReportCallback* aHandleReport, + nsISupports* aHandlerData, + bool aAnonymize, + const nsACString& aPath) + : mHandleReport(aHandleReport), + mHandlerData(aHandlerData), + mAnonymize(aAnonymize), + mSuccess(false), + mCxStats(aPath) +{ } + +NS_IMETHODIMP +WorkerPrivate::MemoryReporter::FinishCollectRunnable::Run() +{ + AssertIsOnMainThread(); + + nsCOMPtr<nsIMemoryReporterManager> manager = + do_GetService("@mozilla.org/memory-reporter-manager;1"); + + if (!manager) + return NS_OK; + + if (mSuccess) { + xpc::ReportJSRuntimeExplicitTreeStats(mCxStats, mCxStats.Path(), + mHandleReport, mHandlerData, + mAnonymize); + } + + manager->EndReport(); + + return NS_OK; +} + +WorkerPrivate::SyncLoopInfo::SyncLoopInfo(EventTarget* aEventTarget) +: mEventTarget(aEventTarget), mCompleted(false), mResult(false) +#ifdef DEBUG + , mHasRun(false) +#endif +{ +} + +template <class Derived> +nsIDocument* +WorkerPrivateParent<Derived>::GetDocument() const +{ + AssertIsOnMainThread(); + if (mLoadInfo.mWindow) { + return mLoadInfo.mWindow->GetExtantDoc(); + } + // if we don't have a document, we should query the document + // from the parent in case of a nested worker + WorkerPrivate* parent = mParent; + while (parent) { + if (parent->mLoadInfo.mWindow) { + return parent->mLoadInfo.mWindow->GetExtantDoc(); + } + parent = parent->GetParent(); + } + // couldn't query a document, give up and return nullptr + return nullptr; +} + + +// Can't use NS_IMPL_CYCLE_COLLECTION_CLASS(WorkerPrivateParent) because of the +// templates. +template <class Derived> +typename WorkerPrivateParent<Derived>::cycleCollection + WorkerPrivateParent<Derived>::_cycleCollectorGlobal = + WorkerPrivateParent<Derived>::cycleCollection(); + +template <class Derived> +WorkerPrivateParent<Derived>::WorkerPrivateParent( + WorkerPrivate* aParent, + const nsAString& aScriptURL, + bool aIsChromeWorker, + WorkerType aWorkerType, + const nsACString& aWorkerName, + WorkerLoadInfo& aLoadInfo) +: mMutex("WorkerPrivateParent Mutex"), + mCondVar(mMutex, "WorkerPrivateParent CondVar"), + mParent(aParent), mScriptURL(aScriptURL), + mWorkerName(aWorkerName), mLoadingWorkerScript(false), + mBusyCount(0), mParentWindowPausedDepth(0), mParentStatus(Pending), + mParentFrozen(false), mIsChromeWorker(aIsChromeWorker), + mMainThreadObjectsForgotten(false), mIsSecureContext(false), + mWorkerType(aWorkerType), + mCreationTimeStamp(TimeStamp::Now()), + mCreationTimeHighRes((double)PR_Now() / PR_USEC_PER_MSEC) +{ + MOZ_ASSERT_IF(!IsDedicatedWorker(), + !aWorkerName.IsVoid() && NS_IsMainThread()); + MOZ_ASSERT_IF(IsDedicatedWorker(), aWorkerName.IsEmpty()); + + if (aLoadInfo.mWindow) { + AssertIsOnMainThread(); + MOZ_ASSERT(aLoadInfo.mWindow->IsInnerWindow(), + "Should have inner window here!"); + BindToOwner(aLoadInfo.mWindow); + } + + mLoadInfo.StealFrom(aLoadInfo); + + if (aParent) { + aParent->AssertIsOnWorkerThread(); + + // Note that this copies our parent's secure context state into mJSSettings. + aParent->CopyJSSettings(mJSSettings); + + // And manually set our mIsSecureContext, though it's not really relevant to + // dedicated workers... + mIsSecureContext = aParent->IsSecureContext(); + MOZ_ASSERT_IF(mIsChromeWorker, mIsSecureContext); + + MOZ_ASSERT(IsDedicatedWorker()); + mNowBaseTimeStamp = aParent->NowBaseTimeStamp(); + mNowBaseTimeHighRes = aParent->NowBaseTime(); + + if (aParent->mParentFrozen) { + Freeze(nullptr); + } + } + else { + AssertIsOnMainThread(); + + RuntimeService::GetDefaultJSSettings(mJSSettings); + + // Our secure context state depends on the kind of worker we have. + if (UsesSystemPrincipal() || IsServiceWorker()) { + mIsSecureContext = true; + } else if (mLoadInfo.mWindow) { + // Shared and dedicated workers both inherit the loading window's secure + // context state. Shared workers then prevent windows with a different + // secure context state from attaching to them. + mIsSecureContext = mLoadInfo.mWindow->IsSecureContext(); + } else { + MOZ_ASSERT_UNREACHABLE("non-chrome worker that is not a service worker " + "that has no parent and no associated window"); + } + + if (mIsSecureContext) { + mJSSettings.chrome.compartmentOptions + .creationOptions().setSecureContext(true); + mJSSettings.content.compartmentOptions + .creationOptions().setSecureContext(true); + } + + if (IsDedicatedWorker() && mLoadInfo.mWindow && + mLoadInfo.mWindow->GetPerformance()) { + mNowBaseTimeStamp = mLoadInfo.mWindow->GetPerformance()->GetDOMTiming()-> + GetNavigationStartTimeStamp(); + mNowBaseTimeHighRes = + mLoadInfo.mWindow->GetPerformance()->GetDOMTiming()-> + GetNavigationStartHighRes(); + } else { + mNowBaseTimeStamp = CreationTimeStamp(); + mNowBaseTimeHighRes = CreationTime(); + } + + // Our parent can get suspended after it initiates the async creation + // of a new worker thread. In this case suspend the new worker as well. + if (mLoadInfo.mWindow && mLoadInfo.mWindow->IsSuspended()) { + ParentWindowPaused(); + } + + if (mLoadInfo.mWindow && mLoadInfo.mWindow->IsFrozen()) { + Freeze(mLoadInfo.mWindow); + } + } +} + +template <class Derived> +WorkerPrivateParent<Derived>::~WorkerPrivateParent() +{ + DropJSObjects(this); +} + +template <class Derived> +JSObject* +WorkerPrivateParent<Derived>::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + MOZ_ASSERT(!IsSharedWorker(), + "We should never wrap a WorkerPrivate for a SharedWorker"); + + AssertIsOnParentThread(); + + // XXXkhuey this should not need to be rooted, the analysis is dumb. + // See bug 980181. + JS::Rooted<JSObject*> wrapper(aCx, + WorkerBinding::Wrap(aCx, ParentAsWorkerPrivate(), aGivenProto)); + if (wrapper) { + MOZ_ALWAYS_TRUE(TryPreserveWrapper(wrapper)); + } + + return wrapper; +} + +template <class Derived> +nsresult +WorkerPrivateParent<Derived>::DispatchPrivate(already_AddRefed<WorkerRunnable> aRunnable, + nsIEventTarget* aSyncLoopTarget) +{ + // May be called on any thread! + RefPtr<WorkerRunnable> runnable(aRunnable); + + WorkerPrivate* self = ParentAsWorkerPrivate(); + + { + MutexAutoLock lock(mMutex); + + MOZ_ASSERT_IF(aSyncLoopTarget, self->mThread); + + if (!self->mThread) { + if (ParentStatus() == Pending || self->mStatus == Pending) { + mPreStartRunnables.AppendElement(runnable); + return NS_OK; + } + + NS_WARNING("Using a worker event target after the thread has already" + "been released!"); + return NS_ERROR_UNEXPECTED; + } + + if (self->mStatus == Dead || + (!aSyncLoopTarget && ParentStatus() > Running)) { + NS_WARNING("A runnable was posted to a worker that is already shutting " + "down!"); + return NS_ERROR_UNEXPECTED; + } + + nsresult rv; + if (aSyncLoopTarget) { + rv = aSyncLoopTarget->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + } else { + rv = self->mThread->DispatchAnyThread(WorkerThreadFriendKey(), runnable.forget()); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mCondVar.Notify(); + } + + return NS_OK; +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::EnableDebugger() +{ + AssertIsOnParentThread(); + + WorkerPrivate* self = ParentAsWorkerPrivate(); + + if (NS_FAILED(RegisterWorkerDebugger(self))) { + NS_WARNING("Failed to register worker debugger!"); + return; + } +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::DisableDebugger() +{ + AssertIsOnParentThread(); + + WorkerPrivate* self = ParentAsWorkerPrivate(); + + if (NS_FAILED(UnregisterWorkerDebugger(self))) { + NS_WARNING("Failed to unregister worker debugger!"); + } +} + +template <class Derived> +nsresult +WorkerPrivateParent<Derived>::DispatchControlRunnable( + already_AddRefed<WorkerControlRunnable> aWorkerControlRunnable) +{ + // May be called on any thread! + RefPtr<WorkerControlRunnable> runnable(aWorkerControlRunnable); + MOZ_ASSERT(runnable); + + WorkerPrivate* self = ParentAsWorkerPrivate(); + + { + MutexAutoLock lock(mMutex); + + if (self->mStatus == Dead) { + return NS_ERROR_UNEXPECTED; + } + + // Transfer ownership to the control queue. + self->mControlQueue.Push(runnable.forget().take()); + + if (JSContext* cx = self->mJSContext) { + MOZ_ASSERT(self->mThread); + JS_RequestInterruptCallback(cx); + } + + mCondVar.Notify(); + } + + return NS_OK; +} + +template <class Derived> +nsresult +WorkerPrivateParent<Derived>::DispatchDebuggerRunnable( + already_AddRefed<WorkerRunnable> aDebuggerRunnable) +{ + // May be called on any thread! + + RefPtr<WorkerRunnable> runnable(aDebuggerRunnable); + + MOZ_ASSERT(runnable); + + WorkerPrivate* self = ParentAsWorkerPrivate(); + + { + MutexAutoLock lock(mMutex); + + if (self->mStatus == Dead) { + NS_WARNING("A debugger runnable was posted to a worker that is already " + "shutting down!"); + return NS_ERROR_UNEXPECTED; + } + + // Transfer ownership to the debugger queue. + self->mDebuggerQueue.Push(runnable.forget().take()); + + mCondVar.Notify(); + } + + return NS_OK; +} + +template <class Derived> +already_AddRefed<WorkerRunnable> +WorkerPrivateParent<Derived>::MaybeWrapAsWorkerRunnable(already_AddRefed<nsIRunnable> aRunnable) +{ + // May be called on any thread! + + nsCOMPtr<nsIRunnable> runnable(aRunnable); + MOZ_ASSERT(runnable); + + RefPtr<WorkerRunnable> workerRunnable = + WorkerRunnable::FromRunnable(runnable); + if (workerRunnable) { + return workerRunnable.forget(); + } + + nsCOMPtr<nsICancelableRunnable> cancelable = do_QueryInterface(runnable); + if (!cancelable) { + MOZ_CRASH("All runnables destined for a worker thread must be cancelable!"); + } + + workerRunnable = + new ExternalRunnableWrapper(ParentAsWorkerPrivate(), runnable); + return workerRunnable.forget(); +} + +template <class Derived> +already_AddRefed<nsIEventTarget> +WorkerPrivateParent<Derived>::GetEventTarget() +{ + WorkerPrivate* self = ParentAsWorkerPrivate(); + + nsCOMPtr<nsIEventTarget> target; + + { + MutexAutoLock lock(mMutex); + + if (!mEventTarget && + ParentStatus() <= Running && + self->mStatus <= Running) { + mEventTarget = new EventTarget(self); + } + + target = mEventTarget; + } + + NS_WARNING_ASSERTION( + target, + "Requested event target for a worker that is already shutting down!"); + + return target.forget(); +} + +template <class Derived> +bool +WorkerPrivateParent<Derived>::Start() +{ + // May be called on any thread! + { + MutexAutoLock lock(mMutex); + + NS_ASSERTION(mParentStatus != Running, "How can this be?!"); + + if (mParentStatus == Pending) { + mParentStatus = Running; + return true; + } + } + + return false; +} + +// aCx is null when called from the finalizer +template <class Derived> +bool +WorkerPrivateParent<Derived>::NotifyPrivate(Status aStatus) +{ + AssertIsOnParentThread(); + + bool pending; + { + MutexAutoLock lock(mMutex); + + if (mParentStatus >= aStatus) { + return true; + } + + pending = mParentStatus == Pending; + mParentStatus = aStatus; + } + + if (IsSharedWorker()) { + RuntimeService* runtime = RuntimeService::GetService(); + MOZ_ASSERT(runtime); + + runtime->ForgetSharedWorker(ParentAsWorkerPrivate()); + } + + if (pending) { + WorkerPrivate* self = ParentAsWorkerPrivate(); + +#ifdef DEBUG + { + // Fake a thread here just so that our assertions don't go off for no + // reason. + nsIThread* currentThread = NS_GetCurrentThread(); + MOZ_ASSERT(currentThread); + + MOZ_ASSERT(!self->mPRThread); + self->mPRThread = PRThreadFromThread(currentThread); + MOZ_ASSERT(self->mPRThread); + } +#endif + + // Worker never got a chance to run, go ahead and delete it. + self->ScheduleDeletion(WorkerPrivate::WorkerNeverRan); + return true; + } + + NS_ASSERTION(aStatus != Terminating || mQueuedRunnables.IsEmpty(), + "Shouldn't have anything queued!"); + + // Anything queued will be discarded. + mQueuedRunnables.Clear(); + + RefPtr<NotifyRunnable> runnable = + new NotifyRunnable(ParentAsWorkerPrivate(), aStatus); + return runnable->Dispatch(); +} + +template <class Derived> +bool +WorkerPrivateParent<Derived>::Freeze(nsPIDOMWindowInner* aWindow) +{ + AssertIsOnParentThread(); + + // Shared workers are only frozen if all of their owning documents are + // frozen. It can happen that mSharedWorkers is empty but this thread has + // not been unregistered yet. + if ((IsSharedWorker() || IsServiceWorker()) && !mSharedWorkers.IsEmpty()) { + AssertIsOnMainThread(); + + bool allFrozen = true; + + for (uint32_t i = 0; i < mSharedWorkers.Length(); ++i) { + if (aWindow && mSharedWorkers[i]->GetOwner() == aWindow) { + // Calling Freeze() may change the refcount, ensure that the worker + // outlives this call. + RefPtr<SharedWorker> kungFuDeathGrip = mSharedWorkers[i]; + + kungFuDeathGrip->Freeze(); + } else { + MOZ_ASSERT_IF(mSharedWorkers[i]->GetOwner() && aWindow, + !SameCOMIdentity(mSharedWorkers[i]->GetOwner(), + aWindow)); + if (!mSharedWorkers[i]->IsFrozen()) { + allFrozen = false; + } + } + } + + if (!allFrozen || mParentFrozen) { + return true; + } + } + + mParentFrozen = true; + + { + MutexAutoLock lock(mMutex); + + if (mParentStatus >= Terminating) { + return true; + } + } + + DisableDebugger(); + + RefPtr<FreezeRunnable> runnable = + new FreezeRunnable(ParentAsWorkerPrivate()); + if (!runnable->Dispatch()) { + return false; + } + + return true; +} + +template <class Derived> +bool +WorkerPrivateParent<Derived>::Thaw(nsPIDOMWindowInner* aWindow) +{ + AssertIsOnParentThread(); + + MOZ_ASSERT(mParentFrozen); + + // Shared workers are resumed if any of their owning documents are thawed. + // It can happen that mSharedWorkers is empty but this thread has not been + // unregistered yet. + if ((IsSharedWorker() || IsServiceWorker()) && !mSharedWorkers.IsEmpty()) { + AssertIsOnMainThread(); + + bool anyRunning = false; + + for (uint32_t i = 0; i < mSharedWorkers.Length(); ++i) { + if (aWindow && mSharedWorkers[i]->GetOwner() == aWindow) { + // Calling Thaw() may change the refcount, ensure that the worker + // outlives this call. + RefPtr<SharedWorker> kungFuDeathGrip = mSharedWorkers[i]; + + kungFuDeathGrip->Thaw(); + anyRunning = true; + } else { + MOZ_ASSERT_IF(mSharedWorkers[i]->GetOwner() && aWindow, + !SameCOMIdentity(mSharedWorkers[i]->GetOwner(), + aWindow)); + if (!mSharedWorkers[i]->IsFrozen()) { + anyRunning = true; + } + } + } + + if (!anyRunning || !mParentFrozen) { + return true; + } + } + + MOZ_ASSERT(mParentFrozen); + + mParentFrozen = false; + + { + MutexAutoLock lock(mMutex); + + if (mParentStatus >= Terminating) { + return true; + } + } + + EnableDebugger(); + + // Execute queued runnables before waking up the worker, otherwise the worker + // could post new messages before we run those that have been queued. + if (!IsParentWindowPaused() && !mQueuedRunnables.IsEmpty()) { + MOZ_ASSERT(IsDedicatedWorker()); + + nsTArray<nsCOMPtr<nsIRunnable>> runnables; + mQueuedRunnables.SwapElements(runnables); + + for (uint32_t index = 0; index < runnables.Length(); index++) { + runnables[index]->Run(); + } + } + + RefPtr<ThawRunnable> runnable = + new ThawRunnable(ParentAsWorkerPrivate()); + if (!runnable->Dispatch()) { + return false; + } + + return true; +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::ParentWindowPaused() +{ + AssertIsOnMainThread(); + MOZ_ASSERT_IF(IsDedicatedWorker(), mParentWindowPausedDepth == 0); + mParentWindowPausedDepth += 1; +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::ParentWindowResumed() +{ + AssertIsOnMainThread(); + + MOZ_ASSERT(mParentWindowPausedDepth > 0); + MOZ_ASSERT_IF(IsDedicatedWorker(), mParentWindowPausedDepth == 1); + mParentWindowPausedDepth -= 1; + if (mParentWindowPausedDepth > 0) { + return; + } + + { + MutexAutoLock lock(mMutex); + + if (mParentStatus >= Terminating) { + return; + } + } + + // Execute queued runnables before waking up, otherwise the worker could post + // new messages before we run those that have been queued. + if (!IsFrozen() && !mQueuedRunnables.IsEmpty()) { + MOZ_ASSERT(IsDedicatedWorker()); + + nsTArray<nsCOMPtr<nsIRunnable>> runnables; + mQueuedRunnables.SwapElements(runnables); + + for (uint32_t index = 0; index < runnables.Length(); index++) { + runnables[index]->Run(); + } + } +} + +template <class Derived> +bool +WorkerPrivateParent<Derived>::Close() +{ + AssertIsOnParentThread(); + + { + MutexAutoLock lock(mMutex); + + if (mParentStatus < Closing) { + mParentStatus = Closing; + } + } + + return true; +} + +template <class Derived> +bool +WorkerPrivateParent<Derived>::ModifyBusyCount(bool aIncrease) +{ + AssertIsOnParentThread(); + + NS_ASSERTION(aIncrease || mBusyCount, "Mismatched busy count mods!"); + + if (aIncrease) { + mBusyCount++; + return true; + } + + if (--mBusyCount == 0) { + + bool shouldCancel; + { + MutexAutoLock lock(mMutex); + shouldCancel = mParentStatus == Terminating; + } + + if (shouldCancel && !Cancel()) { + return false; + } + } + + return true; +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::ForgetOverridenLoadGroup( + nsCOMPtr<nsILoadGroup>& aLoadGroupOut) +{ + AssertIsOnParentThread(); + + // If we're not overriden, then do nothing here. Let the load group get + // handled in ForgetMainThreadObjects(). + if (!mLoadInfo.mInterfaceRequestor) { + return; + } + + mLoadInfo.mLoadGroup.swap(aLoadGroupOut); +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::ForgetMainThreadObjects( + nsTArray<nsCOMPtr<nsISupports> >& aDoomed) +{ + AssertIsOnParentThread(); + MOZ_ASSERT(!mMainThreadObjectsForgotten); + + static const uint32_t kDoomedCount = 10; + + aDoomed.SetCapacity(kDoomedCount); + + SwapToISupportsArray(mLoadInfo.mWindow, aDoomed); + SwapToISupportsArray(mLoadInfo.mScriptContext, aDoomed); + SwapToISupportsArray(mLoadInfo.mBaseURI, aDoomed); + SwapToISupportsArray(mLoadInfo.mResolvedScriptURI, aDoomed); + SwapToISupportsArray(mLoadInfo.mPrincipal, aDoomed); + SwapToISupportsArray(mLoadInfo.mChannel, aDoomed); + SwapToISupportsArray(mLoadInfo.mCSP, aDoomed); + SwapToISupportsArray(mLoadInfo.mLoadGroup, aDoomed); + SwapToISupportsArray(mLoadInfo.mLoadFailedAsyncRunnable, aDoomed); + SwapToISupportsArray(mLoadInfo.mInterfaceRequestor, aDoomed); + // Before adding anything here update kDoomedCount above! + + MOZ_ASSERT(aDoomed.Length() == kDoomedCount); + + mMainThreadObjectsForgotten = true; +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::PostMessageInternal( + JSContext* aCx, + JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + UniquePtr<ServiceWorkerClientInfo>&& aClientInfo, + PromiseNativeHandler* aHandler, + ErrorResult& aRv) +{ + AssertIsOnParentThread(); + + { + MutexAutoLock lock(mMutex); + if (mParentStatus > Running) { + return; + } + } + + JS::Rooted<JS::Value> transferable(aCx, JS::UndefinedValue()); + if (aTransferable.WasPassed()) { + const Sequence<JS::Value>& realTransferable = aTransferable.Value(); + + // The input sequence only comes from the generated bindings code, which + // ensures it is rooted. + JS::HandleValueArray elements = + JS::HandleValueArray::fromMarkedLocation(realTransferable.Length(), + realTransferable.Elements()); + + JSObject* array = + JS_NewArrayObject(aCx, elements); + if (!array) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + transferable.setObject(*array); + } + + RefPtr<MessageEventRunnable> runnable = + new MessageEventRunnable(ParentAsWorkerPrivate(), + WorkerRunnable::WorkerThreadModifyBusyCount); + + UniquePtr<AbstractTimelineMarker> start; + UniquePtr<AbstractTimelineMarker> end; + RefPtr<TimelineConsumers> timelines = TimelineConsumers::Get(); + bool isTimelineRecording = timelines && !timelines->IsEmpty(); + + if (isTimelineRecording) { + start = MakeUnique<WorkerTimelineMarker>(NS_IsMainThread() + ? ProfileTimelineWorkerOperationType::SerializeDataOnMainThread + : ProfileTimelineWorkerOperationType::SerializeDataOffMainThread, + MarkerTracingType::START); + } + + runnable->Write(aCx, aMessage, transferable, JS::CloneDataPolicy(), aRv); + + if (isTimelineRecording) { + end = MakeUnique<WorkerTimelineMarker>(NS_IsMainThread() + ? ProfileTimelineWorkerOperationType::SerializeDataOnMainThread + : ProfileTimelineWorkerOperationType::SerializeDataOffMainThread, + MarkerTracingType::END); + timelines->AddMarkerForAllObservedDocShells(start); + timelines->AddMarkerForAllObservedDocShells(end); + } + + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + runnable->SetServiceWorkerData(Move(aClientInfo), aHandler); + + if (!runnable->Dispatch()) { + aRv.Throw(NS_ERROR_FAILURE); + } +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::PostMessage( + JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + ErrorResult& aRv) +{ + PostMessageInternal(aCx, aMessage, aTransferable, nullptr, nullptr, aRv); +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::PostMessageToServiceWorker( + JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + UniquePtr<ServiceWorkerClientInfo>&& aClientInfo, + PromiseNativeHandler* aHandler, + ErrorResult& aRv) +{ + AssertIsOnMainThread(); + PostMessageInternal(aCx, aMessage, aTransferable, Move(aClientInfo), + aHandler, aRv); +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::UpdateContextOptions( + const JS::ContextOptions& aContextOptions) +{ + AssertIsOnParentThread(); + + { + MutexAutoLock lock(mMutex); + mJSSettings.contextOptions = aContextOptions; + } + + RefPtr<UpdateContextOptionsRunnable> runnable = + new UpdateContextOptionsRunnable(ParentAsWorkerPrivate(), aContextOptions); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to update worker context options!"); + } +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::UpdatePreference(WorkerPreference aPref, bool aValue) +{ + AssertIsOnParentThread(); + MOZ_ASSERT(aPref >= 0 && aPref < WORKERPREF_COUNT); + + RefPtr<UpdatePreferenceRunnable> runnable = + new UpdatePreferenceRunnable(ParentAsWorkerPrivate(), aPref, aValue); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to update worker preferences!"); + } +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::UpdateLanguages(const nsTArray<nsString>& aLanguages) +{ + AssertIsOnParentThread(); + + RefPtr<UpdateLanguagesRunnable> runnable = + new UpdateLanguagesRunnable(ParentAsWorkerPrivate(), aLanguages); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to update worker languages!"); + } +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::UpdateJSWorkerMemoryParameter(JSGCParamKey aKey, + uint32_t aValue) +{ + AssertIsOnParentThread(); + + bool found = false; + + { + MutexAutoLock lock(mMutex); + found = mJSSettings.ApplyGCSetting(aKey, aValue); + } + + if (found) { + RefPtr<UpdateJSWorkerMemoryParameterRunnable> runnable = + new UpdateJSWorkerMemoryParameterRunnable(ParentAsWorkerPrivate(), aKey, + aValue); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to update memory parameter!"); + } + } +} + +#ifdef JS_GC_ZEAL +template <class Derived> +void +WorkerPrivateParent<Derived>::UpdateGCZeal(uint8_t aGCZeal, uint32_t aFrequency) +{ + AssertIsOnParentThread(); + + { + MutexAutoLock lock(mMutex); + mJSSettings.gcZeal = aGCZeal; + mJSSettings.gcZealFrequency = aFrequency; + } + + RefPtr<UpdateGCZealRunnable> runnable = + new UpdateGCZealRunnable(ParentAsWorkerPrivate(), aGCZeal, aFrequency); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to update worker gczeal!"); + } +} +#endif + +template <class Derived> +void +WorkerPrivateParent<Derived>::GarbageCollect(bool aShrinking) +{ + AssertIsOnParentThread(); + + RefPtr<GarbageCollectRunnable> runnable = + new GarbageCollectRunnable(ParentAsWorkerPrivate(), aShrinking, + /* collectChildren = */ true); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to GC worker!"); + } +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::CycleCollect(bool aDummy) +{ + AssertIsOnParentThread(); + + RefPtr<CycleCollectRunnable> runnable = + new CycleCollectRunnable(ParentAsWorkerPrivate(), + /* collectChildren = */ true); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to CC worker!"); + } +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::OfflineStatusChangeEvent(bool aIsOffline) +{ + AssertIsOnParentThread(); + + RefPtr<OfflineStatusChangeRunnable> runnable = + new OfflineStatusChangeRunnable(ParentAsWorkerPrivate(), aIsOffline); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to dispatch offline status change event!"); + } +} + +void +WorkerPrivate::OfflineStatusChangeEventInternal(bool aIsOffline) +{ + AssertIsOnWorkerThread(); + + // The worker is already in this state. No need to dispatch an event. + if (mOnLine == !aIsOffline) { + return; + } + + for (uint32_t index = 0; index < mChildWorkers.Length(); ++index) { + mChildWorkers[index]->OfflineStatusChangeEvent(aIsOffline); + } + + mOnLine = !aIsOffline; + WorkerGlobalScope* globalScope = GlobalScope(); + RefPtr<WorkerNavigator> nav = globalScope->GetExistingNavigator(); + if (nav) { + nav->SetOnLine(mOnLine); + } + + nsString eventType; + if (aIsOffline) { + eventType.AssignLiteral("offline"); + } else { + eventType.AssignLiteral("online"); + } + + RefPtr<Event> event = NS_NewDOMEvent(globalScope, nullptr, nullptr); + + event->InitEvent(eventType, false, false); + event->SetTrusted(true); + + globalScope->DispatchDOMEvent(nullptr, event, nullptr, nullptr); +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::MemoryPressure(bool aDummy) +{ + AssertIsOnParentThread(); + + RefPtr<MemoryPressureRunnable> runnable = + new MemoryPressureRunnable(ParentAsWorkerPrivate()); + Unused << NS_WARN_IF(!runnable->Dispatch()); +} + +template <class Derived> +bool +WorkerPrivateParent<Derived>::RegisterSharedWorker(SharedWorker* aSharedWorker, + MessagePort* aPort) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aSharedWorker); + MOZ_ASSERT(IsSharedWorker()); + MOZ_ASSERT(!mSharedWorkers.Contains(aSharedWorker)); + + if (IsSharedWorker()) { + RefPtr<MessagePortRunnable> runnable = + new MessagePortRunnable(ParentAsWorkerPrivate(), aPort); + if (!runnable->Dispatch()) { + return false; + } + } + + mSharedWorkers.AppendElement(aSharedWorker); + + // If there were other SharedWorker objects attached to this worker then they + // may all have been frozen and this worker would need to be thawed. + if (mSharedWorkers.Length() > 1 && IsFrozen() && !Thaw(nullptr)) { + return false; + } + + return true; +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::BroadcastErrorToSharedWorkers( + JSContext* aCx, + const nsAString& aMessage, + const nsAString& aFilename, + const nsAString& aLine, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aFlags) +{ + AssertIsOnMainThread(); + + if (JSREPORT_IS_WARNING(aFlags)) { + // Don't fire any events anywhere. Just log to console. + // XXXbz should we log to all the consoles of all the relevant windows? + LogErrorToConsole(aMessage, aFilename, aLine, aLineNumber, aColumnNumber, + aFlags, 0); + return; + } + + AutoTArray<RefPtr<SharedWorker>, 10> sharedWorkers; + GetAllSharedWorkers(sharedWorkers); + + if (sharedWorkers.IsEmpty()) { + return; + } + + AutoTArray<WindowAction, 10> windowActions; + nsresult rv; + + // First fire the error event at all SharedWorker objects. This may include + // multiple objects in a single window as well as objects in different + // windows. + for (size_t index = 0; index < sharedWorkers.Length(); index++) { + RefPtr<SharedWorker>& sharedWorker = sharedWorkers[index]; + + // May be null. + nsPIDOMWindowInner* window = sharedWorker->GetOwner(); + + RootedDictionary<ErrorEventInit> errorInit(aCx); + errorInit.mBubbles = false; + errorInit.mCancelable = true; + errorInit.mMessage = aMessage; + errorInit.mFilename = aFilename; + errorInit.mLineno = aLineNumber; + errorInit.mColno = aColumnNumber; + + RefPtr<ErrorEvent> errorEvent = + ErrorEvent::Constructor(sharedWorker, NS_LITERAL_STRING("error"), + errorInit); + if (!errorEvent) { + ThrowAndReport(window, NS_ERROR_UNEXPECTED); + continue; + } + + errorEvent->SetTrusted(true); + + bool defaultActionEnabled; + nsresult rv = sharedWorker->DispatchEvent(errorEvent, &defaultActionEnabled); + if (NS_FAILED(rv)) { + ThrowAndReport(window, rv); + continue; + } + + if (defaultActionEnabled) { + // Add the owning window to our list so that we will fire an error event + // at it later. + if (!windowActions.Contains(window)) { + windowActions.AppendElement(WindowAction(window)); + } + } else { + size_t actionsIndex = windowActions.LastIndexOf(WindowAction(window)); + if (actionsIndex != windowActions.NoIndex) { + // Any listener that calls preventDefault() will prevent the window from + // receiving the error event. + windowActions[actionsIndex].mDefaultAction = false; + } + } + } + + // If there are no windows to consider further then we're done. + if (windowActions.IsEmpty()) { + return; + } + + bool shouldLogErrorToConsole = true; + + // Now fire error events at all the windows remaining. + for (uint32_t index = 0; index < windowActions.Length(); index++) { + WindowAction& windowAction = windowActions[index]; + + // If there is no window or the script already called preventDefault then + // skip this window. + if (!windowAction.mWindow || !windowAction.mDefaultAction) { + continue; + } + + nsCOMPtr<nsIScriptGlobalObject> sgo = + do_QueryInterface(windowAction.mWindow); + MOZ_ASSERT(sgo); + + MOZ_ASSERT(NS_IsMainThread()); + RootedDictionary<ErrorEventInit> init(aCx); + init.mLineno = aLineNumber; + init.mFilename = aFilename; + init.mMessage = aMessage; + init.mCancelable = true; + init.mBubbles = true; + + nsEventStatus status = nsEventStatus_eIgnore; + rv = sgo->HandleScriptError(init, &status); + if (NS_FAILED(rv)) { + ThrowAndReport(windowAction.mWindow, rv); + continue; + } + + if (status == nsEventStatus_eConsumeNoDefault) { + shouldLogErrorToConsole = false; + } + } + + // Finally log a warning in the console if no window tried to prevent it. + if (shouldLogErrorToConsole) { + LogErrorToConsole(aMessage, aFilename, aLine, aLineNumber, aColumnNumber, + aFlags, 0); + } +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::GetAllSharedWorkers( + nsTArray<RefPtr<SharedWorker>>& aSharedWorkers) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(IsSharedWorker() || IsServiceWorker()); + + if (!aSharedWorkers.IsEmpty()) { + aSharedWorkers.Clear(); + } + + for (uint32_t i = 0; i < mSharedWorkers.Length(); ++i) { + aSharedWorkers.AppendElement(mSharedWorkers[i]); + } +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::CloseSharedWorkersForWindow( + nsPIDOMWindowInner* aWindow) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(IsSharedWorker() || IsServiceWorker()); + MOZ_ASSERT(aWindow); + + bool someRemoved = false; + + for (uint32_t i = 0; i < mSharedWorkers.Length();) { + if (mSharedWorkers[i]->GetOwner() == aWindow) { + mSharedWorkers[i]->Close(); + mSharedWorkers.RemoveElementAt(i); + someRemoved = true; + } else { + MOZ_ASSERT(!SameCOMIdentity(mSharedWorkers[i]->GetOwner(), + aWindow)); + ++i; + } + } + + if (!someRemoved) { + return; + } + + // If there are still SharedWorker objects attached to this worker then they + // may all be frozen and this worker would need to be frozen. Otherwise, + // if that was the last SharedWorker then it's time to cancel this worker. + + if (!mSharedWorkers.IsEmpty()) { + Freeze(nullptr); + } else { + Cancel(); + } +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::CloseAllSharedWorkers() +{ + AssertIsOnMainThread(); + MOZ_ASSERT(IsSharedWorker() || IsServiceWorker()); + + for (uint32_t i = 0; i < mSharedWorkers.Length(); ++i) { + mSharedWorkers[i]->Close(); + } + + mSharedWorkers.Clear(); + + Cancel(); +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::WorkerScriptLoaded() +{ + AssertIsOnMainThread(); + + if (IsSharedWorker() || IsServiceWorker()) { + // No longer need to hold references to the window or document we came from. + mLoadInfo.mWindow = nullptr; + mLoadInfo.mScriptContext = nullptr; + } +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::SetBaseURI(nsIURI* aBaseURI) +{ + AssertIsOnMainThread(); + + if (!mLoadInfo.mBaseURI) { + NS_ASSERTION(GetParent(), "Shouldn't happen without a parent!"); + mLoadInfo.mResolvedScriptURI = aBaseURI; + } + + mLoadInfo.mBaseURI = aBaseURI; + + if (NS_FAILED(aBaseURI->GetSpec(mLocationInfo.mHref))) { + mLocationInfo.mHref.Truncate(); + } + + mLocationInfo.mHostname.Truncate(); + nsContentUtils::GetHostOrIPv6WithBrackets(aBaseURI, mLocationInfo.mHostname); + + nsCOMPtr<nsIURL> url(do_QueryInterface(aBaseURI)); + if (!url || NS_FAILED(url->GetFilePath(mLocationInfo.mPathname))) { + mLocationInfo.mPathname.Truncate(); + } + + nsCString temp; + + if (url && NS_SUCCEEDED(url->GetQuery(temp)) && !temp.IsEmpty()) { + mLocationInfo.mSearch.Assign('?'); + mLocationInfo.mSearch.Append(temp); + } + + if (NS_SUCCEEDED(aBaseURI->GetRef(temp)) && !temp.IsEmpty()) { + nsCOMPtr<nsITextToSubURI> converter = + do_GetService(NS_ITEXTTOSUBURI_CONTRACTID); + if (converter && nsContentUtils::GettersDecodeURLHash()) { + nsCString charset; + nsAutoString unicodeRef; + if (NS_SUCCEEDED(aBaseURI->GetOriginCharset(charset)) && + NS_SUCCEEDED(converter->UnEscapeURIForUI(charset, temp, + unicodeRef))) { + mLocationInfo.mHash.Assign('#'); + mLocationInfo.mHash.Append(NS_ConvertUTF16toUTF8(unicodeRef)); + } + } + + if (mLocationInfo.mHash.IsEmpty()) { + mLocationInfo.mHash.Assign('#'); + mLocationInfo.mHash.Append(temp); + } + } + + if (NS_SUCCEEDED(aBaseURI->GetScheme(mLocationInfo.mProtocol))) { + mLocationInfo.mProtocol.Append(':'); + } + else { + mLocationInfo.mProtocol.Truncate(); + } + + int32_t port; + if (NS_SUCCEEDED(aBaseURI->GetPort(&port)) && port != -1) { + mLocationInfo.mPort.AppendInt(port); + + nsAutoCString host(mLocationInfo.mHostname); + host.Append(':'); + host.Append(mLocationInfo.mPort); + + mLocationInfo.mHost.Assign(host); + } + else { + mLocationInfo.mHost.Assign(mLocationInfo.mHostname); + } + + nsContentUtils::GetUTFOrigin(aBaseURI, mLocationInfo.mOrigin); +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::SetPrincipal(nsIPrincipal* aPrincipal, + nsILoadGroup* aLoadGroup) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(aLoadGroup, aPrincipal)); + MOZ_ASSERT(!mLoadInfo.mPrincipalInfo); + + mLoadInfo.mPrincipal = aPrincipal; + mLoadInfo.mPrincipalIsSystem = nsContentUtils::IsSystemPrincipal(aPrincipal); + + aPrincipal->GetCsp(getter_AddRefs(mLoadInfo.mCSP)); + + if (mLoadInfo.mCSP) { + mLoadInfo.mCSP->GetAllowsEval(&mLoadInfo.mReportCSPViolations, + &mLoadInfo.mEvalAllowed); + // Set ReferrerPolicy + bool hasReferrerPolicy = false; + uint32_t rp = mozilla::net::RP_Default; + + nsresult rv = mLoadInfo.mCSP->GetReferrerPolicy(&rp, &hasReferrerPolicy); + NS_ENSURE_SUCCESS_VOID(rv); + + if (hasReferrerPolicy) { + mLoadInfo.mReferrerPolicy = static_cast<net::ReferrerPolicy>(rp); + } + } else { + mLoadInfo.mEvalAllowed = true; + mLoadInfo.mReportCSPViolations = false; + } + + mLoadInfo.mLoadGroup = aLoadGroup; + + mLoadInfo.mPrincipalInfo = new PrincipalInfo(); + mLoadInfo.mOriginAttributes = nsContentUtils::GetOriginAttributes(aLoadGroup); + + MOZ_ALWAYS_SUCCEEDS( + PrincipalToPrincipalInfo(aPrincipal, mLoadInfo.mPrincipalInfo)); +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::UpdateOverridenLoadGroup(nsILoadGroup* aBaseLoadGroup) +{ + AssertIsOnMainThread(); + + // The load group should have been overriden at init time. + mLoadInfo.mInterfaceRequestor->MaybeAddTabChild(aBaseLoadGroup); +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::FlushReportsToSharedWorkers( + nsIConsoleReportCollector* aReporter) +{ + AssertIsOnMainThread(); + + AutoTArray<RefPtr<SharedWorker>, 10> sharedWorkers; + AutoTArray<WindowAction, 10> windowActions; + GetAllSharedWorkers(sharedWorkers); + + // First find out all the shared workers' window. + for (size_t index = 0; index < sharedWorkers.Length(); index++) { + RefPtr<SharedWorker>& sharedWorker = sharedWorkers[index]; + + // May be null. + nsPIDOMWindowInner* window = sharedWorker->GetOwner(); + + // Add the owning window to our list so that we will flush the reports later. + if (window && !windowActions.Contains(window)) { + windowActions.AppendElement(WindowAction(window)); + } + } + + bool reportErrorToBrowserConsole = true; + + // Flush the reports. + for (uint32_t index = 0; index < windowActions.Length(); index++) { + WindowAction& windowAction = windowActions[index]; + + aReporter->FlushConsoleReports(windowAction.mWindow->GetExtantDoc(), + nsIConsoleReportCollector::ReportAction::Save); + reportErrorToBrowserConsole = false; + } + + // Finally report to broswer console if there is no any window or shared + // worker. + if (reportErrorToBrowserConsole) { + aReporter->FlushConsoleReports((nsIDocument*)nullptr); + return; + } + + aReporter->ClearConsoleReports(); +} + +template <class Derived> +NS_IMPL_ADDREF_INHERITED(WorkerPrivateParent<Derived>, DOMEventTargetHelper) + +template <class Derived> +NS_IMPL_RELEASE_INHERITED(WorkerPrivateParent<Derived>, DOMEventTargetHelper) + +template <class Derived> +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(WorkerPrivateParent<Derived>) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +template <class Derived> +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WorkerPrivateParent<Derived>, + DOMEventTargetHelper) + tmp->AssertIsOnParentThread(); + + // The WorkerPrivate::mSelfRef has a reference to itself, which is really + // held by the worker thread. We traverse this reference if and only if our + // busy count is zero and we have not released the main thread reference. + // We do not unlink it. This allows the CC to break cycles involving the + // WorkerPrivate and begin shutting it down (which does happen in unlink) but + // ensures that the WorkerPrivate won't be deleted before we're done shutting + // down the thread. + + if (!tmp->mBusyCount && !tmp->mMainThreadObjectsForgotten) { + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelfRef) + } + + // The various strong references in LoadInfo are managed manually and cannot + // be cycle collected. +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +template <class Derived> +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WorkerPrivateParent<Derived>, + DOMEventTargetHelper) + tmp->Terminate(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +template <class Derived> +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(WorkerPrivateParent<Derived>, + DOMEventTargetHelper) + tmp->AssertIsOnParentThread(); +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +#ifdef DEBUG + +template <class Derived> +void +WorkerPrivateParent<Derived>::AssertIsOnParentThread() const +{ + if (GetParent()) { + GetParent()->AssertIsOnWorkerThread(); + } + else { + AssertIsOnMainThread(); + } +} + +template <class Derived> +void +WorkerPrivateParent<Derived>::AssertInnerWindowIsCorrect() const +{ + AssertIsOnParentThread(); + + // Only care about top level workers from windows. + if (mParent || !mLoadInfo.mWindow) { + return; + } + + AssertIsOnMainThread(); + + nsPIDOMWindowOuter* outer = mLoadInfo.mWindow->GetOuterWindow(); + NS_ASSERTION(outer && outer->GetCurrentInnerWindow() == mLoadInfo.mWindow, + "Inner window no longer correct!"); +} + +#endif + +class PostDebuggerMessageRunnable final : public Runnable +{ + WorkerDebugger *mDebugger; + nsString mMessage; + +public: + PostDebuggerMessageRunnable(WorkerDebugger* aDebugger, + const nsAString& aMessage) + : mDebugger(aDebugger), + mMessage(aMessage) + { + } + +private: + ~PostDebuggerMessageRunnable() + { } + + NS_IMETHOD + Run() override + { + mDebugger->PostMessageToDebuggerOnMainThread(mMessage); + + return NS_OK; + } +}; + +class ReportDebuggerErrorRunnable final : public Runnable +{ + WorkerDebugger *mDebugger; + nsString mFilename; + uint32_t mLineno; + nsString mMessage; + +public: + ReportDebuggerErrorRunnable(WorkerDebugger* aDebugger, + const nsAString& aFilename, uint32_t aLineno, + const nsAString& aMessage) + : mDebugger(aDebugger), + mFilename(aFilename), + mLineno(aLineno), + mMessage(aMessage) + { + } + +private: + ~ReportDebuggerErrorRunnable() + { } + + NS_IMETHOD + Run() override + { + mDebugger->ReportErrorToDebuggerOnMainThread(mFilename, mLineno, mMessage); + + return NS_OK; + } +}; + +WorkerDebugger::WorkerDebugger(WorkerPrivate* aWorkerPrivate) +: mWorkerPrivate(aWorkerPrivate), + mIsInitialized(false) +{ + AssertIsOnMainThread(); +} + +WorkerDebugger::~WorkerDebugger() +{ + MOZ_ASSERT(!mWorkerPrivate); + + if (!NS_IsMainThread()) { + for (size_t index = 0; index < mListeners.Length(); ++index) { + NS_ReleaseOnMainThread(mListeners[index].forget()); + } + } +} + +NS_IMPL_ISUPPORTS(WorkerDebugger, nsIWorkerDebugger) + +NS_IMETHODIMP +WorkerDebugger::GetIsClosed(bool* aResult) +{ + AssertIsOnMainThread(); + + *aResult = !mWorkerPrivate; + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetIsChrome(bool* aResult) +{ + AssertIsOnMainThread(); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + *aResult = mWorkerPrivate->IsChromeWorker(); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetIsInitialized(bool* aResult) +{ + AssertIsOnMainThread(); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + *aResult = mIsInitialized; + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetParent(nsIWorkerDebugger** aResult) +{ + AssertIsOnMainThread(); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + WorkerPrivate* parent = mWorkerPrivate->GetParent(); + if (!parent) { + *aResult = nullptr; + return NS_OK; + } + + MOZ_ASSERT(mWorkerPrivate->IsDedicatedWorker()); + + nsCOMPtr<nsIWorkerDebugger> debugger = parent->Debugger(); + debugger.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetType(uint32_t* aResult) +{ + AssertIsOnMainThread(); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + *aResult = mWorkerPrivate->Type(); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetUrl(nsAString& aResult) +{ + AssertIsOnMainThread(); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + aResult = mWorkerPrivate->ScriptURL(); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetWindow(mozIDOMWindow** aResult) +{ + AssertIsOnMainThread(); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + if (mWorkerPrivate->GetParent() || !mWorkerPrivate->IsDedicatedWorker()) { + *aResult = nullptr; + return NS_OK; + } + + nsCOMPtr<nsPIDOMWindowInner> window = mWorkerPrivate->GetWindow(); + window.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetPrincipal(nsIPrincipal** aResult) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aResult); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr<nsIPrincipal> prin = mWorkerPrivate->GetPrincipal(); + prin.forget(aResult); + + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::GetServiceWorkerID(uint32_t* aResult) +{ + AssertIsOnMainThread(); + MOZ_ASSERT(aResult); + + if (!mWorkerPrivate || !mWorkerPrivate->IsServiceWorker()) { + return NS_ERROR_UNEXPECTED; + } + + *aResult = mWorkerPrivate->ServiceWorkerID(); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::Initialize(const nsAString& aURL) +{ + AssertIsOnMainThread(); + + if (!mWorkerPrivate) { + return NS_ERROR_UNEXPECTED; + } + + if (!mIsInitialized) { + RefPtr<CompileDebuggerScriptRunnable> runnable = + new CompileDebuggerScriptRunnable(mWorkerPrivate, aURL); + if (!runnable->Dispatch()) { + return NS_ERROR_FAILURE; + } + + mIsInitialized = true; + } + + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::PostMessageMoz(const nsAString& aMessage) +{ + AssertIsOnMainThread(); + + if (!mWorkerPrivate || !mIsInitialized) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<DebuggerMessageEventRunnable> runnable = + new DebuggerMessageEventRunnable(mWorkerPrivate, aMessage); + if (!runnable->Dispatch()) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::AddListener(nsIWorkerDebuggerListener* aListener) +{ + AssertIsOnMainThread(); + + if (mListeners.Contains(aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mListeners.AppendElement(aListener); + return NS_OK; +} + +NS_IMETHODIMP +WorkerDebugger::RemoveListener(nsIWorkerDebuggerListener* aListener) +{ + AssertIsOnMainThread(); + + if (!mListeners.Contains(aListener)) { + return NS_ERROR_INVALID_ARG; + } + + mListeners.RemoveElement(aListener); + return NS_OK; +} + +void +WorkerDebugger::Close() +{ + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate = nullptr; + + nsTArray<nsCOMPtr<nsIWorkerDebuggerListener>> listeners(mListeners); + for (size_t index = 0; index < listeners.Length(); ++index) { + listeners[index]->OnClose(); + } +} + +void +WorkerDebugger::PostMessageToDebugger(const nsAString& aMessage) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<PostDebuggerMessageRunnable> runnable = + new PostDebuggerMessageRunnable(this, aMessage); + if (NS_FAILED(mWorkerPrivate->DispatchToMainThread(runnable.forget()))) { + NS_WARNING("Failed to post message to debugger on main thread!"); + } +} + +void +WorkerDebugger::PostMessageToDebuggerOnMainThread(const nsAString& aMessage) +{ + AssertIsOnMainThread(); + + nsTArray<nsCOMPtr<nsIWorkerDebuggerListener>> listeners(mListeners); + for (size_t index = 0; index < listeners.Length(); ++index) { + listeners[index]->OnMessage(aMessage); + } +} + +void +WorkerDebugger::ReportErrorToDebugger(const nsAString& aFilename, + uint32_t aLineno, + const nsAString& aMessage) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<ReportDebuggerErrorRunnable> runnable = + new ReportDebuggerErrorRunnable(this, aFilename, aLineno, aMessage); + if (NS_FAILED(mWorkerPrivate->DispatchToMainThread(runnable.forget()))) { + NS_WARNING("Failed to report error to debugger on main thread!"); + } +} + +void +WorkerDebugger::ReportErrorToDebuggerOnMainThread(const nsAString& aFilename, + uint32_t aLineno, + const nsAString& aMessage) +{ + AssertIsOnMainThread(); + + nsTArray<nsCOMPtr<nsIWorkerDebuggerListener>> listeners(mListeners); + for (size_t index = 0; index < listeners.Length(); ++index) { + listeners[index]->OnError(aFilename, aLineno, aMessage); + } + + LogErrorToConsole(aMessage, aFilename, nsString(), aLineno, 0, 0, 0); +} + +WorkerPrivate::WorkerPrivate(WorkerPrivate* aParent, + const nsAString& aScriptURL, + bool aIsChromeWorker, WorkerType aWorkerType, + const nsACString& aWorkerName, + WorkerLoadInfo& aLoadInfo) + : WorkerPrivateParent<WorkerPrivate>(aParent, aScriptURL, + aIsChromeWorker, aWorkerType, + aWorkerName, aLoadInfo) + , mDebuggerRegistered(false) + , mDebugger(nullptr) + , mJSContext(nullptr) + , mPRThread(nullptr) + , mDebuggerEventLoopLevel(0) + , mMainThreadEventTarget(do_GetMainThread()) + , mErrorHandlerRecursionCount(0) + , mNextTimeoutId(1) + , mStatus(Pending) + , mFrozen(false) + , mTimerRunning(false) + , mRunningExpiredTimeouts(false) + , mPendingEventQueueClearing(false) + , mCancelAllPendingRunnables(false) + , mPeriodicGCTimerRunning(false) + , mIdleGCTimerRunning(false) + , mWorkerScriptExecutedSuccessfully(false) + , mOnLine(false) +{ + MOZ_ASSERT_IF(!IsDedicatedWorker(), !aWorkerName.IsVoid()); + MOZ_ASSERT_IF(IsDedicatedWorker(), aWorkerName.IsEmpty()); + + if (aParent) { + aParent->AssertIsOnWorkerThread(); + aParent->GetAllPreferences(mPreferences); + mOnLine = aParent->OnLine(); + } + else { + AssertIsOnMainThread(); + RuntimeService::GetDefaultPreferences(mPreferences); + mOnLine = !NS_IsOffline(); + } + + nsCOMPtr<nsIEventTarget> target; + + // A child worker just inherits the parent workers ThrottledEventQueue + // and main thread target for now. This is mainly due to the restriction + // that ThrottledEventQueue can only be created on the main thread at the + // moment. + if (aParent) { + mMainThreadThrottledEventQueue = aParent->mMainThreadThrottledEventQueue; + mMainThreadEventTarget = aParent->mMainThreadEventTarget; + return; + } + + MOZ_ASSERT(NS_IsMainThread()); + target = GetWindow() ? GetWindow()->GetThrottledEventQueue() : nullptr; + + if (!target) { + nsCOMPtr<nsIThread> mainThread; + NS_GetMainThread(getter_AddRefs(mainThread)); + MOZ_DIAGNOSTIC_ASSERT(mainThread); + target = mainThread; + } + + // Throttle events to the main thread using a ThrottledEventQueue specific to + // this worker thread. This may return nullptr during shutdown. + mMainThreadThrottledEventQueue = ThrottledEventQueue::Create(target); + + // If we were able to creat the throttled event queue, then use it for + // dispatching our main thread runnables. Otherwise use our underlying + // base target. + if (mMainThreadThrottledEventQueue) { + mMainThreadEventTarget = mMainThreadThrottledEventQueue; + } else { + mMainThreadEventTarget = target.forget(); + } +} + +WorkerPrivate::~WorkerPrivate() +{ +} + +// static +already_AddRefed<WorkerPrivate> +WorkerPrivate::Constructor(const GlobalObject& aGlobal, + const nsAString& aScriptURL, + ErrorResult& aRv) +{ + return WorkerPrivate::Constructor(aGlobal, aScriptURL, false, + WorkerTypeDedicated, EmptyCString(), + nullptr, aRv); +} + +// static +bool +WorkerPrivate::WorkerAvailable(JSContext* /* unused */, JSObject* /* unused */) +{ + // If we're already on a worker workers are clearly enabled. + if (!NS_IsMainThread()) { + return true; + } + + // If our caller is chrome, workers are always available. + if (nsContentUtils::IsCallerChrome()) { + return true; + } + + // Else check the pref. + return Preferences::GetBool(PREF_WORKERS_ENABLED); +} + +// static +already_AddRefed<ChromeWorkerPrivate> +ChromeWorkerPrivate::Constructor(const GlobalObject& aGlobal, + const nsAString& aScriptURL, + ErrorResult& aRv) +{ + return WorkerPrivate::Constructor(aGlobal, aScriptURL, true, + WorkerTypeDedicated, EmptyCString(), + nullptr, aRv) + .downcast<ChromeWorkerPrivate>(); +} + +// static +bool +ChromeWorkerPrivate::WorkerAvailable(JSContext* aCx, JSObject* /* unused */) +{ + // Chrome is always allowed to use workers, and content is never + // allowed to use ChromeWorker, so all we have to check is the + // caller. However, chrome workers apparently might not have a + // system principal, so we have to check for them manually. + if (NS_IsMainThread()) { + return nsContentUtils::IsCallerChrome(); + } + + return GetWorkerPrivateFromContext(aCx)->IsChromeWorker(); +} + +// static +already_AddRefed<WorkerPrivate> +WorkerPrivate::Constructor(const GlobalObject& aGlobal, + const nsAString& aScriptURL, + bool aIsChromeWorker, WorkerType aWorkerType, + const nsACString& aWorkerName, + WorkerLoadInfo* aLoadInfo, ErrorResult& aRv) +{ + JSContext* cx = aGlobal.Context(); + return Constructor(cx, aScriptURL, aIsChromeWorker, aWorkerType, + aWorkerName, aLoadInfo, aRv); +} + +// static +already_AddRefed<WorkerPrivate> +WorkerPrivate::Constructor(JSContext* aCx, + const nsAString& aScriptURL, + bool aIsChromeWorker, WorkerType aWorkerType, + const nsACString& aWorkerName, + WorkerLoadInfo* aLoadInfo, ErrorResult& aRv) +{ + // If this is a sub-worker, we need to keep the parent worker alive until this + // one is registered. + UniquePtr<SimpleWorkerHolder> holder; + + WorkerPrivate* parent = NS_IsMainThread() ? + nullptr : + GetCurrentThreadWorkerPrivate(); + if (parent) { + parent->AssertIsOnWorkerThread(); + + holder.reset(new SimpleWorkerHolder()); + if (!holder->HoldWorker(parent, Canceling)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + } else { + AssertIsOnMainThread(); + } + + // Only service and shared workers can have names. + MOZ_ASSERT_IF(aWorkerType != WorkerTypeDedicated, + !aWorkerName.IsVoid()); + MOZ_ASSERT_IF(aWorkerType == WorkerTypeDedicated, + aWorkerName.IsEmpty()); + + Maybe<WorkerLoadInfo> stackLoadInfo; + if (!aLoadInfo) { + stackLoadInfo.emplace(); + + nsresult rv = GetLoadInfo(aCx, nullptr, parent, aScriptURL, + aIsChromeWorker, InheritLoadGroup, + aWorkerType, stackLoadInfo.ptr()); + aRv.MightThrowJSException(); + if (NS_FAILED(rv)) { + scriptloader::ReportLoadError(aRv, rv, aScriptURL); + return nullptr; + } + + aLoadInfo = stackLoadInfo.ptr(); + } + + // NB: This has to be done before creating the WorkerPrivate, because it will + // attempt to use static variables that are initialized in the RuntimeService + // constructor. + RuntimeService* runtimeService; + + if (!parent) { + runtimeService = RuntimeService::GetOrCreateService(); + if (!runtimeService) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + } + else { + runtimeService = RuntimeService::GetService(); + } + + MOZ_ASSERT(runtimeService); + + RefPtr<WorkerPrivate> worker = + new WorkerPrivate(parent, aScriptURL, aIsChromeWorker, + aWorkerType, aWorkerName, *aLoadInfo); + + // Gecko contexts always have an explicitly-set default locale (set by + // XPJSRuntime::Initialize for the main thread, set by + // WorkerThreadPrimaryRunnable::Run for workers just before running worker + // code), so this is never SpiderMonkey's builtin default locale. + JS::UniqueChars defaultLocale = JS_GetDefaultLocale(aCx); + if (NS_WARN_IF(!defaultLocale)) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + worker->mDefaultLocale = Move(defaultLocale); + + if (!runtimeService->RegisterWorker(worker)) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + worker->EnableDebugger(); + + RefPtr<CompileScriptRunnable> compiler = + new CompileScriptRunnable(worker, aScriptURL); + if (!compiler->Dispatch()) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + worker->mSelfRef = worker; + + return worker.forget(); +} + +// static +nsresult +WorkerPrivate::GetLoadInfo(JSContext* aCx, nsPIDOMWindowInner* aWindow, + WorkerPrivate* aParent, const nsAString& aScriptURL, + bool aIsChromeWorker, + LoadGroupBehavior aLoadGroupBehavior, + WorkerType aWorkerType, + WorkerLoadInfo* aLoadInfo) +{ + using namespace mozilla::dom::workers::scriptloader; + + MOZ_ASSERT(aCx); + MOZ_ASSERT_IF(NS_IsMainThread(), aCx == nsContentUtils::GetCurrentJSContext()); + + if (aWindow) { + AssertIsOnMainThread(); + } + + WorkerLoadInfo loadInfo; + nsresult rv; + + if (aParent) { + aParent->AssertIsOnWorkerThread(); + + // If the parent is going away give up now. + Status parentStatus; + { + MutexAutoLock lock(aParent->mMutex); + parentStatus = aParent->mStatus; + } + + if (parentStatus > Running) { + return NS_ERROR_FAILURE; + } + + // StartAssignment() is used instead getter_AddRefs because, getter_AddRefs + // does QI in debug build and, if this worker runs in a child process, + // HttpChannelChild will crash because it's not thread-safe. + rv = ChannelFromScriptURLWorkerThread(aCx, aParent, aScriptURL, + loadInfo.mChannel.StartAssignment()); + NS_ENSURE_SUCCESS(rv, rv); + + // Now that we've spun the loop there's no guarantee that our parent is + // still alive. We may have received control messages initiating shutdown. + { + MutexAutoLock lock(aParent->mMutex); + parentStatus = aParent->mStatus; + } + + if (parentStatus > Running) { + NS_ReleaseOnMainThread(loadInfo.mChannel.forget()); + return NS_ERROR_FAILURE; + } + + loadInfo.mDomain = aParent->Domain(); + loadInfo.mFromWindow = aParent->IsFromWindow(); + loadInfo.mWindowID = aParent->WindowID(); + loadInfo.mStorageAllowed = aParent->IsStorageAllowed(); + loadInfo.mOriginAttributes = aParent->GetOriginAttributes(); + loadInfo.mServiceWorkersTestingInWindow = + aParent->ServiceWorkersTestingInWindow(); + } else { + AssertIsOnMainThread(); + + // Make sure that the IndexedDatabaseManager is set up + Unused << NS_WARN_IF(!IndexedDatabaseManager::GetOrCreate()); + + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + MOZ_ASSERT(ssm); + + bool isChrome = nsContentUtils::IsCallerChrome(); + + // First check to make sure the caller has permission to make a privileged + // worker if they called the ChromeWorker/ChromeSharedWorker constructor. + if (aIsChromeWorker && !isChrome) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + // Chrome callers (whether creating a ChromeWorker or Worker) always get the + // system principal here as they're allowed to load anything. The script + // loader will refuse to run any script that does not also have the system + // principal. + if (isChrome) { + rv = ssm->GetSystemPrincipal(getter_AddRefs(loadInfo.mPrincipal)); + NS_ENSURE_SUCCESS(rv, rv); + + loadInfo.mPrincipalIsSystem = true; + } + + // See if we're being called from a window. + nsCOMPtr<nsPIDOMWindowInner> globalWindow = aWindow; + if (!globalWindow) { + nsCOMPtr<nsIScriptGlobalObject> scriptGlobal = + nsJSUtils::GetStaticScriptGlobal(JS::CurrentGlobalOrNull(aCx)); + if (scriptGlobal) { + globalWindow = do_QueryInterface(scriptGlobal); + MOZ_ASSERT(globalWindow); + } + } + + nsCOMPtr<nsIDocument> document; + + if (globalWindow) { + // Only use the current inner window, and only use it if the caller can + // access it. + if (nsPIDOMWindowOuter* outerWindow = globalWindow->GetOuterWindow()) { + loadInfo.mWindow = outerWindow->GetCurrentInnerWindow(); + // TODO: fix this for SharedWorkers with multiple documents (bug 1177935) + loadInfo.mServiceWorkersTestingInWindow = + outerWindow->GetServiceWorkersTestingEnabled(); + } + + if (!loadInfo.mWindow || + (globalWindow != loadInfo.mWindow && + !nsContentUtils::CanCallerAccess(loadInfo.mWindow))) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + nsCOMPtr<nsIScriptGlobalObject> sgo = do_QueryInterface(loadInfo.mWindow); + MOZ_ASSERT(sgo); + + loadInfo.mScriptContext = sgo->GetContext(); + NS_ENSURE_TRUE(loadInfo.mScriptContext, NS_ERROR_FAILURE); + + // If we're called from a window then we can dig out the principal and URI + // from the document. + document = loadInfo.mWindow->GetExtantDoc(); + NS_ENSURE_TRUE(document, NS_ERROR_FAILURE); + + loadInfo.mBaseURI = document->GetDocBaseURI(); + loadInfo.mLoadGroup = document->GetDocumentLoadGroup(); + + // Use the document's NodePrincipal as our principal if we're not being + // called from chrome. + if (!loadInfo.mPrincipal) { + loadInfo.mPrincipal = document->NodePrincipal(); + NS_ENSURE_TRUE(loadInfo.mPrincipal, NS_ERROR_FAILURE); + + // We use the document's base domain to limit the number of workers + // each domain can create. For sandboxed documents, we use the domain + // of their first non-sandboxed document, walking up until we find + // one. If we can't find one, we fall back to using the GUID of the + // null principal as the base domain. + if (document->GetSandboxFlags() & SANDBOXED_ORIGIN) { + nsCOMPtr<nsIDocument> tmpDoc = document; + do { + tmpDoc = tmpDoc->GetParentDocument(); + } while (tmpDoc && tmpDoc->GetSandboxFlags() & SANDBOXED_ORIGIN); + + if (tmpDoc) { + // There was an unsandboxed ancestor, yay! + nsCOMPtr<nsIPrincipal> tmpPrincipal = tmpDoc->NodePrincipal(); + rv = tmpPrincipal->GetBaseDomain(loadInfo.mDomain); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // No unsandboxed ancestor, use our GUID. + rv = loadInfo.mPrincipal->GetBaseDomain(loadInfo.mDomain); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + // Document creating the worker is not sandboxed. + rv = loadInfo.mPrincipal->GetBaseDomain(loadInfo.mDomain); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + nsCOMPtr<nsIPermissionManager> permMgr = + do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t perm; + rv = permMgr->TestPermissionFromPrincipal(loadInfo.mPrincipal, "systemXHR", + &perm); + NS_ENSURE_SUCCESS(rv, rv); + + loadInfo.mXHRParamsAllowed = perm == nsIPermissionManager::ALLOW_ACTION; + + loadInfo.mFromWindow = true; + loadInfo.mWindowID = globalWindow->WindowID(); + nsContentUtils::StorageAccess access = + nsContentUtils::StorageAllowedForWindow(globalWindow); + loadInfo.mStorageAllowed = access > nsContentUtils::StorageAccess::eDeny; + loadInfo.mOriginAttributes = nsContentUtils::GetOriginAttributes(document); + } else { + // Not a window + MOZ_ASSERT(isChrome); + + // We're being created outside of a window. Need to figure out the script + // that is creating us in order for us to use relative URIs later on. + JS::AutoFilename fileName; + if (JS::DescribeScriptedCaller(aCx, &fileName)) { + // In most cases, fileName is URI. In a few other cases + // (e.g. xpcshell), fileName is a file path. Ideally, we would + // prefer testing whether fileName parses as an URI and fallback + // to file path in case of error, but Windows file paths have + // the interesting property that they can be parsed as bogus + // URIs (e.g. C:/Windows/Tmp is interpreted as scheme "C", + // hostname "Windows", path "Tmp"), which defeats this algorithm. + // Therefore, we adopt the opposite convention. + nsCOMPtr<nsIFile> scriptFile = + do_CreateInstance("@mozilla.org/file/local;1", &rv); + if (NS_FAILED(rv)) { + return rv; + } + + rv = scriptFile->InitWithPath(NS_ConvertUTF8toUTF16(fileName.get())); + if (NS_SUCCEEDED(rv)) { + rv = NS_NewFileURI(getter_AddRefs(loadInfo.mBaseURI), + scriptFile); + } + if (NS_FAILED(rv)) { + // As expected, fileName is not a path, so proceed with + // a uri. + rv = NS_NewURI(getter_AddRefs(loadInfo.mBaseURI), + fileName.get()); + } + if (NS_FAILED(rv)) { + return rv; + } + } + loadInfo.mXHRParamsAllowed = true; + loadInfo.mFromWindow = false; + loadInfo.mWindowID = UINT64_MAX; + loadInfo.mStorageAllowed = true; + loadInfo.mOriginAttributes = PrincipalOriginAttributes(); + } + + MOZ_ASSERT(loadInfo.mPrincipal); + MOZ_ASSERT(isChrome || !loadInfo.mDomain.IsEmpty()); + + if (!loadInfo.mLoadGroup || aLoadGroupBehavior == OverrideLoadGroup) { + OverrideLoadInfoLoadGroup(loadInfo); + } + MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(loadInfo.mLoadGroup, + loadInfo.mPrincipal)); + + // Top level workers' main script use the document charset for the script + // uri encoding. + bool useDefaultEncoding = false; + rv = ChannelFromScriptURLMainThread(loadInfo.mPrincipal, loadInfo.mBaseURI, + document, loadInfo.mLoadGroup, + aScriptURL, + ContentPolicyType(aWorkerType), + useDefaultEncoding, + getter_AddRefs(loadInfo.mChannel)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_GetFinalChannelURI(loadInfo.mChannel, + getter_AddRefs(loadInfo.mResolvedScriptURI)); + NS_ENSURE_SUCCESS(rv, rv); + } + + aLoadInfo->StealFrom(loadInfo); + return NS_OK; +} + +// static +void +WorkerPrivate::OverrideLoadInfoLoadGroup(WorkerLoadInfo& aLoadInfo) +{ + MOZ_ASSERT(!aLoadInfo.mInterfaceRequestor); + + aLoadInfo.mInterfaceRequestor = + new WorkerLoadInfo::InterfaceRequestor(aLoadInfo.mPrincipal, + aLoadInfo.mLoadGroup); + aLoadInfo.mInterfaceRequestor->MaybeAddTabChild(aLoadInfo.mLoadGroup); + + // NOTE: this defaults the load context to: + // - private browsing = false + // - content = true + // - use remote tabs = false + nsCOMPtr<nsILoadGroup> loadGroup = + do_CreateInstance(NS_LOADGROUP_CONTRACTID); + + nsresult rv = + loadGroup->SetNotificationCallbacks(aLoadInfo.mInterfaceRequestor); + MOZ_ALWAYS_SUCCEEDS(rv); + + aLoadInfo.mLoadGroup = loadGroup.forget(); +} + +void +WorkerPrivate::DoRunLoop(JSContext* aCx) +{ + AssertIsOnWorkerThread(); + MOZ_ASSERT(mThread); + + { + MutexAutoLock lock(mMutex); + mJSContext = aCx; + + MOZ_ASSERT(mStatus == Pending); + mStatus = Running; + } + + // Now that we've done that, we can go ahead and set up our AutoJSAPI. We + // can't before this point, because it can't find the right JSContext before + // then, since it gets it from our mJSContext. + AutoJSAPI jsapi; + jsapi.Init(); + MOZ_ASSERT(jsapi.cx() == aCx); + + EnableMemoryReporter(); + + InitializeGCTimers(); + + Maybe<JSAutoCompartment> workerCompartment; + + for (;;) { + Status currentStatus, previousStatus; + bool debuggerRunnablesPending = false; + bool normalRunnablesPending = false; + + { + MutexAutoLock lock(mMutex); + previousStatus = mStatus; + + while (mControlQueue.IsEmpty() && + !(debuggerRunnablesPending = !mDebuggerQueue.IsEmpty()) && + !(normalRunnablesPending = NS_HasPendingEvents(mThread))) { + WaitForWorkerEvents(); + } + + auto result = ProcessAllControlRunnablesLocked(); + if (result != ProcessAllControlRunnablesResult::Nothing) { + // NB: There's no JS on the stack here, so Abort vs MayContinue is + // irrelevant + + // The state of the world may have changed, recheck it. + normalRunnablesPending = NS_HasPendingEvents(mThread); + // The debugger queue doesn't get cleared, so we can ignore that. + } + + currentStatus = mStatus; + } + + // if all holders are done then we can kill this thread. + if (currentStatus != Running && !HasActiveHolders()) { + + // If we just changed status, we must schedule the current runnables. + if (previousStatus != Running && currentStatus != Killing) { + NotifyInternal(aCx, Killing); + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + +#ifdef DEBUG + { + MutexAutoLock lock(mMutex); + currentStatus = mStatus; + } + MOZ_ASSERT(currentStatus == Killing); +#else + currentStatus = Killing; +#endif + } + + // If we're supposed to die then we should exit the loop. + if (currentStatus == Killing) { + // Flush uncaught rejections immediately, without + // waiting for a next tick. + PromiseDebugging::FlushUncaughtRejections(); + + ShutdownGCTimers(); + + DisableMemoryReporter(); + + { + MutexAutoLock lock(mMutex); + + mStatus = Dead; + mJSContext = nullptr; + } + + // After mStatus is set to Dead there can be no more + // WorkerControlRunnables so no need to lock here. + if (!mControlQueue.IsEmpty()) { + WorkerControlRunnable* runnable; + while (mControlQueue.Pop(runnable)) { + runnable->Cancel(); + runnable->Release(); + } + } + + // Unroot the globals + mScope = nullptr; + mDebuggerScope = nullptr; + + return; + } + } + + if (debuggerRunnablesPending || normalRunnablesPending) { + // Start the periodic GC timer if it is not already running. + SetGCTimerMode(PeriodicTimer); + } + + if (debuggerRunnablesPending) { + WorkerRunnable* runnable; + + { + MutexAutoLock lock(mMutex); + + mDebuggerQueue.Pop(runnable); + debuggerRunnablesPending = !mDebuggerQueue.IsEmpty(); + } + + MOZ_ASSERT(runnable); + static_cast<nsIRunnable*>(runnable)->Run(); + runnable->Release(); + + // Flush the promise queue. + Promise::PerformWorkerDebuggerMicroTaskCheckpoint(); + + if (debuggerRunnablesPending) { + WorkerDebuggerGlobalScope* globalScope = DebuggerGlobalScope(); + MOZ_ASSERT(globalScope); + + // Now *might* be a good time to GC. Let the JS engine make the decision. + JSAutoCompartment ac(aCx, globalScope->GetGlobalJSObject()); + JS_MaybeGC(aCx); + } + } else if (normalRunnablesPending) { + // Process a single runnable from the main queue. + NS_ProcessNextEvent(mThread, false); + + normalRunnablesPending = NS_HasPendingEvents(mThread); + if (normalRunnablesPending && GlobalScope()) { + // Now *might* be a good time to GC. Let the JS engine make the decision. + JSAutoCompartment ac(aCx, GlobalScope()->GetGlobalJSObject()); + JS_MaybeGC(aCx); + } + } + + if (!debuggerRunnablesPending && !normalRunnablesPending) { + // Both the debugger event queue and the normal event queue has been + // exhausted, cancel the periodic GC timer and schedule the idle GC timer. + SetGCTimerMode(IdleTimer); + } + + // If the worker thread is spamming the main thread faster than it can + // process the work, then pause the worker thread until the MT catches + // up. + if (mMainThreadThrottledEventQueue && + mMainThreadThrottledEventQueue->Length() > 5000) { + mMainThreadThrottledEventQueue->AwaitIdle(); + } + } + + MOZ_CRASH("Shouldn't get here!"); +} + +void +WorkerPrivate::OnProcessNextEvent() +{ + AssertIsOnWorkerThread(); + + uint32_t recursionDepth = CycleCollectedJSContext::Get()->RecursionDepth(); + MOZ_ASSERT(recursionDepth); + + // Normally we process control runnables in DoRunLoop or RunCurrentSyncLoop. + // However, it's possible that non-worker C++ could spin its own nested event + // loop, and in that case we must ensure that we continue to process control + // runnables here. + if (recursionDepth > 1 && + mSyncLoopStack.Length() < recursionDepth - 1) { + Unused << ProcessAllControlRunnables(); + // There's no running JS, and no state to revalidate, so we can ignore the + // return value. + } +} + +void +WorkerPrivate::AfterProcessNextEvent() +{ + AssertIsOnWorkerThread(); + MOZ_ASSERT(CycleCollectedJSContext::Get()->RecursionDepth()); +} + +void +WorkerPrivate::MaybeDispatchLoadFailedRunnable() +{ + AssertIsOnWorkerThread(); + + nsCOMPtr<nsIRunnable> runnable = StealLoadFailedAsyncRunnable(); + if (!runnable) { + return; + } + + MOZ_ALWAYS_SUCCEEDS(DispatchToMainThread(runnable.forget())); +} + +nsIEventTarget* +WorkerPrivate::MainThreadEventTarget() +{ + return mMainThreadEventTarget; +} + +nsresult +WorkerPrivate::DispatchToMainThread(nsIRunnable* aRunnable, uint32_t aFlags) +{ + nsCOMPtr<nsIRunnable> r = aRunnable; + return DispatchToMainThread(r.forget(), aFlags); +} + +nsresult +WorkerPrivate::DispatchToMainThread(already_AddRefed<nsIRunnable> aRunnable, + uint32_t aFlags) +{ + return mMainThreadEventTarget->Dispatch(Move(aRunnable), aFlags); +} + +void +WorkerPrivate::InitializeGCTimers() +{ + AssertIsOnWorkerThread(); + + // We need a timer for GC. The basic plan is to run a non-shrinking GC + // periodically (PERIODIC_GC_TIMER_DELAY_SEC) while the worker is running. + // Once the worker goes idle we set a short (IDLE_GC_TIMER_DELAY_SEC) timer to + // run a shrinking GC. If the worker receives more messages then the short + // timer is canceled and the periodic timer resumes. + mGCTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + MOZ_ASSERT(mGCTimer); + + RefPtr<GarbageCollectRunnable> runnable = + new GarbageCollectRunnable(this, false, false); + mPeriodicGCTimerTarget = new TimerThreadEventTarget(this, runnable); + + runnable = new GarbageCollectRunnable(this, true, false); + mIdleGCTimerTarget = new TimerThreadEventTarget(this, runnable); + + mPeriodicGCTimerRunning = false; + mIdleGCTimerRunning = false; +} + +void +WorkerPrivate::SetGCTimerMode(GCTimerMode aMode) +{ + AssertIsOnWorkerThread(); + MOZ_ASSERT(mGCTimer); + MOZ_ASSERT(mPeriodicGCTimerTarget); + MOZ_ASSERT(mIdleGCTimerTarget); + + if ((aMode == PeriodicTimer && mPeriodicGCTimerRunning) || + (aMode == IdleTimer && mIdleGCTimerRunning)) { + return; + } + + MOZ_ALWAYS_SUCCEEDS(mGCTimer->Cancel()); + + mPeriodicGCTimerRunning = false; + mIdleGCTimerRunning = false; + LOG(WorkerLog(), + ("Worker %p canceled GC timer because %s\n", this, + aMode == PeriodicTimer ? + "periodic" : + aMode == IdleTimer ? "idle" : "none")); + + if (aMode == NoTimer) { + return; + } + + MOZ_ASSERT(aMode == PeriodicTimer || aMode == IdleTimer); + + nsIEventTarget* target; + uint32_t delay; + int16_t type; + + if (aMode == PeriodicTimer) { + target = mPeriodicGCTimerTarget; + delay = PERIODIC_GC_TIMER_DELAY_SEC * 1000; + type = nsITimer::TYPE_REPEATING_SLACK; + } + else { + target = mIdleGCTimerTarget; + delay = IDLE_GC_TIMER_DELAY_SEC * 1000; + type = nsITimer::TYPE_ONE_SHOT; + } + + MOZ_ALWAYS_SUCCEEDS(mGCTimer->SetTarget(target)); + MOZ_ALWAYS_SUCCEEDS( + mGCTimer->InitWithNamedFuncCallback(DummyCallback, nullptr, delay, type, + "dom::workers::DummyCallback(2)")); + + if (aMode == PeriodicTimer) { + LOG(WorkerLog(), ("Worker %p scheduled periodic GC timer\n", this)); + mPeriodicGCTimerRunning = true; + } + else { + LOG(WorkerLog(), ("Worker %p scheduled idle GC timer\n", this)); + mIdleGCTimerRunning = true; + } +} + +void +WorkerPrivate::ShutdownGCTimers() +{ + AssertIsOnWorkerThread(); + + MOZ_ASSERT(mGCTimer); + + // Always make sure the timer is canceled. + MOZ_ALWAYS_SUCCEEDS(mGCTimer->Cancel()); + + LOG(WorkerLog(), ("Worker %p killed the GC timer\n", this)); + + mGCTimer = nullptr; + mPeriodicGCTimerTarget = nullptr; + mIdleGCTimerTarget = nullptr; + mPeriodicGCTimerRunning = false; + mIdleGCTimerRunning = false; +} + +bool +WorkerPrivate::InterruptCallback(JSContext* aCx) +{ + AssertIsOnWorkerThread(); + + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + + bool mayContinue = true; + bool scheduledIdleGC = false; + + for (;;) { + // Run all control events now. + auto result = ProcessAllControlRunnables(); + if (result == ProcessAllControlRunnablesResult::Abort) { + mayContinue = false; + } + + bool mayFreeze = mFrozen; + if (mayFreeze) { + MutexAutoLock lock(mMutex); + mayFreeze = mStatus <= Running; + } + + if (!mayContinue || !mayFreeze) { + break; + } + + // Cancel the periodic GC timer here before freezing. The idle GC timer + // will clean everything up once it runs. + if (!scheduledIdleGC) { + SetGCTimerMode(IdleTimer); + scheduledIdleGC = true; + } + + while ((mayContinue = MayContinueRunning())) { + MutexAutoLock lock(mMutex); + if (!mControlQueue.IsEmpty()) { + break; + } + + WaitForWorkerEvents(PR_MillisecondsToInterval(UINT32_MAX)); + } + } + + if (!mayContinue) { + // We want only uncatchable exceptions here. + NS_ASSERTION(!JS_IsExceptionPending(aCx), + "Should not have an exception set here!"); + return false; + } + + // Make sure the periodic timer gets turned back on here. + SetGCTimerMode(PeriodicTimer); + + return true; +} + +nsresult +WorkerPrivate::IsOnCurrentThread(bool* aIsOnCurrentThread) +{ + // May be called on any thread! + + MOZ_ASSERT(aIsOnCurrentThread); + MOZ_ASSERT(mPRThread); + + *aIsOnCurrentThread = PR_GetCurrentThread() == mPRThread; + return NS_OK; +} + +void +WorkerPrivate::ScheduleDeletion(WorkerRanOrNot aRanOrNot) +{ + AssertIsOnWorkerThread(); + MOZ_ASSERT(mChildWorkers.IsEmpty()); + MOZ_ASSERT(mSyncLoopStack.IsEmpty()); + MOZ_ASSERT(!mPendingEventQueueClearing); + + ClearMainEventQueue(aRanOrNot); +#ifdef DEBUG + if (WorkerRan == aRanOrNot) { + nsIThread* currentThread = NS_GetCurrentThread(); + MOZ_ASSERT(currentThread); + MOZ_ASSERT(!NS_HasPendingEvents(currentThread)); + } +#endif + + if (WorkerPrivate* parent = GetParent()) { + RefPtr<WorkerFinishedRunnable> runnable = + new WorkerFinishedRunnable(parent, this); + if (!runnable->Dispatch()) { + NS_WARNING("Failed to dispatch runnable!"); + } + } + else { + RefPtr<TopLevelWorkerFinishedRunnable> runnable = + new TopLevelWorkerFinishedRunnable(this); + if (NS_FAILED(DispatchToMainThread(runnable.forget()))) { + NS_WARNING("Failed to dispatch runnable!"); + } + } +} + +bool +WorkerPrivate::CollectRuntimeStats(JS::RuntimeStats* aRtStats, + bool aAnonymize) +{ + AssertIsOnWorkerThread(); + NS_ASSERTION(aRtStats, "Null RuntimeStats!"); + NS_ASSERTION(mJSContext, "This must never be null!"); + + return JS::CollectRuntimeStats(mJSContext, aRtStats, nullptr, aAnonymize); +} + +void +WorkerPrivate::EnableMemoryReporter() +{ + AssertIsOnWorkerThread(); + MOZ_ASSERT(!mMemoryReporter); + + // No need to lock here since the main thread can't race until we've + // successfully registered the reporter. + mMemoryReporter = new MemoryReporter(this); + + if (NS_FAILED(RegisterWeakAsyncMemoryReporter(mMemoryReporter))) { + NS_WARNING("Failed to register memory reporter!"); + // No need to lock here since a failed registration means our memory + // reporter can't start running. Just clean up. + mMemoryReporter = nullptr; + } +} + +void +WorkerPrivate::DisableMemoryReporter() +{ + AssertIsOnWorkerThread(); + + RefPtr<MemoryReporter> memoryReporter; + { + // Mutex protectes MemoryReporter::mWorkerPrivate which is cleared by + // MemoryReporter::Disable() below. + MutexAutoLock lock(mMutex); + + // There is nothing to do here if the memory reporter was never successfully + // registered. + if (!mMemoryReporter) { + return; + } + + // We don't need this set any longer. Swap it out so that we can unregister + // below. + mMemoryReporter.swap(memoryReporter); + + // Next disable the memory reporter so that the main thread stops trying to + // signal us. + memoryReporter->Disable(); + } + + // Finally unregister the memory reporter. + if (NS_FAILED(UnregisterWeakMemoryReporter(memoryReporter))) { + NS_WARNING("Failed to unregister memory reporter!"); + } +} + +void +WorkerPrivate::WaitForWorkerEvents(PRIntervalTime aInterval) +{ + AssertIsOnWorkerThread(); + mMutex.AssertCurrentThreadOwns(); + + // Wait for a worker event. + mCondVar.Wait(aInterval); +} + +WorkerPrivate::ProcessAllControlRunnablesResult +WorkerPrivate::ProcessAllControlRunnablesLocked() +{ + AssertIsOnWorkerThread(); + mMutex.AssertCurrentThreadOwns(); + + auto result = ProcessAllControlRunnablesResult::Nothing; + + for (;;) { + WorkerControlRunnable* event; + if (!mControlQueue.Pop(event)) { + break; + } + + MutexAutoUnlock unlock(mMutex); + + MOZ_ASSERT(event); + if (NS_FAILED(static_cast<nsIRunnable*>(event)->Run())) { + result = ProcessAllControlRunnablesResult::Abort; + } + + if (result == ProcessAllControlRunnablesResult::Nothing) { + // We ran at least one thing. + result = ProcessAllControlRunnablesResult::MayContinue; + } + event->Release(); + } + + return result; +} + +void +WorkerPrivate::ClearMainEventQueue(WorkerRanOrNot aRanOrNot) +{ + AssertIsOnWorkerThread(); + + MOZ_ASSERT(mSyncLoopStack.IsEmpty()); + MOZ_ASSERT(!mCancelAllPendingRunnables); + mCancelAllPendingRunnables = true; + + if (WorkerNeverRan == aRanOrNot) { + for (uint32_t count = mPreStartRunnables.Length(), index = 0; + index < count; + index++) { + RefPtr<WorkerRunnable> runnable = mPreStartRunnables[index].forget(); + static_cast<nsIRunnable*>(runnable.get())->Run(); + } + } else { + nsIThread* currentThread = NS_GetCurrentThread(); + MOZ_ASSERT(currentThread); + + NS_ProcessPendingEvents(currentThread); + } + + MOZ_ASSERT(mCancelAllPendingRunnables); + mCancelAllPendingRunnables = false; +} + +void +WorkerPrivate::ClearDebuggerEventQueue() +{ + while (!mDebuggerQueue.IsEmpty()) { + WorkerRunnable* runnable; + mDebuggerQueue.Pop(runnable); + // It should be ok to simply release the runnable, without running it. + runnable->Release(); + } +} + +bool +WorkerPrivate::FreezeInternal() +{ + AssertIsOnWorkerThread(); + + NS_ASSERTION(!mFrozen, "Already frozen!"); + + mFrozen = true; + + for (uint32_t index = 0; index < mChildWorkers.Length(); index++) { + mChildWorkers[index]->Freeze(nullptr); + } + + return true; +} + +bool +WorkerPrivate::ThawInternal() +{ + AssertIsOnWorkerThread(); + + NS_ASSERTION(mFrozen, "Not yet frozen!"); + + for (uint32_t index = 0; index < mChildWorkers.Length(); index++) { + mChildWorkers[index]->Thaw(nullptr); + } + + mFrozen = false; + return true; +} + +void +WorkerPrivate::TraverseTimeouts(nsCycleCollectionTraversalCallback& cb) +{ + for (uint32_t i = 0; i < mTimeouts.Length(); ++i) { + TimeoutInfo* tmp = mTimeouts[i]; + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHandler) + } +} + +void +WorkerPrivate::UnlinkTimeouts() +{ + mTimeouts.Clear(); +} + +bool +WorkerPrivate::ModifyBusyCountFromWorker(bool aIncrease) +{ + AssertIsOnWorkerThread(); + + { + MutexAutoLock lock(mMutex); + + // If we're in shutdown then the busy count is no longer being considered so + // just return now. + if (mStatus >= Killing) { + return true; + } + } + + RefPtr<ModifyBusyCountRunnable> runnable = + new ModifyBusyCountRunnable(this, aIncrease); + return runnable->Dispatch(); +} + +bool +WorkerPrivate::AddChildWorker(ParentType* aChildWorker) +{ + AssertIsOnWorkerThread(); + +#ifdef DEBUG + { + Status currentStatus; + { + MutexAutoLock lock(mMutex); + currentStatus = mStatus; + } + + MOZ_ASSERT(currentStatus == Running); + } +#endif + + NS_ASSERTION(!mChildWorkers.Contains(aChildWorker), + "Already know about this one!"); + mChildWorkers.AppendElement(aChildWorker); + + return mChildWorkers.Length() == 1 ? + ModifyBusyCountFromWorker(true) : + true; +} + +void +WorkerPrivate::RemoveChildWorker(ParentType* aChildWorker) +{ + AssertIsOnWorkerThread(); + + NS_ASSERTION(mChildWorkers.Contains(aChildWorker), + "Didn't know about this one!"); + mChildWorkers.RemoveElement(aChildWorker); + + if (mChildWorkers.IsEmpty() && !ModifyBusyCountFromWorker(false)) { + NS_WARNING("Failed to modify busy count!"); + } +} + +bool +WorkerPrivate::AddHolder(WorkerHolder* aHolder, Status aFailStatus) +{ + AssertIsOnWorkerThread(); + + { + MutexAutoLock lock(mMutex); + + if (mStatus >= aFailStatus) { + return false; + } + } + + MOZ_ASSERT(!mHolders.Contains(aHolder), "Already know about this one!"); + + if (mHolders.IsEmpty() && !ModifyBusyCountFromWorker(true)) { + return false; + } + + mHolders.AppendElement(aHolder); + return true; +} + +void +WorkerPrivate::RemoveHolder(WorkerHolder* aHolder) +{ + AssertIsOnWorkerThread(); + + MOZ_ASSERT(mHolders.Contains(aHolder), "Didn't know about this one!"); + mHolders.RemoveElement(aHolder); + + if (mHolders.IsEmpty() && !ModifyBusyCountFromWorker(false)) { + NS_WARNING("Failed to modify busy count!"); + } +} + +void +WorkerPrivate::NotifyHolders(JSContext* aCx, Status aStatus) +{ + AssertIsOnWorkerThread(); + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + + NS_ASSERTION(aStatus > Running, "Bad status!"); + + if (aStatus >= Closing) { + CancelAllTimeouts(); + } + + nsTObserverArray<WorkerHolder*>::ForwardIterator iter(mHolders); + while (iter.HasMore()) { + WorkerHolder* holder = iter.GetNext(); + if (!holder->Notify(aStatus)) { + NS_WARNING("Failed to notify holder!"); + } + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + } + + AutoTArray<ParentType*, 10> children; + children.AppendElements(mChildWorkers); + + for (uint32_t index = 0; index < children.Length(); index++) { + if (!children[index]->Notify(aStatus)) { + NS_WARNING("Failed to notify child worker!"); + } + } +} + +void +WorkerPrivate::CancelAllTimeouts() +{ + AssertIsOnWorkerThread(); + + LOG(TimeoutsLog(), ("Worker %p CancelAllTimeouts.\n", this)); + + if (mTimerRunning) { + NS_ASSERTION(mTimer && mTimerRunnable, "Huh?!"); + NS_ASSERTION(!mTimeouts.IsEmpty(), "Huh?!"); + + if (NS_FAILED(mTimer->Cancel())) { + NS_WARNING("Failed to cancel timer!"); + } + + for (uint32_t index = 0; index < mTimeouts.Length(); index++) { + mTimeouts[index]->mCanceled = true; + } + + // If mRunningExpiredTimeouts, then the fact that they are all canceled now + // means that the currently executing RunExpiredTimeouts will deal with + // them. Otherwise, we need to clean them up ourselves. + if (!mRunningExpiredTimeouts) { + mTimeouts.Clear(); + ModifyBusyCountFromWorker(false); + } + + // Set mTimerRunning false even if mRunningExpiredTimeouts is true, so that + // if we get reentered under this same RunExpiredTimeouts call we don't + // assert above that !mTimeouts().IsEmpty(), because that's clearly false + // now. + mTimerRunning = false; + } +#ifdef DEBUG + else if (!mRunningExpiredTimeouts) { + NS_ASSERTION(mTimeouts.IsEmpty(), "Huh?!"); + } +#endif + + mTimer = nullptr; + mTimerRunnable = nullptr; +} + +already_AddRefed<nsIEventTarget> +WorkerPrivate::CreateNewSyncLoop() +{ + AssertIsOnWorkerThread(); + + nsCOMPtr<nsIThreadInternal> thread = do_QueryInterface(NS_GetCurrentThread()); + MOZ_ASSERT(thread); + + nsCOMPtr<nsIEventTarget> realEventTarget; + MOZ_ALWAYS_SUCCEEDS(thread->PushEventQueue(getter_AddRefs(realEventTarget))); + + RefPtr<EventTarget> workerEventTarget = + new EventTarget(this, realEventTarget); + + { + // Modifications must be protected by mMutex in DEBUG builds, see comment + // about mSyncLoopStack in WorkerPrivate.h. +#ifdef DEBUG + MutexAutoLock lock(mMutex); +#endif + + mSyncLoopStack.AppendElement(new SyncLoopInfo(workerEventTarget)); + } + + return workerEventTarget.forget(); +} + +bool +WorkerPrivate::RunCurrentSyncLoop() +{ + AssertIsOnWorkerThread(); + + JSContext* cx = GetJSContext(); + MOZ_ASSERT(cx); + + // This should not change between now and the time we finish running this sync + // loop. + uint32_t currentLoopIndex = mSyncLoopStack.Length() - 1; + + SyncLoopInfo* loopInfo = mSyncLoopStack[currentLoopIndex]; + + MOZ_ASSERT(loopInfo); + MOZ_ASSERT(!loopInfo->mHasRun); + MOZ_ASSERT(!loopInfo->mCompleted); + +#ifdef DEBUG + loopInfo->mHasRun = true; +#endif + + while (!loopInfo->mCompleted) { + bool normalRunnablesPending = false; + + // Don't block with the periodic GC timer running. + if (!NS_HasPendingEvents(mThread)) { + SetGCTimerMode(IdleTimer); + } + + // Wait for something to do. + { + MutexAutoLock lock(mMutex); + + for (;;) { + while (mControlQueue.IsEmpty() && + !normalRunnablesPending && + !(normalRunnablesPending = NS_HasPendingEvents(mThread))) { + WaitForWorkerEvents(); + } + + auto result = ProcessAllControlRunnablesLocked(); + if (result != ProcessAllControlRunnablesResult::Nothing) { + // XXXkhuey how should we handle Abort here? See Bug 1003730. + + // The state of the world may have changed. Recheck it. + normalRunnablesPending = NS_HasPendingEvents(mThread); + + // NB: If we processed a NotifyRunnable, we might have run + // non-control runnables, one of which may have shut down the + // sync loop. + if (loopInfo->mCompleted) { + break; + } + } + + // If we *didn't* run any control runnables, this should be unchanged. + MOZ_ASSERT(!loopInfo->mCompleted); + + if (normalRunnablesPending) { + break; + } + } + } + + if (normalRunnablesPending) { + // Make sure the periodic timer is running before we continue. + SetGCTimerMode(PeriodicTimer); + + MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(mThread, false)); + + // Now *might* be a good time to GC. Let the JS engine make the decision. + if (JS::CurrentGlobalOrNull(cx)) { + JS_MaybeGC(cx); + } + } + } + + // Make sure that the stack didn't change underneath us. + MOZ_ASSERT(mSyncLoopStack[currentLoopIndex] == loopInfo); + + return DestroySyncLoop(currentLoopIndex); +} + +bool +WorkerPrivate::DestroySyncLoop(uint32_t aLoopIndex, nsIThreadInternal* aThread) +{ + MOZ_ASSERT(!mSyncLoopStack.IsEmpty()); + MOZ_ASSERT(mSyncLoopStack.Length() - 1 == aLoopIndex); + + if (!aThread) { + aThread = mThread; + } + + // We're about to delete the loop, stash its event target and result. + SyncLoopInfo* loopInfo = mSyncLoopStack[aLoopIndex]; + nsIEventTarget* nestedEventTarget = + loopInfo->mEventTarget->GetWeakNestedEventTarget(); + MOZ_ASSERT(nestedEventTarget); + + bool result = loopInfo->mResult; + + { + // Modifications must be protected by mMutex in DEBUG builds, see comment + // about mSyncLoopStack in WorkerPrivate.h. +#ifdef DEBUG + MutexAutoLock lock(mMutex); +#endif + + // This will delete |loopInfo|! + mSyncLoopStack.RemoveElementAt(aLoopIndex); + } + + MOZ_ALWAYS_SUCCEEDS(aThread->PopEventQueue(nestedEventTarget)); + + if (mSyncLoopStack.IsEmpty() && mPendingEventQueueClearing) { + mPendingEventQueueClearing = false; + ClearMainEventQueue(WorkerRan); + } + + return result; +} + +void +WorkerPrivate::StopSyncLoop(nsIEventTarget* aSyncLoopTarget, bool aResult) +{ + AssertIsOnWorkerThread(); + AssertValidSyncLoop(aSyncLoopTarget); + + MOZ_ASSERT(!mSyncLoopStack.IsEmpty()); + + for (uint32_t index = mSyncLoopStack.Length(); index > 0; index--) { + nsAutoPtr<SyncLoopInfo>& loopInfo = mSyncLoopStack[index - 1]; + MOZ_ASSERT(loopInfo); + MOZ_ASSERT(loopInfo->mEventTarget); + + if (loopInfo->mEventTarget == aSyncLoopTarget) { + // Can't assert |loop->mHasRun| here because dispatch failures can cause + // us to bail out early. + MOZ_ASSERT(!loopInfo->mCompleted); + + loopInfo->mResult = aResult; + loopInfo->mCompleted = true; + + loopInfo->mEventTarget->Disable(); + + return; + } + + MOZ_ASSERT(!SameCOMIdentity(loopInfo->mEventTarget, aSyncLoopTarget)); + } + + MOZ_CRASH("Unknown sync loop!"); +} + +#ifdef DEBUG +void +WorkerPrivate::AssertValidSyncLoop(nsIEventTarget* aSyncLoopTarget) +{ + MOZ_ASSERT(aSyncLoopTarget); + + EventTarget* workerTarget; + nsresult rv = + aSyncLoopTarget->QueryInterface(kDEBUGWorkerEventTargetIID, + reinterpret_cast<void**>(&workerTarget)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + MOZ_ASSERT(workerTarget); + + bool valid = false; + + { + MutexAutoLock lock(mMutex); + + for (uint32_t index = 0; index < mSyncLoopStack.Length(); index++) { + nsAutoPtr<SyncLoopInfo>& loopInfo = mSyncLoopStack[index]; + MOZ_ASSERT(loopInfo); + MOZ_ASSERT(loopInfo->mEventTarget); + + if (loopInfo->mEventTarget == aSyncLoopTarget) { + valid = true; + break; + } + + MOZ_ASSERT(!SameCOMIdentity(loopInfo->mEventTarget, aSyncLoopTarget)); + } + } + + MOZ_ASSERT(valid); +} +#endif + +void +WorkerPrivate::PostMessageToParentInternal( + JSContext* aCx, + JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + ErrorResult& aRv) +{ + AssertIsOnWorkerThread(); + + JS::Rooted<JS::Value> transferable(aCx, JS::UndefinedValue()); + if (aTransferable.WasPassed()) { + const Sequence<JS::Value>& realTransferable = aTransferable.Value(); + + // The input sequence only comes from the generated bindings code, which + // ensures it is rooted. + JS::HandleValueArray elements = + JS::HandleValueArray::fromMarkedLocation(realTransferable.Length(), + realTransferable.Elements()); + + JSObject* array = JS_NewArrayObject(aCx, elements); + if (!array) { + aRv = NS_ERROR_OUT_OF_MEMORY; + return; + } + transferable.setObject(*array); + } + + RefPtr<MessageEventRunnable> runnable = + new MessageEventRunnable(this, + WorkerRunnable::ParentThreadUnchangedBusyCount); + + UniquePtr<AbstractTimelineMarker> start; + UniquePtr<AbstractTimelineMarker> end; + RefPtr<TimelineConsumers> timelines = TimelineConsumers::Get(); + bool isTimelineRecording = timelines && !timelines->IsEmpty(); + + if (isTimelineRecording) { + start = MakeUnique<WorkerTimelineMarker>(NS_IsMainThread() + ? ProfileTimelineWorkerOperationType::SerializeDataOnMainThread + : ProfileTimelineWorkerOperationType::SerializeDataOffMainThread, + MarkerTracingType::START); + } + + runnable->Write(aCx, aMessage, transferable, JS::CloneDataPolicy(), aRv); + + if (isTimelineRecording) { + end = MakeUnique<WorkerTimelineMarker>(NS_IsMainThread() + ? ProfileTimelineWorkerOperationType::SerializeDataOnMainThread + : ProfileTimelineWorkerOperationType::SerializeDataOffMainThread, + MarkerTracingType::END); + timelines->AddMarkerForAllObservedDocShells(start); + timelines->AddMarkerForAllObservedDocShells(end); + } + + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + if (!runnable->Dispatch()) { + aRv = NS_ERROR_FAILURE; + } +} + +void +WorkerPrivate::EnterDebuggerEventLoop() +{ + AssertIsOnWorkerThread(); + + JSContext* cx = GetJSContext(); + MOZ_ASSERT(cx); + + uint32_t currentEventLoopLevel = ++mDebuggerEventLoopLevel; + + while (currentEventLoopLevel <= mDebuggerEventLoopLevel) { + bool debuggerRunnablesPending = false; + + { + MutexAutoLock lock(mMutex); + + debuggerRunnablesPending = !mDebuggerQueue.IsEmpty(); + } + + // Don't block with the periodic GC timer running. + if (!debuggerRunnablesPending) { + SetGCTimerMode(IdleTimer); + } + + // Wait for something to do + { + MutexAutoLock lock(mMutex); + + while (mControlQueue.IsEmpty() && + !(debuggerRunnablesPending = !mDebuggerQueue.IsEmpty())) { + WaitForWorkerEvents(); + } + + ProcessAllControlRunnablesLocked(); + + // XXXkhuey should we abort JS on the stack here if we got Abort above? + } + + if (debuggerRunnablesPending) { + // Start the periodic GC timer if it is not already running. + SetGCTimerMode(PeriodicTimer); + + WorkerRunnable* runnable; + + { + MutexAutoLock lock(mMutex); + + mDebuggerQueue.Pop(runnable); + } + + MOZ_ASSERT(runnable); + static_cast<nsIRunnable*>(runnable)->Run(); + runnable->Release(); + + // Flush the promise queue. + Promise::PerformWorkerDebuggerMicroTaskCheckpoint(); + + // Now *might* be a good time to GC. Let the JS engine make the decision. + if (JS::CurrentGlobalOrNull(cx)) { + JS_MaybeGC(cx); + } + } + } +} + +void +WorkerPrivate::LeaveDebuggerEventLoop() +{ + AssertIsOnWorkerThread(); + + MutexAutoLock lock(mMutex); + + if (mDebuggerEventLoopLevel > 0) { + --mDebuggerEventLoopLevel; + } +} + +void +WorkerPrivate::PostMessageToDebugger(const nsAString& aMessage) +{ + mDebugger->PostMessageToDebugger(aMessage); +} + +void +WorkerPrivate::SetDebuggerImmediate(dom::Function& aHandler, ErrorResult& aRv) +{ + AssertIsOnWorkerThread(); + + RefPtr<DebuggerImmediateRunnable> runnable = + new DebuggerImmediateRunnable(this, aHandler); + if (!runnable->Dispatch()) { + aRv.Throw(NS_ERROR_FAILURE); + } +} + +void +WorkerPrivate::ReportErrorToDebugger(const nsAString& aFilename, + uint32_t aLineno, + const nsAString& aMessage) +{ + mDebugger->ReportErrorToDebugger(aFilename, aLineno, aMessage); +} + +bool +WorkerPrivate::NotifyInternal(JSContext* aCx, Status aStatus) +{ + AssertIsOnWorkerThread(); + + NS_ASSERTION(aStatus > Running && aStatus < Dead, "Bad status!"); + + RefPtr<EventTarget> eventTarget; + + // Save the old status and set the new status. + Status previousStatus; + { + MutexAutoLock lock(mMutex); + + if (mStatus >= aStatus) { + MOZ_ASSERT(!mEventTarget); + return true; + } + + previousStatus = mStatus; + mStatus = aStatus; + + mEventTarget.swap(eventTarget); + } + + // Now that mStatus > Running, no-one can create a new WorkerEventTarget or + // WorkerCrossThreadDispatcher if we don't already have one. + if (eventTarget) { + // Since we'll no longer process events, make sure we no longer allow anyone + // to post them. We have to do this without mMutex held, since our mutex + // must be acquired *after* the WorkerEventTarget's mutex when they're both + // held. + eventTarget->Disable(); + eventTarget = nullptr; + } + + if (mCrossThreadDispatcher) { + // Since we'll no longer process events, make sure we no longer allow + // anyone to post them. We have to do this without mMutex held, since our + // mutex must be acquired *after* mCrossThreadDispatcher's mutex when + // they're both held. + mCrossThreadDispatcher->Forget(); + mCrossThreadDispatcher = nullptr; + } + + MOZ_ASSERT(previousStatus != Pending); + + // Let all our holders know the new status. + NotifyHolders(aCx, aStatus); + MOZ_ASSERT(!JS_IsExceptionPending(aCx)); + + // If this is the first time our status has changed then we need to clear the + // main event queue. + if (previousStatus == Running) { + // NB: If we're in a sync loop, we can't clear the queue immediately, + // because this is the wrong queue. So we have to defer it until later. + if (!mSyncLoopStack.IsEmpty()) { + mPendingEventQueueClearing = true; + } else { + ClearMainEventQueue(WorkerRan); + } + } + + // If the worker script never ran, or failed to compile, we don't need to do + // anything else. + if (!GlobalScope()) { + return true; + } + + if (aStatus == Closing) { + // Notify parent to stop sending us messages and balance our busy count. + RefPtr<CloseRunnable> runnable = new CloseRunnable(this); + if (!runnable->Dispatch()) { + return false; + } + + // Don't abort the script. + return true; + } + + MOZ_ASSERT(aStatus == Terminating || + aStatus == Canceling || + aStatus == Killing); + + // Always abort the script. + return false; +} + +void +WorkerPrivate::ReportError(JSContext* aCx, JS::ConstUTF8CharsZ aToStringResult, + JSErrorReport* aReport) +{ + AssertIsOnWorkerThread(); + + if (!MayContinueRunning() || mErrorHandlerRecursionCount == 2) { + return; + } + + NS_ASSERTION(mErrorHandlerRecursionCount == 0 || + mErrorHandlerRecursionCount == 1, + "Bad recursion logic!"); + + JS::Rooted<JS::Value> exn(aCx); + if (!JS_GetPendingException(aCx, &exn)) { + // Probably shouldn't actually happen? But let's go ahead and just use null + // for lack of anything better. + exn.setNull(); + } + JS_ClearPendingException(aCx); + + nsString message, filename, line; + uint32_t lineNumber, columnNumber, flags, errorNumber; + JSExnType exnType = JSEXN_ERR; + bool mutedError = aReport && aReport->isMuted; + + if (aReport) { + // We want the same behavior here as xpc::ErrorReport::init here. + xpc::ErrorReport::ErrorReportToMessageString(aReport, message); + + filename = NS_ConvertUTF8toUTF16(aReport->filename); + line.Assign(aReport->linebuf(), aReport->linebufLength()); + lineNumber = aReport->lineno; + columnNumber = aReport->tokenOffset(); + flags = aReport->flags; + errorNumber = aReport->errorNumber; + MOZ_ASSERT(aReport->exnType >= JSEXN_FIRST && aReport->exnType < JSEXN_LIMIT); + exnType = JSExnType(aReport->exnType); + } + else { + lineNumber = columnNumber = errorNumber = 0; + flags = nsIScriptError::errorFlag | nsIScriptError::exceptionFlag; + } + + if (message.IsEmpty() && aToStringResult) { + nsDependentCString toStringResult(aToStringResult.c_str()); + if (!AppendUTF8toUTF16(toStringResult, message, mozilla::fallible)) { + // Try again, with only a 1 KB string. Do this infallibly this time. + // If the user doesn't have 1 KB to spare we're done anyways. + uint32_t index = std::min(uint32_t(1024), toStringResult.Length()); + + // Drop the last code point that may be cropped. + index = RewindToPriorUTF8Codepoint(toStringResult.BeginReading(), index); + + nsDependentCString truncatedToStringResult(aToStringResult.c_str(), + index); + AppendUTF8toUTF16(truncatedToStringResult, message); + } + } + + mErrorHandlerRecursionCount++; + + // Don't want to run the scope's error handler if this is a recursive error or + // if we ran out of memory. + bool fireAtScope = mErrorHandlerRecursionCount == 1 && + errorNumber != JSMSG_OUT_OF_MEMORY && + JS::CurrentGlobalOrNull(aCx); + + ReportErrorRunnable::ReportError(aCx, this, fireAtScope, nullptr, message, + filename, line, lineNumber, + columnNumber, flags, errorNumber, exnType, + mutedError, 0, exn); + + mErrorHandlerRecursionCount--; +} + +// static +void +WorkerPrivate::ReportErrorToConsole(const char* aMessage) +{ + WorkerPrivate* wp = nullptr; + if (!NS_IsMainThread()) { + wp = GetCurrentThreadWorkerPrivate(); + } + + ReportErrorToConsoleRunnable::Report(wp, aMessage); +} + +int32_t +WorkerPrivate::SetTimeout(JSContext* aCx, + nsIScriptTimeoutHandler* aHandler, + int32_t aTimeout, bool aIsInterval, + ErrorResult& aRv) +{ + AssertIsOnWorkerThread(); + MOZ_ASSERT(aHandler); + + const int32_t timerId = mNextTimeoutId++; + + Status currentStatus; + { + MutexAutoLock lock(mMutex); + currentStatus = mStatus; + } + + // If the worker is trying to call setTimeout/setInterval and the parent + // thread has initiated the close process then just silently fail. + if (currentStatus >= Closing) { + aRv.Throw(NS_ERROR_FAILURE); + return 0; + } + + nsAutoPtr<TimeoutInfo> newInfo(new TimeoutInfo()); + newInfo->mIsInterval = aIsInterval; + newInfo->mId = timerId; + + if (MOZ_UNLIKELY(timerId == INT32_MAX)) { + NS_WARNING("Timeout ids overflowed!"); + mNextTimeoutId = 1; + } + + newInfo->mHandler = aHandler; + + // See if any of the optional arguments were passed. + aTimeout = std::max(0, aTimeout); + newInfo->mInterval = TimeDuration::FromMilliseconds(aTimeout); + + newInfo->mTargetTime = TimeStamp::Now() + newInfo->mInterval; + + nsAutoPtr<TimeoutInfo>* insertedInfo = + mTimeouts.InsertElementSorted(newInfo.forget(), GetAutoPtrComparator(mTimeouts)); + + LOG(TimeoutsLog(), ("Worker %p has new timeout: delay=%d interval=%s\n", + this, aTimeout, aIsInterval ? "yes" : "no")); + + // If the timeout we just made is set to fire next then we need to update the + // timer, unless we're currently running timeouts. + if (insertedInfo == mTimeouts.Elements() && !mRunningExpiredTimeouts) { + nsresult rv; + + if (!mTimer) { + mTimer = do_CreateInstance(NS_TIMER_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return 0; + } + + mTimerRunnable = new TimerRunnable(this); + } + + if (!mTimerRunning) { + if (!ModifyBusyCountFromWorker(true)) { + aRv.Throw(NS_ERROR_FAILURE); + return 0; + } + mTimerRunning = true; + } + + if (!RescheduleTimeoutTimer(aCx)) { + aRv.Throw(NS_ERROR_FAILURE); + return 0; + } + } + + return timerId; +} + +void +WorkerPrivate::ClearTimeout(int32_t aId) +{ + AssertIsOnWorkerThread(); + + if (!mTimeouts.IsEmpty()) { + NS_ASSERTION(mTimerRunning, "Huh?!"); + + for (uint32_t index = 0; index < mTimeouts.Length(); index++) { + nsAutoPtr<TimeoutInfo>& info = mTimeouts[index]; + if (info->mId == aId) { + info->mCanceled = true; + break; + } + } + } +} + +bool +WorkerPrivate::RunExpiredTimeouts(JSContext* aCx) +{ + AssertIsOnWorkerThread(); + + // We may be called recursively (e.g. close() inside a timeout) or we could + // have been canceled while this event was pending, bail out if there is + // nothing to do. + if (mRunningExpiredTimeouts || !mTimerRunning) { + return true; + } + + NS_ASSERTION(mTimer && mTimerRunnable, "Must have a timer!"); + NS_ASSERTION(!mTimeouts.IsEmpty(), "Should have some work to do!"); + + bool retval = true; + + AutoPtrComparator<TimeoutInfo> comparator = GetAutoPtrComparator(mTimeouts); + JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx)); + + // We want to make sure to run *something*, even if the timer fired a little + // early. Fudge the value of now to at least include the first timeout. + const TimeStamp actual_now = TimeStamp::Now(); + const TimeStamp now = std::max(actual_now, mTimeouts[0]->mTargetTime); + + if (now != actual_now) { + LOG(TimeoutsLog(), ("Worker %p fudged timeout by %f ms.\n", this, + (now - actual_now).ToMilliseconds())); + } + + AutoTArray<TimeoutInfo*, 10> expiredTimeouts; + for (uint32_t index = 0; index < mTimeouts.Length(); index++) { + nsAutoPtr<TimeoutInfo>& info = mTimeouts[index]; + if (info->mTargetTime > now) { + break; + } + expiredTimeouts.AppendElement(info); + } + + // Guard against recursion. + mRunningExpiredTimeouts = true; + + // Run expired timeouts. + for (uint32_t index = 0; index < expiredTimeouts.Length(); index++) { + TimeoutInfo*& info = expiredTimeouts[index]; + + if (info->mCanceled) { + continue; + } + + LOG(TimeoutsLog(), ("Worker %p executing timeout with original delay %f ms.\n", + this, info->mInterval.ToMilliseconds())); + + // Always check JS_IsExceptionPending if something fails, and if + // JS_IsExceptionPending returns false (i.e. uncatchable exception) then + // break out of the loop. + const char *reason; + if (info->mIsInterval) { + reason = "setInterval handler"; + } else { + reason = "setTimeout handler"; + } + + RefPtr<Function> callback = info->mHandler->GetCallback(); + if (!callback) { + // scope for the AutoEntryScript, so it comes off the stack before we do + // Promise::PerformMicroTaskCheckpoint. + AutoEntryScript aes(global, reason, false); + + // Evaluate the timeout expression. + const nsAString& script = info->mHandler->GetHandlerText(); + + const char* filename = nullptr; + uint32_t lineNo = 0, dummyColumn = 0; + info->mHandler->GetLocation(&filename, &lineNo, &dummyColumn); + + JS::CompileOptions options(aes.cx()); + options.setFileAndLine(filename, lineNo).setNoScriptRval(true); + + JS::Rooted<JS::Value> unused(aes.cx()); + + if (!JS::Evaluate(aes.cx(), options, script.BeginReading(), + script.Length(), &unused) && + !JS_IsExceptionPending(aCx)) { + retval = false; + break; + } + } else { + ErrorResult rv; + JS::Rooted<JS::Value> ignoredVal(aCx); + callback->Call(GlobalScope(), info->mHandler->GetArgs(), &ignoredVal, rv, + reason); + if (rv.IsUncatchableException()) { + rv.SuppressException(); + retval = false; + break; + } + + rv.SuppressException(); + } + + // Since we might be processing more timeouts, go ahead and flush + // the promise queue now before we do that. + Promise::PerformWorkerMicroTaskCheckpoint(); + + NS_ASSERTION(mRunningExpiredTimeouts, "Someone changed this!"); + } + + // No longer possible to be called recursively. + mRunningExpiredTimeouts = false; + + // Now remove canceled and expired timeouts from the main list. + // NB: The timeouts present in expiredTimeouts must have the same order + // with respect to each other in mTimeouts. That is, mTimeouts is just + // expiredTimeouts with extra elements inserted. There may be unexpired + // timeouts that have been inserted between the expired timeouts if the + // timeout event handler called setTimeout/setInterval. + for (uint32_t index = 0, expiredTimeoutIndex = 0, + expiredTimeoutLength = expiredTimeouts.Length(); + index < mTimeouts.Length(); ) { + nsAutoPtr<TimeoutInfo>& info = mTimeouts[index]; + if ((expiredTimeoutIndex < expiredTimeoutLength && + info == expiredTimeouts[expiredTimeoutIndex] && + ++expiredTimeoutIndex) || + info->mCanceled) { + if (info->mIsInterval && !info->mCanceled) { + // Reschedule intervals. + info->mTargetTime = info->mTargetTime + info->mInterval; + // Don't resort the list here, we'll do that at the end. + ++index; + } + else { + mTimeouts.RemoveElement(info); + } + } + else { + // If info did not match the current entry in expiredTimeouts, it + // shouldn't be there at all. + NS_ASSERTION(!expiredTimeouts.Contains(info), + "Our timeouts are out of order!"); + ++index; + } + } + + mTimeouts.Sort(comparator); + + // Either signal the parent that we're no longer using timeouts or reschedule + // the timer. + if (mTimeouts.IsEmpty()) { + if (!ModifyBusyCountFromWorker(false)) { + retval = false; + } + mTimerRunning = false; + } + else if (retval && !RescheduleTimeoutTimer(aCx)) { + retval = false; + } + + return retval; +} + +bool +WorkerPrivate::RescheduleTimeoutTimer(JSContext* aCx) +{ + AssertIsOnWorkerThread(); + MOZ_ASSERT(!mRunningExpiredTimeouts); + NS_ASSERTION(!mTimeouts.IsEmpty(), "Should have some timeouts!"); + NS_ASSERTION(mTimer && mTimerRunnable, "Should have a timer!"); + + // NB: This is important! The timer may have already fired, e.g. if a timeout + // callback itself calls setTimeout for a short duration and then takes longer + // than that to finish executing. If that has happened, it's very important + // that we don't execute the event that is now pending in our event queue, or + // our code in RunExpiredTimeouts to "fudge" the timeout value will unleash an + // early timeout when we execute the event we're about to queue. + mTimer->Cancel(); + + double delta = + (mTimeouts[0]->mTargetTime - TimeStamp::Now()).ToMilliseconds(); + uint32_t delay = delta > 0 ? std::min(delta, double(UINT32_MAX)) : 0; + + LOG(TimeoutsLog(), ("Worker %p scheduled timer for %d ms, %d pending timeouts\n", + this, delay, mTimeouts.Length())); + + nsresult rv = mTimer->InitWithCallback(mTimerRunnable, delay, nsITimer::TYPE_ONE_SHOT); + if (NS_FAILED(rv)) { + JS_ReportErrorASCII(aCx, "Failed to start timer!"); + return false; + } + + return true; +} + +void +WorkerPrivate::UpdateContextOptionsInternal( + JSContext* aCx, + const JS::ContextOptions& aContextOptions) +{ + AssertIsOnWorkerThread(); + + JS::ContextOptionsRef(aCx) = aContextOptions; + + for (uint32_t index = 0; index < mChildWorkers.Length(); index++) { + mChildWorkers[index]->UpdateContextOptions(aContextOptions); + } +} + +void +WorkerPrivate::UpdateLanguagesInternal(const nsTArray<nsString>& aLanguages) +{ + WorkerGlobalScope* globalScope = GlobalScope(); + if (globalScope) { + RefPtr<WorkerNavigator> nav = globalScope->GetExistingNavigator(); + if (nav) { + nav->SetLanguages(aLanguages); + } + } + + for (uint32_t index = 0; index < mChildWorkers.Length(); index++) { + mChildWorkers[index]->UpdateLanguages(aLanguages); + } +} + +void +WorkerPrivate::UpdatePreferenceInternal(WorkerPreference aPref, bool aValue) +{ + AssertIsOnWorkerThread(); + MOZ_ASSERT(aPref >= 0 && aPref < WORKERPREF_COUNT); + + mPreferences[aPref] = aValue; + + for (uint32_t index = 0; index < mChildWorkers.Length(); index++) { + mChildWorkers[index]->UpdatePreference(aPref, aValue); + } +} + +void +WorkerPrivate::UpdateJSWorkerMemoryParameterInternal(JSContext* aCx, + JSGCParamKey aKey, + uint32_t aValue) +{ + AssertIsOnWorkerThread(); + + // XXX aValue might be 0 here (telling us to unset a previous value for child + // workers). Calling JS_SetGCParameter with a value of 0 isn't actually + // supported though. We really need some way to revert to a default value + // here. + if (aValue) { + JS_SetGCParameter(aCx, aKey, aValue); + } + + for (uint32_t index = 0; index < mChildWorkers.Length(); index++) { + mChildWorkers[index]->UpdateJSWorkerMemoryParameter(aKey, aValue); + } +} + +#ifdef JS_GC_ZEAL +void +WorkerPrivate::UpdateGCZealInternal(JSContext* aCx, uint8_t aGCZeal, + uint32_t aFrequency) +{ + AssertIsOnWorkerThread(); + + JS_SetGCZeal(aCx, aGCZeal, aFrequency); + + for (uint32_t index = 0; index < mChildWorkers.Length(); index++) { + mChildWorkers[index]->UpdateGCZeal(aGCZeal, aFrequency); + } +} +#endif + +void +WorkerPrivate::GarbageCollectInternal(JSContext* aCx, bool aShrinking, + bool aCollectChildren) +{ + AssertIsOnWorkerThread(); + + if (!GlobalScope()) { + // We haven't compiled anything yet. Just bail out. + return; + } + + if (aShrinking || aCollectChildren) { + JS::PrepareForFullGC(aCx); + + if (aShrinking) { + JS::GCForReason(aCx, GC_SHRINK, JS::gcreason::DOM_WORKER); + + if (!aCollectChildren) { + LOG(WorkerLog(), ("Worker %p collected idle garbage\n", this)); + } + } + else { + JS::GCForReason(aCx, GC_NORMAL, JS::gcreason::DOM_WORKER); + LOG(WorkerLog(), ("Worker %p collected garbage\n", this)); + } + } + else { + JS_MaybeGC(aCx); + LOG(WorkerLog(), ("Worker %p collected periodic garbage\n", this)); + } + + if (aCollectChildren) { + for (uint32_t index = 0; index < mChildWorkers.Length(); index++) { + mChildWorkers[index]->GarbageCollect(aShrinking); + } + } +} + +void +WorkerPrivate::CycleCollectInternal(bool aCollectChildren) +{ + AssertIsOnWorkerThread(); + + nsCycleCollector_collect(nullptr); + + if (aCollectChildren) { + for (uint32_t index = 0; index < mChildWorkers.Length(); index++) { + mChildWorkers[index]->CycleCollect(/* dummy = */ false); + } + } +} + +void +WorkerPrivate::MemoryPressureInternal() +{ + AssertIsOnWorkerThread(); + + RefPtr<Console> console = mScope ? mScope->GetConsoleIfExists() : nullptr; + if (console) { + console->ClearStorage(); + } + + console = mDebuggerScope ? mDebuggerScope->GetConsoleIfExists() : nullptr; + if (console) { + console->ClearStorage(); + } + + for (uint32_t index = 0; index < mChildWorkers.Length(); index++) { + mChildWorkers[index]->MemoryPressure(false); + } +} + +void +WorkerPrivate::SetThread(WorkerThread* aThread) +{ + if (aThread) { +#ifdef DEBUG + { + bool isOnCurrentThread; + MOZ_ASSERT(NS_SUCCEEDED(aThread->IsOnCurrentThread(&isOnCurrentThread))); + MOZ_ASSERT(isOnCurrentThread); + } +#endif + + MOZ_ASSERT(!mPRThread); + mPRThread = PRThreadFromThread(aThread); + MOZ_ASSERT(mPRThread); + } + else { + MOZ_ASSERT(mPRThread); + } + + const WorkerThreadFriendKey friendKey; + + RefPtr<WorkerThread> doomedThread; + + { // Scope so that |doomedThread| is released without holding the lock. + MutexAutoLock lock(mMutex); + + if (aThread) { + MOZ_ASSERT(!mThread); + MOZ_ASSERT(mStatus == Pending); + + mThread = aThread; + mThread->SetWorker(friendKey, this); + + if (!mPreStartRunnables.IsEmpty()) { + for (uint32_t index = 0; index < mPreStartRunnables.Length(); index++) { + MOZ_ALWAYS_SUCCEEDS( + mThread->DispatchAnyThread(friendKey, mPreStartRunnables[index].forget())); + } + mPreStartRunnables.Clear(); + } + } + else { + MOZ_ASSERT(mThread); + + mThread->SetWorker(friendKey, nullptr); + + mThread.swap(doomedThread); + } + } +} + +WorkerCrossThreadDispatcher* +WorkerPrivate::GetCrossThreadDispatcher() +{ + MutexAutoLock lock(mMutex); + + if (!mCrossThreadDispatcher && mStatus <= Running) { + mCrossThreadDispatcher = new WorkerCrossThreadDispatcher(this); + } + + return mCrossThreadDispatcher; +} + +void +WorkerPrivate::BeginCTypesCall() +{ + AssertIsOnWorkerThread(); + + // Don't try to GC while we're blocked in a ctypes call. + SetGCTimerMode(NoTimer); +} + +void +WorkerPrivate::EndCTypesCall() +{ + AssertIsOnWorkerThread(); + + // Make sure the periodic timer is running before we start running JS again. + SetGCTimerMode(PeriodicTimer); +} + +bool +WorkerPrivate::ConnectMessagePort(JSContext* aCx, + MessagePortIdentifier& aIdentifier) +{ + AssertIsOnWorkerThread(); + + WorkerGlobalScope* globalScope = GlobalScope(); + + JS::Rooted<JSObject*> jsGlobal(aCx, globalScope->GetWrapper()); + MOZ_ASSERT(jsGlobal); + + // This MessagePortIdentifier is used to create a new port, still connected + // with the other one, but in the worker thread. + ErrorResult rv; + RefPtr<MessagePort> port = MessagePort::Create(globalScope, aIdentifier, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + return false; + } + + GlobalObject globalObject(aCx, jsGlobal); + if (globalObject.Failed()) { + return false; + } + + RootedDictionary<MessageEventInit> init(aCx); + init.mBubbles = false; + init.mCancelable = false; + init.mSource.SetValue().SetAsMessagePort() = port; + if (!init.mPorts.AppendElement(port.forget(), fallible)) { + return false; + } + + RefPtr<MessageEvent> event = + MessageEvent::Constructor(globalObject, + NS_LITERAL_STRING("connect"), init, rv); + + event->SetTrusted(true); + + nsCOMPtr<nsIDOMEvent> domEvent = do_QueryObject(event); + + nsEventStatus dummy = nsEventStatus_eIgnore; + globalScope->DispatchDOMEvent(nullptr, domEvent, nullptr, &dummy); + + return true; +} + +WorkerGlobalScope* +WorkerPrivate::GetOrCreateGlobalScope(JSContext* aCx) +{ + AssertIsOnWorkerThread(); + + if (!mScope) { + RefPtr<WorkerGlobalScope> globalScope; + if (IsSharedWorker()) { + globalScope = new SharedWorkerGlobalScope(this, WorkerName()); + } else if (IsServiceWorker()) { + globalScope = new ServiceWorkerGlobalScope(this, WorkerName()); + } else { + globalScope = new DedicatedWorkerGlobalScope(this); + } + + JS::Rooted<JSObject*> global(aCx); + NS_ENSURE_TRUE(globalScope->WrapGlobalObject(aCx, &global), nullptr); + + JSAutoCompartment ac(aCx, global); + + // RegisterBindings() can spin a nested event loop so we have to set mScope + // before calling it, and we have to make sure to unset mScope if it fails. + mScope = Move(globalScope); + + if (!RegisterBindings(aCx, global)) { + mScope = nullptr; + return nullptr; + } + + JS_FireOnNewGlobalObject(aCx, global); + } + + return mScope; +} + +WorkerDebuggerGlobalScope* +WorkerPrivate::CreateDebuggerGlobalScope(JSContext* aCx) +{ + AssertIsOnWorkerThread(); + + MOZ_ASSERT(!mDebuggerScope); + + RefPtr<WorkerDebuggerGlobalScope> globalScope = + new WorkerDebuggerGlobalScope(this); + + JS::Rooted<JSObject*> global(aCx); + NS_ENSURE_TRUE(globalScope->WrapGlobalObject(aCx, &global), nullptr); + + JSAutoCompartment ac(aCx, global); + + // RegisterDebuggerBindings() can spin a nested event loop so we have to set + // mDebuggerScope before calling it, and we have to make sure to unset + // mDebuggerScope if it fails. + mDebuggerScope = Move(globalScope); + + if (!RegisterDebuggerBindings(aCx, global)) { + mDebuggerScope = nullptr; + return nullptr; + } + + JS_FireOnNewGlobalObject(aCx, global); + + return mDebuggerScope; +} + +#ifdef DEBUG + +void +WorkerPrivate::AssertIsOnWorkerThread() const +{ + // This is much more complicated than it needs to be but we can't use mThread + // because it must be protected by mMutex and sometimes this method is called + // when mMutex is already locked. This method should always work. + MOZ_ASSERT(mPRThread, + "AssertIsOnWorkerThread() called before a thread was assigned!"); + + nsCOMPtr<nsIThread> thread; + nsresult rv = + nsThreadManager::get().GetThreadFromPRThread(mPRThread, + getter_AddRefs(thread)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + MOZ_ASSERT(thread); + + bool current; + rv = thread->IsOnCurrentThread(¤t); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + MOZ_ASSERT(current, "Wrong thread!"); +} + +#endif // DEBUG + +NS_IMPL_ISUPPORTS_INHERITED0(ExternalRunnableWrapper, WorkerRunnable) + +template <class Derived> +NS_IMPL_ADDREF(WorkerPrivateParent<Derived>::EventTarget) + +template <class Derived> +NS_IMPL_RELEASE(WorkerPrivateParent<Derived>::EventTarget) + +template <class Derived> +NS_INTERFACE_MAP_BEGIN(WorkerPrivateParent<Derived>::EventTarget) + NS_INTERFACE_MAP_ENTRY(nsIEventTarget) + NS_INTERFACE_MAP_ENTRY(nsISupports) +#ifdef DEBUG + // kDEBUGWorkerEventTargetIID is special in that it does not AddRef its + // result. + if (aIID.Equals(kDEBUGWorkerEventTargetIID)) { + *aInstancePtr = this; + return NS_OK; + } + else +#endif +NS_INTERFACE_MAP_END + +template <class Derived> +NS_IMETHODIMP +WorkerPrivateParent<Derived>:: +EventTarget::DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) +{ + nsCOMPtr<nsIRunnable> event(aRunnable); + return Dispatch(event.forget(), aFlags); +} + +template <class Derived> +NS_IMETHODIMP +WorkerPrivateParent<Derived>:: +EventTarget::Dispatch(already_AddRefed<nsIRunnable> aRunnable, uint32_t aFlags) +{ + // May be called on any thread! + nsCOMPtr<nsIRunnable> event(aRunnable); + + // Workers only support asynchronous dispatch for now. + if (NS_WARN_IF(aFlags != NS_DISPATCH_NORMAL)) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<WorkerRunnable> workerRunnable; + + MutexAutoLock lock(mMutex); + + if (!mWorkerPrivate) { + NS_WARNING("A runnable was posted to a worker that is already shutting " + "down!"); + return NS_ERROR_UNEXPECTED; + } + + if (event) { + workerRunnable = mWorkerPrivate->MaybeWrapAsWorkerRunnable(event.forget()); + } + + nsresult rv = + mWorkerPrivate->DispatchPrivate(workerRunnable.forget(), mNestedEventTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +template <class Derived> +NS_IMETHODIMP +WorkerPrivateParent<Derived>:: +EventTarget::DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +template <class Derived> +NS_IMETHODIMP +WorkerPrivateParent<Derived>:: +EventTarget::IsOnCurrentThread(bool* aIsOnCurrentThread) +{ + // May be called on any thread! + + MOZ_ASSERT(aIsOnCurrentThread); + + MutexAutoLock lock(mMutex); + + if (!mWorkerPrivate) { + NS_WARNING("A worker's event target was used after the worker has !"); + return NS_ERROR_UNEXPECTED; + } + + nsresult rv = mWorkerPrivate->IsOnCurrentThread(aIsOnCurrentThread); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +BEGIN_WORKERS_NAMESPACE + +WorkerCrossThreadDispatcher* +GetWorkerCrossThreadDispatcher(JSContext* aCx, const JS::Value& aWorker) +{ + if (!aWorker.isObject()) { + return nullptr; + } + + JS::Rooted<JSObject*> obj(aCx, &aWorker.toObject()); + WorkerPrivate* w = nullptr; + UNWRAP_OBJECT(Worker, &obj, w); + MOZ_ASSERT(w); + return w->GetCrossThreadDispatcher(); +} + +// Force instantiation. +template class WorkerPrivateParent<WorkerPrivate>; + +END_WORKERS_NAMESPACE diff --git a/dom/workers/WorkerPrivate.h b/dom/workers/WorkerPrivate.h new file mode 100644 index 000000000..ad906b054 --- /dev/null +++ b/dom/workers/WorkerPrivate.h @@ -0,0 +1,1594 @@ +/* -*- 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_workers_workerprivate_h__ +#define mozilla_dom_workers_workerprivate_h__ + +#include "Workers.h" + +#include "js/CharacterEncoding.h" +#include "nsIContentPolicy.h" +#include "nsIContentSecurityPolicy.h" +#include "nsILoadGroup.h" +#include "nsIWorkerDebugger.h" +#include "nsPIDOMWindow.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/CondVar.h" +#include "mozilla/ConsoleReportCollector.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/Move.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "nsAutoPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsDataHashtable.h" +#include "nsHashKeys.h" +#include "nsRefPtrHashtable.h" +#include "nsString.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "nsTObserverArray.h" + +#include "Queue.h" +#include "WorkerHolder.h" + +#ifdef XP_WIN +#undef PostMessage +#endif + +class nsIChannel; +class nsIConsoleReportCollector; +class nsIDocument; +class nsIEventTarget; +class nsIPrincipal; +class nsIScriptContext; +class nsIScriptTimeoutHandler; +class nsISerializable; +class nsIThread; +class nsIThreadInternal; +class nsITimer; +class nsIURI; +template<class T> class nsMainThreadPtrHandle; + +namespace JS { +struct RuntimeStats; +} // namespace JS + +namespace mozilla { +class ThrottledEventQueue; +namespace dom { +class Function; +class MessagePort; +class MessagePortIdentifier; +class PromiseNativeHandler; +class StructuredCloneHolder; +class WorkerDebuggerGlobalScope; +class WorkerGlobalScope; +} // namespace dom +namespace ipc { +class PrincipalInfo; +} // namespace ipc +} // namespace mozilla + +struct PRThread; + +class ReportDebuggerErrorRunnable; +class PostDebuggerMessageRunnable; + +BEGIN_WORKERS_NAMESPACE + +class AutoSyncLoopHolder; +class SharedWorker; +class ServiceWorkerClientInfo; +class WorkerControlRunnable; +class WorkerDebugger; +class WorkerPrivate; +class WorkerRunnable; +class WorkerThread; + +// SharedMutex is a small wrapper around an (internal) reference-counted Mutex +// object. It exists to avoid changing a lot of code to use Mutex* instead of +// Mutex&. +class SharedMutex +{ + typedef mozilla::Mutex Mutex; + + class RefCountedMutex final : public Mutex + { + public: + explicit RefCountedMutex(const char* aName) + : Mutex(aName) + { } + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RefCountedMutex) + + private: + ~RefCountedMutex() + { } + }; + + RefPtr<RefCountedMutex> mMutex; + +public: + explicit SharedMutex(const char* aName) + : mMutex(new RefCountedMutex(aName)) + { } + + SharedMutex(SharedMutex& aOther) + : mMutex(aOther.mMutex) + { } + + operator Mutex&() + { + return *mMutex; + } + + operator const Mutex&() const + { + return *mMutex; + } + + void + AssertCurrentThreadOwns() const + { + mMutex->AssertCurrentThreadOwns(); + } +}; + +template <class Derived> +class WorkerPrivateParent : public DOMEventTargetHelper +{ +protected: + class EventTarget; + friend class EventTarget; + + typedef mozilla::ipc::PrincipalInfo PrincipalInfo; + +public: + struct LocationInfo + { + nsCString mHref; + nsCString mProtocol; + nsCString mHost; + nsCString mHostname; + nsCString mPort; + nsCString mPathname; + nsCString mSearch; + nsCString mHash; + nsString mOrigin; + }; + +protected: + typedef mozilla::ErrorResult ErrorResult; + + SharedMutex mMutex; + mozilla::CondVar mCondVar; + + // Protected by mMutex. + RefPtr<EventTarget> mEventTarget; + nsTArray<RefPtr<WorkerRunnable>> mPreStartRunnables; + +private: + WorkerPrivate* mParent; + nsString mScriptURL; + // This is the worker name for shared workers or the worker scope + // for service workers. + nsCString mWorkerName; + LocationInfo mLocationInfo; + // The lifetime of these objects within LoadInfo is managed explicitly; + // they do not need to be cycle collected. + WorkerLoadInfo mLoadInfo; + + Atomic<bool> mLoadingWorkerScript; + + // Only used for top level workers. + nsTArray<nsCOMPtr<nsIRunnable>> mQueuedRunnables; + + // Protected by mMutex. + JSSettings mJSSettings; + + // Only touched on the parent thread (currently this is always the main + // thread as SharedWorkers are always top-level). + nsTArray<RefPtr<SharedWorker>> mSharedWorkers; + + uint64_t mBusyCount; + // SharedWorkers may have multiple windows paused, so this must be + // a count instead of just a boolean. + uint32_t mParentWindowPausedDepth; + Status mParentStatus; + bool mParentFrozen; + bool mIsChromeWorker; + bool mMainThreadObjectsForgotten; + // mIsSecureContext is set once in our constructor; after that it can be read + // from various threads. We could make this const if we were OK with setting + // it in the initializer list via calling some function that takes all sorts + // of state (loadinfo, worker type, parent). + // + // It's a bit unfortunate that we have to have an out-of-band boolean for + // this, but we need access to this state from the parent thread, and we can't + // use our global object's secure state there. + bool mIsSecureContext; + WorkerType mWorkerType; + TimeStamp mCreationTimeStamp; + DOMHighResTimeStamp mCreationTimeHighRes; + TimeStamp mNowBaseTimeStamp; + DOMHighResTimeStamp mNowBaseTimeHighRes; + +protected: + // The worker is owned by its thread, which is represented here. This is set + // in Construct() and emptied by WorkerFinishedRunnable, and conditionally + // traversed by the cycle collector if the busy count is zero. + RefPtr<WorkerPrivate> mSelfRef; + + WorkerPrivateParent(WorkerPrivate* aParent, + const nsAString& aScriptURL, bool aIsChromeWorker, + WorkerType aWorkerType, + const nsACString& aSharedWorkerName, + WorkerLoadInfo& aLoadInfo); + + ~WorkerPrivateParent(); + +private: + Derived* + ParentAsWorkerPrivate() const + { + return static_cast<Derived*>(const_cast<WorkerPrivateParent*>(this)); + } + + bool + NotifyPrivate(Status aStatus); + + bool + TerminatePrivate() + { + return NotifyPrivate(Terminating); + } + + void + PostMessageInternal(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + UniquePtr<ServiceWorkerClientInfo>&& aClientInfo, + PromiseNativeHandler* aHandler, + ErrorResult& aRv); + + nsresult + DispatchPrivate(already_AddRefed<WorkerRunnable> aRunnable, nsIEventTarget* aSyncLoopTarget); + +public: + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(WorkerPrivateParent, + DOMEventTargetHelper) + + void + EnableDebugger(); + + void + DisableDebugger(); + + void + ClearSelfRef() + { + AssertIsOnParentThread(); + MOZ_ASSERT(mSelfRef); + mSelfRef = nullptr; + } + + nsresult + Dispatch(already_AddRefed<WorkerRunnable> aRunnable) + { + return DispatchPrivate(Move(aRunnable), nullptr); + } + + nsresult + DispatchControlRunnable(already_AddRefed<WorkerControlRunnable> aWorkerControlRunnable); + + nsresult + DispatchDebuggerRunnable(already_AddRefed<WorkerRunnable> aDebuggerRunnable); + + already_AddRefed<WorkerRunnable> + MaybeWrapAsWorkerRunnable(already_AddRefed<nsIRunnable> aRunnable); + + already_AddRefed<nsIEventTarget> + GetEventTarget(); + + // May be called on any thread... + bool + Start(); + + // Called on the parent thread. + bool + Notify(Status aStatus) + { + return NotifyPrivate(aStatus); + } + + bool + Cancel() + { + return Notify(Canceling); + } + + bool + Kill() + { + return Notify(Killing); + } + + // We can assume that an nsPIDOMWindow will be available for Freeze, Thaw + // as these are only used for globals going in and out of the bfcache. + // + // XXXbz: This is a bald-faced lie given the uses in RegisterSharedWorker and + // CloseSharedWorkersForWindow, which pass null for aWindow to Thaw and Freeze + // respectively. See bug 1251722. + bool + Freeze(nsPIDOMWindowInner* aWindow); + + bool + Thaw(nsPIDOMWindowInner* aWindow); + + // When we debug a worker, we want to disconnect the window and the worker + // communication. This happens calling this method. + // Note: this method doesn't suspend the worker! Use Freeze/Thaw instead. + void + ParentWindowPaused(); + + void + ParentWindowResumed(); + + bool + Terminate() + { + AssertIsOnParentThread(); + return TerminatePrivate(); + } + + bool + Close(); + + bool + ModifyBusyCount(bool aIncrease); + + void + ForgetOverridenLoadGroup(nsCOMPtr<nsILoadGroup>& aLoadGroupOut); + + void + ForgetMainThreadObjects(nsTArray<nsCOMPtr<nsISupports> >& aDoomed); + + void + PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + ErrorResult& aRv); + + void + PostMessageToServiceWorker(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + UniquePtr<ServiceWorkerClientInfo>&& aClientInfo, + PromiseNativeHandler* aHandler, + ErrorResult& aRv); + + void + UpdateContextOptions(const JS::ContextOptions& aContextOptions); + + void + UpdateLanguages(const nsTArray<nsString>& aLanguages); + + void + UpdatePreference(WorkerPreference aPref, bool aValue); + + void + UpdateJSWorkerMemoryParameter(JSGCParamKey key, uint32_t value); + +#ifdef JS_GC_ZEAL + void + UpdateGCZeal(uint8_t aGCZeal, uint32_t aFrequency); +#endif + + void + GarbageCollect(bool aShrinking); + + void + CycleCollect(bool aDummy); + + void + OfflineStatusChangeEvent(bool aIsOffline); + + void + MemoryPressure(bool aDummy); + + bool + RegisterSharedWorker(SharedWorker* aSharedWorker, MessagePort* aPort); + + void + BroadcastErrorToSharedWorkers(JSContext* aCx, + const nsAString& aMessage, + const nsAString& aFilename, + const nsAString& aLine, + uint32_t aLineNumber, + uint32_t aColumnNumber, + uint32_t aFlags); + + void + WorkerScriptLoaded(); + + void + QueueRunnable(nsIRunnable* aRunnable) + { + AssertIsOnParentThread(); + mQueuedRunnables.AppendElement(aRunnable); + } + + WorkerPrivate* + GetParent() const + { + return mParent; + } + + bool + IsFrozen() const + { + AssertIsOnParentThread(); + return mParentFrozen; + } + + bool + IsParentWindowPaused() const + { + AssertIsOnParentThread(); + return mParentWindowPausedDepth > 0; + } + + bool + IsAcceptingEvents() + { + AssertIsOnParentThread(); + + MutexAutoLock lock(mMutex); + return mParentStatus < Terminating; + } + + Status + ParentStatus() const + { + mMutex.AssertCurrentThreadOwns(); + return mParentStatus; + } + + nsIScriptContext* + GetScriptContext() const + { + AssertIsOnMainThread(); + return mLoadInfo.mScriptContext; + } + + const nsString& + ScriptURL() const + { + return mScriptURL; + } + + const nsCString& + Domain() const + { + return mLoadInfo.mDomain; + } + + bool + IsFromWindow() const + { + return mLoadInfo.mFromWindow; + } + + uint64_t + WindowID() const + { + return mLoadInfo.mWindowID; + } + + uint64_t + ServiceWorkerID() const + { + return mLoadInfo.mServiceWorkerID; + } + + nsIURI* + GetBaseURI() const + { + AssertIsOnMainThread(); + return mLoadInfo.mBaseURI; + } + + void + SetBaseURI(nsIURI* aBaseURI); + + nsIURI* + GetResolvedScriptURI() const + { + AssertIsOnMainThread(); + return mLoadInfo.mResolvedScriptURI; + } + + const nsString& + ServiceWorkerCacheName() const + { + MOZ_ASSERT(IsServiceWorker()); + AssertIsOnMainThread(); + return mLoadInfo.mServiceWorkerCacheName; + } + + const ChannelInfo& + GetChannelInfo() const + { + return mLoadInfo.mChannelInfo; + } + + void + SetChannelInfo(const ChannelInfo& aChannelInfo) + { + AssertIsOnMainThread(); + MOZ_ASSERT(!mLoadInfo.mChannelInfo.IsInitialized()); + MOZ_ASSERT(aChannelInfo.IsInitialized()); + mLoadInfo.mChannelInfo = aChannelInfo; + } + + void + InitChannelInfo(nsIChannel* aChannel) + { + mLoadInfo.mChannelInfo.InitFromChannel(aChannel); + } + + void + InitChannelInfo(const ChannelInfo& aChannelInfo) + { + mLoadInfo.mChannelInfo = aChannelInfo; + } + + // This is used to handle importScripts(). When the worker is first loaded + // and executed, it happens in a sync loop. At this point it sets + // mLoadingWorkerScript to true. importScripts() calls that occur during the + // execution run in nested sync loops and so this continues to return true, + // leading to these scripts being cached offline. + // mLoadingWorkerScript is set to false when the top level loop ends. + // importScripts() in function calls or event handlers are always fetched + // from the network. + bool + LoadScriptAsPartOfLoadingServiceWorkerScript() + { + MOZ_ASSERT(IsServiceWorker()); + return mLoadingWorkerScript; + } + + void + SetLoadingWorkerScript(bool aLoadingWorkerScript) + { + // any thread + MOZ_ASSERT(IsServiceWorker()); + mLoadingWorkerScript = aLoadingWorkerScript; + } + + TimeStamp CreationTimeStamp() const + { + return mCreationTimeStamp; + } + + DOMHighResTimeStamp CreationTime() const + { + return mCreationTimeHighRes; + } + + TimeStamp NowBaseTimeStamp() const + { + return mNowBaseTimeStamp; + } + + DOMHighResTimeStamp NowBaseTime() const + { + return mNowBaseTimeHighRes; + } + + nsIPrincipal* + GetPrincipal() const + { + AssertIsOnMainThread(); + return mLoadInfo.mPrincipal; + } + + nsILoadGroup* + GetLoadGroup() const + { + AssertIsOnMainThread(); + return mLoadInfo.mLoadGroup; + } + + // This method allows the principal to be retrieved off the main thread. + // Principals are main-thread objects so the caller must ensure that all + // access occurs on the main thread. + nsIPrincipal* + GetPrincipalDontAssertMainThread() const + { + return mLoadInfo.mPrincipal; + } + + void + SetPrincipal(nsIPrincipal* aPrincipal, nsILoadGroup* aLoadGroup); + + bool + UsesSystemPrincipal() const + { + return mLoadInfo.mPrincipalIsSystem; + } + + const PrincipalInfo& + GetPrincipalInfo() const + { + return *mLoadInfo.mPrincipalInfo; + } + + already_AddRefed<nsIChannel> + ForgetWorkerChannel() + { + AssertIsOnMainThread(); + return mLoadInfo.mChannel.forget(); + } + + nsIDocument* GetDocument() const; + + nsPIDOMWindowInner* + GetWindow() + { + AssertIsOnMainThread(); + return mLoadInfo.mWindow; + } + + nsIContentSecurityPolicy* + GetCSP() const + { + AssertIsOnMainThread(); + return mLoadInfo.mCSP; + } + + void + SetCSP(nsIContentSecurityPolicy* aCSP) + { + AssertIsOnMainThread(); + mLoadInfo.mCSP = aCSP; + } + + net::ReferrerPolicy + GetReferrerPolicy() const + { + return mLoadInfo.mReferrerPolicy; + } + + void + SetReferrerPolicy(net::ReferrerPolicy aReferrerPolicy) + { + mLoadInfo.mReferrerPolicy = aReferrerPolicy; + } + + bool + IsEvalAllowed() const + { + return mLoadInfo.mEvalAllowed; + } + + void + SetEvalAllowed(bool aEvalAllowed) + { + mLoadInfo.mEvalAllowed = aEvalAllowed; + } + + bool + GetReportCSPViolations() const + { + return mLoadInfo.mReportCSPViolations; + } + + void + SetReportCSPViolations(bool aReport) + { + mLoadInfo.mReportCSPViolations = aReport; + } + + bool + XHRParamsAllowed() const + { + return mLoadInfo.mXHRParamsAllowed; + } + + void + SetXHRParamsAllowed(bool aAllowed) + { + mLoadInfo.mXHRParamsAllowed = aAllowed; + } + + LocationInfo& + GetLocationInfo() + { + return mLocationInfo; + } + + void + CopyJSSettings(JSSettings& aSettings) + { + mozilla::MutexAutoLock lock(mMutex); + aSettings = mJSSettings; + } + + void + CopyJSCompartmentOptions(JS::CompartmentOptions& aOptions) + { + mozilla::MutexAutoLock lock(mMutex); + aOptions = IsChromeWorker() ? mJSSettings.chrome.compartmentOptions + : mJSSettings.content.compartmentOptions; + } + + // The ability to be a chrome worker is orthogonal to the type of + // worker [Dedicated|Shared|Service]. + bool + IsChromeWorker() const + { + return mIsChromeWorker; + } + + WorkerType + Type() const + { + return mWorkerType; + } + + bool + IsDedicatedWorker() const + { + return mWorkerType == WorkerTypeDedicated; + } + + bool + IsSharedWorker() const + { + return mWorkerType == WorkerTypeShared; + } + + bool + IsServiceWorker() const + { + return mWorkerType == WorkerTypeService; + } + + nsContentPolicyType + ContentPolicyType() const + { + return ContentPolicyType(mWorkerType); + } + + static nsContentPolicyType + ContentPolicyType(WorkerType aWorkerType) + { + switch (aWorkerType) { + case WorkerTypeDedicated: + return nsIContentPolicy::TYPE_INTERNAL_WORKER; + case WorkerTypeShared: + return nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER; + case WorkerTypeService: + return nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER; + default: + MOZ_ASSERT_UNREACHABLE("Invalid worker type"); + return nsIContentPolicy::TYPE_INVALID; + } + } + + const nsCString& + WorkerName() const + { + MOZ_ASSERT(IsServiceWorker() || IsSharedWorker()); + return mWorkerName; + } + + bool + IsStorageAllowed() const + { + return mLoadInfo.mStorageAllowed; + } + + const PrincipalOriginAttributes& + GetOriginAttributes() const + { + return mLoadInfo.mOriginAttributes; + } + + // Determine if the SW testing per-window flag is set by devtools + bool + ServiceWorkersTestingInWindow() const + { + return mLoadInfo.mServiceWorkersTestingInWindow; + } + + void + GetAllSharedWorkers(nsTArray<RefPtr<SharedWorker>>& aSharedWorkers); + + void + CloseSharedWorkersForWindow(nsPIDOMWindowInner* aWindow); + + void + CloseAllSharedWorkers(); + + void + UpdateOverridenLoadGroup(nsILoadGroup* aBaseLoadGroup); + + already_AddRefed<nsIRunnable> + StealLoadFailedAsyncRunnable() + { + return mLoadInfo.mLoadFailedAsyncRunnable.forget(); + } + + void + FlushReportsToSharedWorkers(nsIConsoleReportCollector* aReporter); + + IMPL_EVENT_HANDLER(message) + IMPL_EVENT_HANDLER(error) + + // Check whether this worker is a secure context. For use from the parent + // thread only; the canonical "is secure context" boolean is stored on the + // compartment of the worker global. The only reason we don't + // AssertIsOnParentThread() here is so we can assert that this value matches + // the one on the compartment, which has to be done from the worker thread. + bool IsSecureContext() const + { + return mIsSecureContext; + } + +#ifdef DEBUG + void + AssertIsOnParentThread() const; + + void + AssertInnerWindowIsCorrect() const; +#else + void + AssertIsOnParentThread() const + { } + + void + AssertInnerWindowIsCorrect() const + { } +#endif +}; + +class WorkerDebugger : public nsIWorkerDebugger { + friend class ::ReportDebuggerErrorRunnable; + friend class ::PostDebuggerMessageRunnable; + + WorkerPrivate* mWorkerPrivate; + bool mIsInitialized; + nsTArray<nsCOMPtr<nsIWorkerDebuggerListener>> mListeners; + +public: + explicit WorkerDebugger(WorkerPrivate* aWorkerPrivate); + + NS_DECL_ISUPPORTS + NS_DECL_NSIWORKERDEBUGGER + + void + AssertIsOnParentThread(); + + void + Close(); + + void + PostMessageToDebugger(const nsAString& aMessage); + + void + ReportErrorToDebugger(const nsAString& aFilename, uint32_t aLineno, + const nsAString& aMessage); + +private: + virtual + ~WorkerDebugger(); + + void + PostMessageToDebuggerOnMainThread(const nsAString& aMessage); + + void + ReportErrorToDebuggerOnMainThread(const nsAString& aFilename, + uint32_t aLineno, + const nsAString& aMessage); +}; + +class WorkerPrivate : public WorkerPrivateParent<WorkerPrivate> +{ + friend class WorkerHolder; + friend class WorkerPrivateParent<WorkerPrivate>; + typedef WorkerPrivateParent<WorkerPrivate> ParentType; + friend class AutoSyncLoopHolder; + + struct TimeoutInfo; + + class MemoryReporter; + friend class MemoryReporter; + + friend class WorkerThread; + + enum GCTimerMode + { + PeriodicTimer = 0, + IdleTimer, + NoTimer + }; + + bool mDebuggerRegistered; + WorkerDebugger* mDebugger; + + Queue<WorkerControlRunnable*, 4> mControlQueue; + Queue<WorkerRunnable*, 4> mDebuggerQueue; + + // Touched on multiple threads, protected with mMutex. + JSContext* mJSContext; + RefPtr<WorkerCrossThreadDispatcher> mCrossThreadDispatcher; + nsTArray<nsCOMPtr<nsIRunnable>> mUndispatchedRunnablesForSyncLoop; + RefPtr<WorkerThread> mThread; + PRThread* mPRThread; + + // Things touched on worker thread only. + RefPtr<WorkerGlobalScope> mScope; + RefPtr<WorkerDebuggerGlobalScope> mDebuggerScope; + nsTArray<ParentType*> mChildWorkers; + nsTObserverArray<WorkerHolder*> mHolders; + nsTArray<nsAutoPtr<TimeoutInfo>> mTimeouts; + uint32_t mDebuggerEventLoopLevel; + RefPtr<ThrottledEventQueue> mMainThreadThrottledEventQueue; + nsCOMPtr<nsIEventTarget> mMainThreadEventTarget; + + struct SyncLoopInfo + { + explicit SyncLoopInfo(EventTarget* aEventTarget); + + RefPtr<EventTarget> mEventTarget; + bool mCompleted; + bool mResult; +#ifdef DEBUG + bool mHasRun; +#endif + }; + + // This is only modified on the worker thread, but in DEBUG builds + // AssertValidSyncLoop function iterates it on other threads. Therefore + // modifications are done with mMutex held *only* in DEBUG builds. + nsTArray<nsAutoPtr<SyncLoopInfo>> mSyncLoopStack; + + nsCOMPtr<nsITimer> mTimer; + nsCOMPtr<nsITimerCallback> mTimerRunnable; + + nsCOMPtr<nsITimer> mGCTimer; + nsCOMPtr<nsIEventTarget> mPeriodicGCTimerTarget; + nsCOMPtr<nsIEventTarget> mIdleGCTimerTarget; + + RefPtr<MemoryReporter> mMemoryReporter; + + // fired on the main thread if the worker script fails to load + nsCOMPtr<nsIRunnable> mLoadFailedRunnable; + + JS::UniqueChars mDefaultLocale; // nulled during worker JSContext init + TimeStamp mKillTime; + uint32_t mErrorHandlerRecursionCount; + uint32_t mNextTimeoutId; + Status mStatus; + bool mFrozen; + bool mTimerRunning; + bool mRunningExpiredTimeouts; + bool mPendingEventQueueClearing; + bool mCancelAllPendingRunnables; + bool mPeriodicGCTimerRunning; + bool mIdleGCTimerRunning; + bool mWorkerScriptExecutedSuccessfully; + bool mPreferences[WORKERPREF_COUNT]; + bool mOnLine; + +protected: + ~WorkerPrivate(); + +public: + static already_AddRefed<WorkerPrivate> + Constructor(const GlobalObject& aGlobal, const nsAString& aScriptURL, + ErrorResult& aRv); + + static already_AddRefed<WorkerPrivate> + Constructor(const GlobalObject& aGlobal, const nsAString& aScriptURL, + bool aIsChromeWorker, WorkerType aWorkerType, + const nsACString& aSharedWorkerName, + WorkerLoadInfo* aLoadInfo, ErrorResult& aRv); + + static already_AddRefed<WorkerPrivate> + Constructor(JSContext* aCx, const nsAString& aScriptURL, bool aIsChromeWorker, + WorkerType aWorkerType, const nsACString& aSharedWorkerName, + WorkerLoadInfo* aLoadInfo, ErrorResult& aRv); + + static bool + WorkerAvailable(JSContext* /* unused */, JSObject* /* unused */); + + enum LoadGroupBehavior + { + InheritLoadGroup, + OverrideLoadGroup + }; + + static nsresult + GetLoadInfo(JSContext* aCx, nsPIDOMWindowInner* aWindow, + WorkerPrivate* aParent, + const nsAString& aScriptURL, bool aIsChromeWorker, + LoadGroupBehavior aLoadGroupBehavior, WorkerType aWorkerType, + WorkerLoadInfo* aLoadInfo); + + static void + OverrideLoadInfoLoadGroup(WorkerLoadInfo& aLoadInfo); + + bool + IsDebuggerRegistered() + { + AssertIsOnMainThread(); + + // No need to lock here since this is only ever modified by the same thread. + return mDebuggerRegistered; + } + + void + SetIsDebuggerRegistered(bool aDebuggerRegistered) + { + AssertIsOnMainThread(); + + MutexAutoLock lock(mMutex); + + MOZ_ASSERT(mDebuggerRegistered != aDebuggerRegistered); + mDebuggerRegistered = aDebuggerRegistered; + + mCondVar.Notify(); + } + + void + WaitForIsDebuggerRegistered(bool aDebuggerRegistered) + { + AssertIsOnParentThread(); + + MOZ_ASSERT(!NS_IsMainThread()); + + MutexAutoLock lock(mMutex); + + while (mDebuggerRegistered != aDebuggerRegistered) { + mCondVar.Wait(); + } + } + + WorkerDebugger* + Debugger() const + { + AssertIsOnMainThread(); + + MOZ_ASSERT(mDebugger); + return mDebugger; + } + + void + SetDebugger(WorkerDebugger* aDebugger) + { + AssertIsOnMainThread(); + + MOZ_ASSERT(mDebugger != aDebugger); + mDebugger = aDebugger; + } + + JS::UniqueChars + AdoptDefaultLocale() + { + MOZ_ASSERT(mDefaultLocale, + "the default locale must have been successfully set for anyone " + "to be trying to adopt it"); + return Move(mDefaultLocale); + } + + void + DoRunLoop(JSContext* aCx); + + bool + InterruptCallback(JSContext* aCx); + + nsresult + IsOnCurrentThread(bool* aIsOnCurrentThread); + + bool + CloseInternal(JSContext* aCx) + { + AssertIsOnWorkerThread(); + return NotifyInternal(aCx, Closing); + } + + bool + FreezeInternal(); + + bool + ThawInternal(); + + void + TraverseTimeouts(nsCycleCollectionTraversalCallback& aCallback); + + void + UnlinkTimeouts(); + + bool + ModifyBusyCountFromWorker(bool aIncrease); + + bool + AddChildWorker(ParentType* aChildWorker); + + void + RemoveChildWorker(ParentType* aChildWorker); + + void + PostMessageToParent(JSContext* aCx, + JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + ErrorResult& aRv) + { + PostMessageToParentInternal(aCx, aMessage, aTransferable, aRv); + } + + void + PostMessageToParentMessagePort( + JSContext* aCx, + JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + ErrorResult& aRv); + + void + EnterDebuggerEventLoop(); + + void + LeaveDebuggerEventLoop(); + + void + PostMessageToDebugger(const nsAString& aMessage); + + void + SetDebuggerImmediate(Function& aHandler, ErrorResult& aRv); + + void + ReportErrorToDebugger(const nsAString& aFilename, uint32_t aLineno, + const nsAString& aMessage); + + bool + NotifyInternal(JSContext* aCx, Status aStatus); + + void + ReportError(JSContext* aCx, JS::ConstUTF8CharsZ aToStringResult, + JSErrorReport* aReport); + + static void + ReportErrorToConsole(const char* aMessage); + + int32_t + SetTimeout(JSContext* aCx, nsIScriptTimeoutHandler* aHandler, + int32_t aTimeout, bool aIsInterval, + ErrorResult& aRv); + + void + ClearTimeout(int32_t aId); + + bool + RunExpiredTimeouts(JSContext* aCx); + + bool + RescheduleTimeoutTimer(JSContext* aCx); + + void + UpdateContextOptionsInternal(JSContext* aCx, const JS::ContextOptions& aContextOptions); + + void + UpdateLanguagesInternal(const nsTArray<nsString>& aLanguages); + + void + UpdatePreferenceInternal(WorkerPreference aPref, bool aValue); + + void + UpdateJSWorkerMemoryParameterInternal(JSContext* aCx, JSGCParamKey key, uint32_t aValue); + + enum WorkerRanOrNot { + WorkerNeverRan = 0, + WorkerRan + }; + + void + ScheduleDeletion(WorkerRanOrNot aRanOrNot); + + bool + CollectRuntimeStats(JS::RuntimeStats* aRtStats, bool aAnonymize); + +#ifdef JS_GC_ZEAL + void + UpdateGCZealInternal(JSContext* aCx, uint8_t aGCZeal, uint32_t aFrequency); +#endif + + void + GarbageCollectInternal(JSContext* aCx, bool aShrinking, + bool aCollectChildren); + + void + CycleCollectInternal(bool aCollectChildren); + + void + OfflineStatusChangeEventInternal(bool aIsOffline); + + void + MemoryPressureInternal(); + + JSContext* + GetJSContext() const + { + AssertIsOnWorkerThread(); + return mJSContext; + } + + WorkerGlobalScope* + GlobalScope() const + { + AssertIsOnWorkerThread(); + return mScope; + } + + WorkerDebuggerGlobalScope* + DebuggerGlobalScope() const + { + AssertIsOnWorkerThread(); + return mDebuggerScope; + } + + void + SetThread(WorkerThread* aThread); + + void + AssertIsOnWorkerThread() const +#ifdef DEBUG + ; +#else + { } +#endif + + WorkerCrossThreadDispatcher* + GetCrossThreadDispatcher(); + + // This may block! + void + BeginCTypesCall(); + + // This may block! + void + EndCTypesCall(); + + void + BeginCTypesCallback() + { + // If a callback is beginning then we need to do the exact same thing as + // when a ctypes call ends. + EndCTypesCall(); + } + + void + EndCTypesCallback() + { + // If a callback is ending then we need to do the exact same thing as + // when a ctypes call begins. + BeginCTypesCall(); + } + + bool + ConnectMessagePort(JSContext* aCx, MessagePortIdentifier& aIdentifier); + + WorkerGlobalScope* + GetOrCreateGlobalScope(JSContext* aCx); + + WorkerDebuggerGlobalScope* + CreateDebuggerGlobalScope(JSContext* aCx); + + bool + RegisterBindings(JSContext* aCx, JS::Handle<JSObject*> aGlobal); + + bool + RegisterDebuggerBindings(JSContext* aCx, JS::Handle<JSObject*> aGlobal); + +#define WORKER_SIMPLE_PREF(name, getter, NAME) \ + bool \ + getter() const \ + { \ + AssertIsOnWorkerThread(); \ + return mPreferences[WORKERPREF_##NAME]; \ + } +#define WORKER_PREF(name, callback) +#include "WorkerPrefs.h" +#undef WORKER_SIMPLE_PREF +#undef WORKER_PREF + + bool + OnLine() const + { + AssertIsOnWorkerThread(); + return mOnLine; + } + + void + StopSyncLoop(nsIEventTarget* aSyncLoopTarget, bool aResult); + + bool + AllPendingRunnablesShouldBeCanceled() const + { + return mCancelAllPendingRunnables; + } + + void + ClearMainEventQueue(WorkerRanOrNot aRanOrNot); + + void + ClearDebuggerEventQueue(); + + void + OnProcessNextEvent(); + + void + AfterProcessNextEvent(); + + void + AssertValidSyncLoop(nsIEventTarget* aSyncLoopTarget) +#ifdef DEBUG + ; +#else + { } +#endif + + void + SetWorkerScriptExecutedSuccessfully() + { + AssertIsOnWorkerThread(); + // Should only be called once! + MOZ_ASSERT(!mWorkerScriptExecutedSuccessfully); + mWorkerScriptExecutedSuccessfully = true; + } + + // Only valid after CompileScriptRunnable has finished running! + bool + WorkerScriptExecutedSuccessfully() const + { + AssertIsOnWorkerThread(); + return mWorkerScriptExecutedSuccessfully; + } + + void + MaybeDispatchLoadFailedRunnable(); + + // Get the event target to use when dispatching to the main thread + // from this Worker thread. This may be the main thread itself or + // a ThrottledEventQueue to the main thread. + nsIEventTarget* + MainThreadEventTarget(); + + nsresult + DispatchToMainThread(nsIRunnable* aRunnable, + uint32_t aFlags = NS_DISPATCH_NORMAL); + + nsresult + DispatchToMainThread(already_AddRefed<nsIRunnable> aRunnable, + uint32_t aFlags = NS_DISPATCH_NORMAL); + +private: + WorkerPrivate(WorkerPrivate* aParent, + const nsAString& aScriptURL, bool aIsChromeWorker, + WorkerType aWorkerType, const nsACString& aSharedWorkerName, + WorkerLoadInfo& aLoadInfo); + + bool + MayContinueRunning() + { + AssertIsOnWorkerThread(); + + Status status; + { + MutexAutoLock lock(mMutex); + status = mStatus; + } + + if (status < Terminating) { + return true; + } + + return false; + } + + void + CancelAllTimeouts(); + + enum class ProcessAllControlRunnablesResult + { + // We did not process anything. + Nothing, + // We did process something, states may have changed, but we can keep + // executing script. + MayContinue, + // We did process something, and should not continue executing script. + Abort + }; + + ProcessAllControlRunnablesResult + ProcessAllControlRunnables() + { + MutexAutoLock lock(mMutex); + return ProcessAllControlRunnablesLocked(); + } + + ProcessAllControlRunnablesResult + ProcessAllControlRunnablesLocked(); + + void + EnableMemoryReporter(); + + void + DisableMemoryReporter(); + + void + WaitForWorkerEvents(PRIntervalTime interval = PR_INTERVAL_NO_TIMEOUT); + + void + PostMessageToParentInternal(JSContext* aCx, + JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + ErrorResult& aRv); + + void + GetAllPreferences(bool aPreferences[WORKERPREF_COUNT]) const + { + AssertIsOnWorkerThread(); + memcpy(aPreferences, mPreferences, WORKERPREF_COUNT * sizeof(bool)); + } + + already_AddRefed<nsIEventTarget> + CreateNewSyncLoop(); + + bool + RunCurrentSyncLoop(); + + bool + DestroySyncLoop(uint32_t aLoopIndex, nsIThreadInternal* aThread = nullptr); + + void + InitializeGCTimers(); + + void + SetGCTimerMode(GCTimerMode aMode); + + void + ShutdownGCTimers(); + + bool + AddHolder(WorkerHolder* aHolder, Status aFailStatus); + + void + RemoveHolder(WorkerHolder* aHolder); + + void + NotifyHolders(JSContext* aCx, Status aStatus); + + bool + HasActiveHolders() + { + return !(mChildWorkers.IsEmpty() && mTimeouts.IsEmpty() && + mHolders.IsEmpty()); + } +}; + +// This class is only used to trick the DOM bindings. We never create +// instances of it, and static_casting to it is fine since it doesn't add +// anything to WorkerPrivate. +class ChromeWorkerPrivate : public WorkerPrivate +{ +public: + static already_AddRefed<ChromeWorkerPrivate> + Constructor(const GlobalObject& aGlobal, const nsAString& aScriptURL, + ErrorResult& rv); + + static bool + WorkerAvailable(JSContext* aCx, JSObject* /* unused */); + +private: + ChromeWorkerPrivate() = delete; + ChromeWorkerPrivate(const ChromeWorkerPrivate& aRHS) = delete; + ChromeWorkerPrivate& operator =(const ChromeWorkerPrivate& aRHS) = delete; +}; + +WorkerPrivate* +GetWorkerPrivateFromContext(JSContext* aCx); + +WorkerPrivate* +GetCurrentThreadWorkerPrivate(); + +bool +IsCurrentThreadRunningChromeWorker(); + +JSContext* +GetCurrentThreadJSContext(); + +JSObject* +GetCurrentThreadWorkerGlobal(); + +class AutoSyncLoopHolder +{ + WorkerPrivate* mWorkerPrivate; + nsCOMPtr<nsIEventTarget> mTarget; + uint32_t mIndex; + +public: + explicit AutoSyncLoopHolder(WorkerPrivate* aWorkerPrivate) + : mWorkerPrivate(aWorkerPrivate) + , mTarget(aWorkerPrivate->CreateNewSyncLoop()) + , mIndex(aWorkerPrivate->mSyncLoopStack.Length() - 1) + { + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + ~AutoSyncLoopHolder() + { + if (mWorkerPrivate) { + mWorkerPrivate->AssertIsOnWorkerThread(); + mWorkerPrivate->StopSyncLoop(mTarget, false); + mWorkerPrivate->DestroySyncLoop(mIndex); + } + } + + bool + Run() + { + WorkerPrivate* workerPrivate = mWorkerPrivate; + mWorkerPrivate = nullptr; + + workerPrivate->AssertIsOnWorkerThread(); + + return workerPrivate->RunCurrentSyncLoop(); + } + + nsIEventTarget* + EventTarget() const + { + return mTarget; + } +}; + +class TimerThreadEventTarget final : public nsIEventTarget +{ + ~TimerThreadEventTarget(); + + WorkerPrivate* mWorkerPrivate; + RefPtr<WorkerRunnable> mWorkerRunnable; +public: + NS_DECL_THREADSAFE_ISUPPORTS + + TimerThreadEventTarget(WorkerPrivate* aWorkerPrivate, + WorkerRunnable* aWorkerRunnable); + +protected: + NS_IMETHOD + DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) override; + + + NS_IMETHOD + Dispatch(already_AddRefed<nsIRunnable> aRunnable, uint32_t aFlags) override; + + NS_IMETHOD + DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t) override; + + NS_IMETHOD + IsOnCurrentThread(bool* aIsOnCurrentThread) override; +}; + +END_WORKERS_NAMESPACE + +#endif /* mozilla_dom_workers_workerprivate_h__ */ diff --git a/dom/workers/WorkerRunnable.cpp b/dom/workers/WorkerRunnable.cpp new file mode 100644 index 000000000..1e16d7254 --- /dev/null +++ b/dom/workers/WorkerRunnable.cpp @@ -0,0 +1,777 @@ +/* -*- 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 "WorkerRunnable.h" + +#include "nsGlobalWindow.h" +#include "nsIEventTarget.h" +#include "nsIGlobalObject.h" +#include "nsIRunnable.h" +#include "nsThreadUtils.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/Telemetry.h" + +#include "js/RootingAPI.h" +#include "js/Value.h" + +#include "WorkerPrivate.h" +#include "WorkerScope.h" + +USING_WORKERS_NAMESPACE + +namespace { + +const nsIID kWorkerRunnableIID = { + 0x320cc0b5, 0xef12, 0x4084, { 0x88, 0x6e, 0xca, 0x6a, 0x81, 0xe4, 0x1d, 0x68 } +}; + +} // namespace + +#ifdef DEBUG +WorkerRunnable::WorkerRunnable(WorkerPrivate* aWorkerPrivate, + TargetAndBusyBehavior aBehavior) +: mWorkerPrivate(aWorkerPrivate), mBehavior(aBehavior), mCanceled(0), + mCallingCancelWithinRun(false) +{ + MOZ_ASSERT(aWorkerPrivate); +} +#endif + +bool +WorkerRunnable::IsDebuggerRunnable() const +{ + return false; +} + +nsIGlobalObject* +WorkerRunnable::DefaultGlobalObject() const +{ + if (IsDebuggerRunnable()) { + return mWorkerPrivate->DebuggerGlobalScope(); + } else { + return mWorkerPrivate->GlobalScope(); + } +} + +bool +WorkerRunnable::PreDispatch(WorkerPrivate* aWorkerPrivate) +{ +#ifdef DEBUG + MOZ_ASSERT(aWorkerPrivate); + + switch (mBehavior) { + case ParentThreadUnchangedBusyCount: + aWorkerPrivate->AssertIsOnWorkerThread(); + break; + + case WorkerThreadModifyBusyCount: + case WorkerThreadUnchangedBusyCount: + aWorkerPrivate->AssertIsOnParentThread(); + break; + + default: + MOZ_ASSERT_UNREACHABLE("Unknown behavior!"); + } +#endif + + if (mBehavior == WorkerThreadModifyBusyCount) { + return aWorkerPrivate->ModifyBusyCount(true); + } + + return true; +} + +bool +WorkerRunnable::Dispatch() +{ + bool ok = PreDispatch(mWorkerPrivate); + if (ok) { + ok = DispatchInternal(); + } + PostDispatch(mWorkerPrivate, ok); + return ok; +} + +bool +WorkerRunnable::DispatchInternal() +{ + RefPtr<WorkerRunnable> runnable(this); + + if (mBehavior == WorkerThreadModifyBusyCount || + mBehavior == WorkerThreadUnchangedBusyCount) { + if (IsDebuggerRunnable()) { + return NS_SUCCEEDED(mWorkerPrivate->DispatchDebuggerRunnable(runnable.forget())); + } else { + return NS_SUCCEEDED(mWorkerPrivate->Dispatch(runnable.forget())); + } + } + + MOZ_ASSERT(mBehavior == ParentThreadUnchangedBusyCount); + + if (WorkerPrivate* parent = mWorkerPrivate->GetParent()) { + return NS_SUCCEEDED(parent->Dispatch(runnable.forget())); + } + + return NS_SUCCEEDED(mWorkerPrivate->DispatchToMainThread(runnable.forget())); +} + +void +WorkerRunnable::PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) +{ + MOZ_ASSERT(aWorkerPrivate); + +#ifdef DEBUG + switch (mBehavior) { + case ParentThreadUnchangedBusyCount: + aWorkerPrivate->AssertIsOnWorkerThread(); + break; + + case WorkerThreadModifyBusyCount: + aWorkerPrivate->AssertIsOnParentThread(); + break; + + case WorkerThreadUnchangedBusyCount: + aWorkerPrivate->AssertIsOnParentThread(); + break; + + default: + MOZ_ASSERT_UNREACHABLE("Unknown behavior!"); + } +#endif + + if (!aDispatchResult) { + if (mBehavior == WorkerThreadModifyBusyCount) { + aWorkerPrivate->ModifyBusyCount(false); + } + } +} + +bool +WorkerRunnable::PreRun(WorkerPrivate* aWorkerPrivate) +{ + return true; +} + +void +WorkerRunnable::PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, + bool aRunResult) +{ + MOZ_ASSERT(aCx); + MOZ_ASSERT(aWorkerPrivate); + +#ifdef DEBUG + switch (mBehavior) { + case ParentThreadUnchangedBusyCount: + aWorkerPrivate->AssertIsOnParentThread(); + break; + + case WorkerThreadModifyBusyCount: + aWorkerPrivate->AssertIsOnWorkerThread(); + break; + + case WorkerThreadUnchangedBusyCount: + aWorkerPrivate->AssertIsOnWorkerThread(); + break; + + default: + MOZ_ASSERT_UNREACHABLE("Unknown behavior!"); + } +#endif + + if (mBehavior == WorkerThreadModifyBusyCount) { + aWorkerPrivate->ModifyBusyCountFromWorker(false); + } +} + +// static +WorkerRunnable* +WorkerRunnable::FromRunnable(nsIRunnable* aRunnable) +{ + MOZ_ASSERT(aRunnable); + + WorkerRunnable* runnable; + nsresult rv = aRunnable->QueryInterface(kWorkerRunnableIID, + reinterpret_cast<void**>(&runnable)); + if (NS_FAILED(rv)) { + return nullptr; + } + + MOZ_ASSERT(runnable); + return runnable; +} + +NS_IMPL_ADDREF(WorkerRunnable) +NS_IMPL_RELEASE(WorkerRunnable) + +NS_INTERFACE_MAP_BEGIN(WorkerRunnable) + NS_INTERFACE_MAP_ENTRY(nsIRunnable) + NS_INTERFACE_MAP_ENTRY(nsICancelableRunnable) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIRunnable) + // kWorkerRunnableIID is special in that it does not AddRef its result. + if (aIID.Equals(kWorkerRunnableIID)) { + *aInstancePtr = this; + return NS_OK; + } + else +NS_INTERFACE_MAP_END + +NS_IMETHODIMP +WorkerRunnable::Run() +{ + bool targetIsWorkerThread = mBehavior == WorkerThreadModifyBusyCount || + mBehavior == WorkerThreadUnchangedBusyCount; + +#ifdef DEBUG + MOZ_ASSERT_IF(mCallingCancelWithinRun, targetIsWorkerThread); + if (targetIsWorkerThread) { + mWorkerPrivate->AssertIsOnWorkerThread(); + } + else { + MOZ_ASSERT(mBehavior == ParentThreadUnchangedBusyCount); + mWorkerPrivate->AssertIsOnParentThread(); + } +#endif + + if (IsCanceled() && !mCallingCancelWithinRun) { + return NS_OK; + } + + if (targetIsWorkerThread && + mWorkerPrivate->AllPendingRunnablesShouldBeCanceled() && + !IsCanceled() && !mCallingCancelWithinRun) { + + // Prevent recursion. + mCallingCancelWithinRun = true; + + Cancel(); + + MOZ_ASSERT(mCallingCancelWithinRun); + mCallingCancelWithinRun = false; + + MOZ_ASSERT(IsCanceled(), "Subclass Cancel() didn't set IsCanceled()!"); + + if (mBehavior == WorkerThreadModifyBusyCount) { + mWorkerPrivate->ModifyBusyCountFromWorker(false); + } + + return NS_OK; + } + + bool result = PreRun(mWorkerPrivate); + if (!result) { + MOZ_ASSERT(targetIsWorkerThread, + "The only PreRun implementation that can fail is " + "ScriptExecutorRunnable"); + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(!JS_IsExceptionPending(mWorkerPrivate->GetJSContext())); + // We can't enter a useful compartment on the JSContext here; just pass it + // in as-is. + PostRun(mWorkerPrivate->GetJSContext(), mWorkerPrivate, false); + return NS_ERROR_FAILURE; + } + + // Track down the appropriate global, if any, to use for the AutoEntryScript. + nsCOMPtr<nsIGlobalObject> globalObject; + bool isMainThread = !targetIsWorkerThread && !mWorkerPrivate->GetParent(); + MOZ_ASSERT(isMainThread == NS_IsMainThread()); + RefPtr<WorkerPrivate> kungFuDeathGrip; + if (targetIsWorkerThread) { + JSContext* cx = GetCurrentThreadJSContext(); + if (NS_WARN_IF(!cx)) { + return NS_ERROR_FAILURE; + } + + JSObject* global = JS::CurrentGlobalOrNull(cx); + if (global) { + globalObject = xpc::NativeGlobal(global); + } else { + globalObject = DefaultGlobalObject(); + } + + // We may still not have a globalObject here: in the case of + // CompileScriptRunnable, we don't actually create the global object until + // we have the script data, which happens in a syncloop under + // CompileScriptRunnable::WorkerRun, so we can't assert that it got created + // in the PreRun call above. + } else { + kungFuDeathGrip = mWorkerPrivate; + if (isMainThread) { + globalObject = nsGlobalWindow::Cast(mWorkerPrivate->GetWindow()); + } else { + globalObject = mWorkerPrivate->GetParent()->GlobalScope(); + } + } + + // We might run script as part of WorkerRun, so we need an AutoEntryScript. + // This is part of the HTML spec for workers at: + // http://www.whatwg.org/specs/web-apps/current-work/#run-a-worker + // If we don't have a globalObject we have to use an AutoJSAPI instead, but + // this is OK as we won't be running script in these circumstances. + Maybe<mozilla::dom::AutoJSAPI> maybeJSAPI; + Maybe<mozilla::dom::AutoEntryScript> aes; + JSContext* cx; + AutoJSAPI* jsapi; + if (globalObject) { + aes.emplace(globalObject, "Worker runnable", isMainThread); + jsapi = aes.ptr(); + cx = aes->cx(); + } else { + maybeJSAPI.emplace(); + maybeJSAPI->Init(); + jsapi = maybeJSAPI.ptr(); + cx = jsapi->cx(); + } + + // Note that we can't assert anything about mWorkerPrivate->GetWrapper() + // existing, since it may in fact have been GCed (and we may be one of the + // runnables cleaning up the worker as a result). + + // If we are on the parent thread and that thread is not the main thread, + // then we must be a dedicated worker (because there are no + // Shared/ServiceWorkers whose parent is itself a worker) and then we + // definitely have a globalObject. If it _is_ the main thread, globalObject + // can be null for workers started from JSMs or other non-window contexts, + // sadly. + MOZ_ASSERT_IF(!targetIsWorkerThread && !isMainThread, + mWorkerPrivate->IsDedicatedWorker() && globalObject); + + // If we're on the parent thread we might be in a null compartment in the + // situation described above when globalObject is null. Make sure to enter + // the compartment of the worker's reflector if there is one. There might + // not be one if we're just starting to compile the script for this worker. + Maybe<JSAutoCompartment> ac; + if (!targetIsWorkerThread && mWorkerPrivate->GetWrapper()) { + // If we're on the parent thread and have a reflector and a globalObject, + // then the compartments of cx, globalObject, and the worker's reflector + // should all match. + MOZ_ASSERT_IF(globalObject, + js::GetObjectCompartment(mWorkerPrivate->GetWrapper()) == + js::GetContextCompartment(cx)); + MOZ_ASSERT_IF(globalObject, + js::GetObjectCompartment(mWorkerPrivate->GetWrapper()) == + js::GetObjectCompartment(globalObject->GetGlobalJSObject())); + + // If we're on the parent thread and have a reflector, then our + // JSContext had better be either in the null compartment (and hence + // have no globalObject) or in the compartment of our reflector. + MOZ_ASSERT(!js::GetContextCompartment(cx) || + js::GetObjectCompartment(mWorkerPrivate->GetWrapper()) == + js::GetContextCompartment(cx), + "Must either be in the null compartment or in our reflector " + "compartment"); + + ac.emplace(cx, mWorkerPrivate->GetWrapper()); + } + + MOZ_ASSERT(!jsapi->HasException()); + result = WorkerRun(cx, mWorkerPrivate); + MOZ_ASSERT_IF(result, !jsapi->HasException()); + jsapi->ReportException(); + + // We can't even assert that this didn't create our global, since in the case + // of CompileScriptRunnable it _does_. + + // It would be nice to avoid passing a JSContext to PostRun, but in the case + // of ScriptExecutorRunnable we need to know the current compartment on the + // JSContext (the one we set up based on the global returned from PreRun) so + // that we can sanely do exception reporting. In particular, we want to make + // sure that we do our JS_SetPendingException while still in that compartment, + // because otherwise we might end up trying to create a cross-compartment + // wrapper when we try to move the JS exception from our runnable's + // ErrorResult to the JSContext, and that's not desirable in this case. + // + // We _could_ skip passing a JSContext here and then in + // ScriptExecutorRunnable::PostRun end up grabbing it from the WorkerPrivate + // and looking at its current compartment. But that seems like slightly weird + // action-at-a-distance... + // + // In any case, we do NOT try to change the compartment on the JSContext at + // this point; in the one case in which we could do that + // (CompileScriptRunnable) it actually doesn't matter which compartment we're + // in for PostRun. + PostRun(cx, mWorkerPrivate, result); + MOZ_ASSERT(!jsapi->HasException()); + + return result ? NS_OK : NS_ERROR_FAILURE; +} + +nsresult +WorkerRunnable::Cancel() +{ + uint32_t canceledCount = ++mCanceled; + + MOZ_ASSERT(canceledCount, "Cancel() overflow!"); + + // The docs say that Cancel() should not be called more than once and that we + // should throw NS_ERROR_UNEXPECTED if it is. + return (canceledCount == 1) ? NS_OK : NS_ERROR_UNEXPECTED; +} + +void +WorkerDebuggerRunnable::PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) +{ +} + +WorkerSyncRunnable::WorkerSyncRunnable(WorkerPrivate* aWorkerPrivate, + nsIEventTarget* aSyncLoopTarget) +: WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), + mSyncLoopTarget(aSyncLoopTarget) +{ +#ifdef DEBUG + if (mSyncLoopTarget) { + mWorkerPrivate->AssertValidSyncLoop(mSyncLoopTarget); + } +#endif +} + +WorkerSyncRunnable::WorkerSyncRunnable( + WorkerPrivate* aWorkerPrivate, + already_AddRefed<nsIEventTarget>&& aSyncLoopTarget) +: WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), + mSyncLoopTarget(aSyncLoopTarget) +{ +#ifdef DEBUG + if (mSyncLoopTarget) { + mWorkerPrivate->AssertValidSyncLoop(mSyncLoopTarget); + } +#endif +} + +WorkerSyncRunnable::~WorkerSyncRunnable() +{ +} + +bool +WorkerSyncRunnable::DispatchInternal() +{ + if (mSyncLoopTarget) { + RefPtr<WorkerSyncRunnable> runnable(this); + return NS_SUCCEEDED(mSyncLoopTarget->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL)); + } + + return WorkerRunnable::DispatchInternal(); +} + +void +MainThreadWorkerSyncRunnable::PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) +{ +} + +MainThreadStopSyncLoopRunnable::MainThreadStopSyncLoopRunnable( + WorkerPrivate* aWorkerPrivate, + already_AddRefed<nsIEventTarget>&& aSyncLoopTarget, + bool aResult) +: WorkerSyncRunnable(aWorkerPrivate, Move(aSyncLoopTarget)), mResult(aResult) +{ + AssertIsOnMainThread(); +#ifdef DEBUG + mWorkerPrivate->AssertValidSyncLoop(mSyncLoopTarget); +#endif +} + +nsresult +MainThreadStopSyncLoopRunnable::Cancel() +{ + nsresult rv = Run(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Run() failed"); + + nsresult rv2 = WorkerSyncRunnable::Cancel(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv2), "Cancel() failed"); + + return NS_FAILED(rv) ? rv : rv2; +} + +bool +MainThreadStopSyncLoopRunnable::WorkerRun(JSContext* aCx, + WorkerPrivate* aWorkerPrivate) +{ + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(mSyncLoopTarget); + + nsCOMPtr<nsIEventTarget> syncLoopTarget; + mSyncLoopTarget.swap(syncLoopTarget); + + aWorkerPrivate->StopSyncLoop(syncLoopTarget, mResult); + return true; +} + +bool +MainThreadStopSyncLoopRunnable::DispatchInternal() +{ + MOZ_ASSERT(mSyncLoopTarget); + + RefPtr<MainThreadStopSyncLoopRunnable> runnable(this); + return NS_SUCCEEDED(mSyncLoopTarget->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL)); +} + +void +MainThreadStopSyncLoopRunnable::PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) +{ +} + +#ifdef DEBUG +WorkerControlRunnable::WorkerControlRunnable(WorkerPrivate* aWorkerPrivate, + TargetAndBusyBehavior aBehavior) +: WorkerRunnable(aWorkerPrivate, aBehavior) +{ + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aBehavior == ParentThreadUnchangedBusyCount || + aBehavior == WorkerThreadUnchangedBusyCount, + "WorkerControlRunnables should not modify the busy count"); +} +#endif + +nsresult +WorkerControlRunnable::Cancel() +{ + if (NS_FAILED(Run())) { + NS_WARNING("WorkerControlRunnable::Run() failed."); + } + + return WorkerRunnable::Cancel(); +} + +bool +WorkerControlRunnable::DispatchInternal() +{ + RefPtr<WorkerControlRunnable> runnable(this); + + if (mBehavior == WorkerThreadUnchangedBusyCount) { + return NS_SUCCEEDED(mWorkerPrivate->DispatchControlRunnable(runnable.forget())); + } + + if (WorkerPrivate* parent = mWorkerPrivate->GetParent()) { + return NS_SUCCEEDED(parent->DispatchControlRunnable(runnable.forget())); + } + + return NS_SUCCEEDED(mWorkerPrivate->DispatchToMainThread(runnable.forget())); +} + +NS_IMPL_ISUPPORTS_INHERITED0(WorkerControlRunnable, WorkerRunnable) + +WorkerMainThreadRunnable::WorkerMainThreadRunnable(WorkerPrivate* aWorkerPrivate, + const nsACString& aTelemetryKey) +: mWorkerPrivate(aWorkerPrivate) +, mTelemetryKey(aTelemetryKey) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); +} + +void +WorkerMainThreadRunnable::Dispatch(ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + TimeStamp startTime = TimeStamp::NowLoRes(); + + AutoSyncLoopHolder syncLoop(mWorkerPrivate); + + mSyncLoopTarget = syncLoop.EventTarget(); + + DebugOnly<nsresult> rv = mWorkerPrivate->DispatchToMainThread(this); + MOZ_ASSERT(NS_SUCCEEDED(rv), + "Should only fail after xpcom-shutdown-threads and we're gone by then"); + + if (!syncLoop.Run()) { + aRv.ThrowUncatchableException(); + } + + Telemetry::Accumulate(Telemetry::SYNC_WORKER_OPERATION, mTelemetryKey, + static_cast<uint32_t>((TimeStamp::NowLoRes() - startTime) + .ToMilliseconds())); + Unused << startTime; // Shut the compiler up. +} + +NS_IMETHODIMP +WorkerMainThreadRunnable::Run() +{ + AssertIsOnMainThread(); + + bool runResult = MainThreadRun(); + + RefPtr<MainThreadStopSyncLoopRunnable> response = + new MainThreadStopSyncLoopRunnable(mWorkerPrivate, + mSyncLoopTarget.forget(), + runResult); + + MOZ_ALWAYS_TRUE(response->Dispatch()); + + return NS_OK; +} + +WorkerCheckAPIExposureOnMainThreadRunnable::WorkerCheckAPIExposureOnMainThreadRunnable(WorkerPrivate* aWorkerPrivate): + WorkerMainThreadRunnable(aWorkerPrivate, + NS_LITERAL_CSTRING("WorkerCheckAPIExposureOnMainThread")) +{} + +WorkerCheckAPIExposureOnMainThreadRunnable::~WorkerCheckAPIExposureOnMainThreadRunnable() +{} + +bool +WorkerCheckAPIExposureOnMainThreadRunnable::Dispatch() +{ + ErrorResult rv; + WorkerMainThreadRunnable::Dispatch(rv); + bool ok = !rv.Failed(); + rv.SuppressException(); + return ok; +} + +bool +WorkerSameThreadRunnable::PreDispatch(WorkerPrivate* aWorkerPrivate) +{ + // We don't call WorkerRunnable::PreDispatch, because we're using + // WorkerThreadModifyBusyCount for mBehavior, and WorkerRunnable will assert + // that PreDispatch is on the parent thread in that case. + aWorkerPrivate->AssertIsOnWorkerThread(); + return true; +} + +void +WorkerSameThreadRunnable::PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) +{ + // We don't call WorkerRunnable::PostDispatch, because we're using + // WorkerThreadModifyBusyCount for mBehavior, and WorkerRunnable will assert + // that PostDispatch is on the parent thread in that case. + aWorkerPrivate->AssertIsOnWorkerThread(); + if (aDispatchResult) { + DebugOnly<bool> willIncrement = aWorkerPrivate->ModifyBusyCountFromWorker(true); + // Should never fail since if this thread is still running, so should the + // parent and it should be able to process a control runnable. + MOZ_ASSERT(willIncrement); + } +} + +WorkerProxyToMainThreadRunnable::WorkerProxyToMainThreadRunnable(WorkerPrivate* aWorkerPrivate) + : mWorkerPrivate(aWorkerPrivate) +{ + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); +} + +WorkerProxyToMainThreadRunnable::~WorkerProxyToMainThreadRunnable() +{} + +bool +WorkerProxyToMainThreadRunnable::Dispatch() +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (NS_WARN_IF(!HoldWorker())) { + RunBackOnWorkerThread(); + return false; + } + + if (NS_WARN_IF(NS_FAILED(mWorkerPrivate->DispatchToMainThread(this)))) { + ReleaseWorker(); + RunBackOnWorkerThread(); + return false; + } + + return true; +} + +NS_IMETHODIMP +WorkerProxyToMainThreadRunnable::Run() +{ + AssertIsOnMainThread(); + RunOnMainThread(); + PostDispatchOnMainThread(); + return NS_OK; +} + +void +WorkerProxyToMainThreadRunnable::PostDispatchOnMainThread() +{ + class ReleaseRunnable final : public MainThreadWorkerControlRunnable + { + RefPtr<WorkerProxyToMainThreadRunnable> mRunnable; + + public: + ReleaseRunnable(WorkerPrivate* aWorkerPrivate, + WorkerProxyToMainThreadRunnable* aRunnable) + : MainThreadWorkerControlRunnable(aWorkerPrivate) + , mRunnable(aRunnable) + { + MOZ_ASSERT(aRunnable); + } + + // We must call RunBackOnWorkerThread() also if the runnable is canceled. + nsresult + Cancel() override + { + WorkerRun(nullptr, mWorkerPrivate); + return MainThreadWorkerControlRunnable::Cancel(); + } + + virtual bool + WorkerRun(JSContext* aCx, workers::WorkerPrivate* aWorkerPrivate) override + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + if (mRunnable) { + mRunnable->RunBackOnWorkerThread(); + + // Let's release the worker thread. + mRunnable->ReleaseWorker(); + mRunnable = nullptr; + } + + return true; + } + + private: + ~ReleaseRunnable() + {} + }; + + RefPtr<WorkerControlRunnable> runnable = + new ReleaseRunnable(mWorkerPrivate, this); + Unused << NS_WARN_IF(!runnable->Dispatch()); +} + +bool +WorkerProxyToMainThreadRunnable::HoldWorker() +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(!mWorkerHolder); + + class SimpleWorkerHolder final : public WorkerHolder + { + public: + bool Notify(Status aStatus) override + { + // We don't care about the notification. We just want to keep the + // mWorkerPrivate alive. + return true; + } + }; + + UniquePtr<WorkerHolder> workerHolder(new SimpleWorkerHolder()); + if (NS_WARN_IF(!workerHolder->HoldWorker(mWorkerPrivate, Canceling))) { + return false; + } + + mWorkerHolder = Move(workerHolder); + return true; +} + +void +WorkerProxyToMainThreadRunnable::ReleaseWorker() +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(mWorkerHolder); + mWorkerHolder = nullptr; +} diff --git a/dom/workers/WorkerRunnable.h b/dom/workers/WorkerRunnable.h new file mode 100644 index 000000000..c65060f44 --- /dev/null +++ b/dom/workers/WorkerRunnable.h @@ -0,0 +1,509 @@ +/* -*- 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_workers_workerrunnable_h__ +#define mozilla_dom_workers_workerrunnable_h__ + +#include "Workers.h" + +#include "nsICancelableRunnable.h" + +#include "mozilla/Atomics.h" +#include "nsISupportsImpl.h" +#include "nsThreadUtils.h" /* nsRunnable */ + +struct JSContext; +class nsIEventTarget; + +namespace mozilla { +class ErrorResult; +} // namespace mozilla + +BEGIN_WORKERS_NAMESPACE + +class WorkerPrivate; + +// Use this runnable to communicate from the worker to its parent or vice-versa. +// The busy count must be taken into consideration and declared at construction +// time. +class WorkerRunnable : public nsIRunnable, + public nsICancelableRunnable +{ +public: + enum TargetAndBusyBehavior { + // Target the main thread for top-level workers, otherwise target the + // WorkerThread of the worker's parent. No change to the busy count. + ParentThreadUnchangedBusyCount, + + // Target the thread where the worker event loop runs. The busy count will + // be incremented before dispatching and decremented (asynchronously) after + // running. + WorkerThreadModifyBusyCount, + + // Target the thread where the worker event loop runs. The busy count will + // not be modified in any way. Besides worker-internal runnables this is + // almost always the wrong choice. + WorkerThreadUnchangedBusyCount + }; + +protected: + // The WorkerPrivate that this runnable is associated with. + WorkerPrivate* mWorkerPrivate; + + // See above. + TargetAndBusyBehavior mBehavior; + + // It's unclear whether or not Cancel() is supposed to work when called on any + // thread. To be safe we're using an atomic but it's likely overkill. + Atomic<uint32_t> mCanceled; + +private: + // Whether or not Cancel() is currently being called from inside the Run() + // method. Avoids infinite recursion when a subclass calls Run() from inside + // Cancel(). Only checked and modified on the target thread. + bool mCallingCancelWithinRun; + +public: + NS_DECL_THREADSAFE_ISUPPORTS + + // If you override Cancel() then you'll need to either call the base class + // Cancel() method or override IsCanceled() so that the Run() method bails out + // appropriately. + nsresult + Cancel() override; + + // The return value is true if and only if both PreDispatch and + // DispatchInternal return true. + bool + Dispatch(); + + // See above note about Cancel(). + virtual bool + IsCanceled() const + { + return mCanceled != 0; + } + + static WorkerRunnable* + FromRunnable(nsIRunnable* aRunnable); + +protected: + WorkerRunnable(WorkerPrivate* aWorkerPrivate, + TargetAndBusyBehavior aBehavior = WorkerThreadModifyBusyCount) +#ifdef DEBUG + ; +#else + : mWorkerPrivate(aWorkerPrivate), mBehavior(aBehavior), mCanceled(0), + mCallingCancelWithinRun(false) + { } +#endif + + // This class is reference counted. + virtual ~WorkerRunnable() + { } + + // Returns true if this runnable should be dispatched to the debugger queue, + // and false otherwise. + virtual bool + IsDebuggerRunnable() const; + + nsIGlobalObject* + DefaultGlobalObject() const; + + // By default asserts that Dispatch() is being called on the right thread + // (ParentThread if |mTarget| is WorkerThread, or WorkerThread otherwise). + // Also increments the busy count of |mWorkerPrivate| if targeting the + // WorkerThread. + virtual bool + PreDispatch(WorkerPrivate* aWorkerPrivate); + + // By default asserts that Dispatch() is being called on the right thread + // (ParentThread if |mTarget| is WorkerThread, or WorkerThread otherwise). + virtual void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult); + + // May be implemented by subclasses if desired if they need to do some sort of + // setup before we try to set up our JSContext and compartment for real. + // Typically the only thing that should go in here is creation of the worker's + // global. + // + // If false is returned, WorkerRun will not be called at all. PostRun will + // still be called, with false passed for aRunResult. + virtual bool + PreRun(WorkerPrivate* aWorkerPrivate); + + // Must be implemented by subclasses. Called on the target thread. The return + // value will be passed to PostRun(). The JSContext passed in here comes from + // an AutoJSAPI (or AutoEntryScript) that we set up on the stack. If + // mBehavior is ParentThreadUnchangedBusyCount, it is in the compartment of + // mWorkerPrivate's reflector (i.e. the worker object in the parent thread), + // unless that reflector is null, in which case it's in the compartment of the + // parent global (which is the compartment reflector would have been in), or + // in the null compartment if there is no parent global. For other mBehavior + // values, we're running on the worker thread and aCx is in whatever + // compartment GetCurrentThreadJSContext() was in when nsIRunnable::Run() got + // called. This is actually important for cases when a runnable spins a + // syncloop and wants everything that happens during the syncloop to happen in + // the compartment that runnable set up (which may, for example, be a debugger + // sandbox compartment!). If aCx wasn't in a compartment to start with, aCx + // will be in either the debugger global's compartment or the worker's + // global's compartment depending on whether IsDebuggerRunnable() is true. + // + // Immediately after WorkerRun returns, the caller will assert that either it + // returns false or there is no exception pending on aCx. Then it will report + // any pending exceptions on aCx. + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) = 0; + + // By default asserts that Run() (and WorkerRun()) were called on the correct + // thread. Also sends an asynchronous message to the ParentThread if the + // busy count was previously modified in PreDispatch(). + // + // The aCx passed here is the same one as was passed to WorkerRun and is + // still in the same compartment. PostRun implementations must NOT leave an + // exception on the JSContext and must not run script, because the incoming + // JSContext may be in the null compartment. + virtual void + PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aRunResult); + + virtual bool + DispatchInternal(); + + // Calling Run() directly is not supported. Just call Dispatch() and + // WorkerRun() will be called on the correct thread automatically. + NS_DECL_NSIRUNNABLE +}; + +// This runnable is used to send a message to a worker debugger. +class WorkerDebuggerRunnable : public WorkerRunnable +{ +protected: + explicit WorkerDebuggerRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) + { + } + + virtual ~WorkerDebuggerRunnable() + { } + +private: + virtual bool + IsDebuggerRunnable() const override + { + return true; + } + + virtual bool + PreDispatch(WorkerPrivate* aWorkerPrivate) override final + { + AssertIsOnMainThread(); + + return true; + } + + virtual void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override; +}; + +// This runnable is used to send a message directly to a worker's sync loop. +class WorkerSyncRunnable : public WorkerRunnable +{ +protected: + nsCOMPtr<nsIEventTarget> mSyncLoopTarget; + + // Passing null for aSyncLoopTarget is allowed and will result in the behavior + // of a normal WorkerRunnable. + WorkerSyncRunnable(WorkerPrivate* aWorkerPrivate, + nsIEventTarget* aSyncLoopTarget); + + WorkerSyncRunnable(WorkerPrivate* aWorkerPrivate, + already_AddRefed<nsIEventTarget>&& aSyncLoopTarget); + + virtual ~WorkerSyncRunnable(); + + virtual bool + DispatchInternal() override; +}; + +// This runnable is identical to WorkerSyncRunnable except it is meant to be +// created on and dispatched from the main thread only. Its WorkerRun/PostRun +// will run on the worker thread. +class MainThreadWorkerSyncRunnable : public WorkerSyncRunnable +{ +protected: + // Passing null for aSyncLoopTarget is allowed and will result in the behavior + // of a normal WorkerRunnable. + MainThreadWorkerSyncRunnable(WorkerPrivate* aWorkerPrivate, + nsIEventTarget* aSyncLoopTarget) + : WorkerSyncRunnable(aWorkerPrivate, aSyncLoopTarget) + { + AssertIsOnMainThread(); + } + + MainThreadWorkerSyncRunnable(WorkerPrivate* aWorkerPrivate, + already_AddRefed<nsIEventTarget>&& aSyncLoopTarget) + : WorkerSyncRunnable(aWorkerPrivate, Move(aSyncLoopTarget)) + { + AssertIsOnMainThread(); + } + + virtual ~MainThreadWorkerSyncRunnable() + { } + +private: + virtual bool + PreDispatch(WorkerPrivate* aWorkerPrivate) override + { + AssertIsOnMainThread(); + return true; + } + + virtual void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override; +}; + +// This runnable is processed as soon as it is received by the worker, +// potentially running before previously queued runnables and perhaps even with +// other JS code executing on the stack. These runnables must not alter the +// state of the JS runtime and should only twiddle state values. The busy count +// is never modified. +class WorkerControlRunnable : public WorkerRunnable +{ + friend class WorkerPrivate; + +protected: + WorkerControlRunnable(WorkerPrivate* aWorkerPrivate, + TargetAndBusyBehavior aBehavior = WorkerThreadModifyBusyCount) +#ifdef DEBUG + ; +#else + : WorkerRunnable(aWorkerPrivate, aBehavior) + { } +#endif + + virtual ~WorkerControlRunnable() + { } + + nsresult + Cancel() override; + +public: + NS_DECL_ISUPPORTS_INHERITED + +private: + virtual bool + DispatchInternal() override; + + // Should only be called by WorkerPrivate::DoRunLoop. + using WorkerRunnable::Cancel; +}; + +// A convenience class for WorkerRunnables that are originated on the main +// thread. +class MainThreadWorkerRunnable : public WorkerRunnable +{ +protected: + explicit MainThreadWorkerRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) + { + AssertIsOnMainThread(); + } + + virtual ~MainThreadWorkerRunnable() + {} + + virtual bool + PreDispatch(WorkerPrivate* aWorkerPrivate) override + { + AssertIsOnMainThread(); + return true; + } + + virtual void + PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override + { + AssertIsOnMainThread(); + } +}; + +// A convenience class for WorkerControlRunnables that originate on the main +// thread. +class MainThreadWorkerControlRunnable : public WorkerControlRunnable +{ +protected: + explicit MainThreadWorkerControlRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount) + { } + + virtual ~MainThreadWorkerControlRunnable() + { } + + virtual bool + PreDispatch(WorkerPrivate* aWorkerPrivate) override + { + AssertIsOnMainThread(); + return true; + } + + virtual void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override + { + AssertIsOnMainThread(); + } +}; + +// A WorkerRunnable that should be dispatched from the worker to itself for +// async tasks. This will increment the busy count PostDispatch() (only if +// dispatch was successful) and decrement it in PostRun(). +// +// Async tasks will almost always want to use this since +// a WorkerSameThreadRunnable keeps the Worker from being GCed. +class WorkerSameThreadRunnable : public WorkerRunnable +{ +protected: + explicit WorkerSameThreadRunnable(WorkerPrivate* aWorkerPrivate) + : WorkerRunnable(aWorkerPrivate, WorkerThreadModifyBusyCount) + { } + + virtual ~WorkerSameThreadRunnable() + { } + + virtual bool + PreDispatch(WorkerPrivate* aWorkerPrivate) override; + + virtual void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override; + + // We just delegate PostRun to WorkerRunnable, since it does exactly + // what we want. +}; + +// Base class for the runnable objects, which makes a synchronous call to +// dispatch the tasks from the worker thread to the main thread. +// +// Note that the derived class must override MainThreadRun. +class WorkerMainThreadRunnable : public Runnable +{ +protected: + WorkerPrivate* mWorkerPrivate; + nsCOMPtr<nsIEventTarget> mSyncLoopTarget; + const nsCString mTelemetryKey; + + explicit WorkerMainThreadRunnable(WorkerPrivate* aWorkerPrivate, + const nsACString& aTelemetryKey); + ~WorkerMainThreadRunnable() {} + + virtual bool MainThreadRun() = 0; + +public: + // Dispatch the runnable to the main thread. If dispatch to main thread + // fails, or if the worker is shut down while dispatching, an error will be + // reported on aRv. In that case the error MUST be propagated out to script. + void Dispatch(ErrorResult& aRv); + +private: + NS_IMETHOD Run() override; +}; + +// This runnable is an helper class for dispatching something from a worker +// thread to the main-thread and back to the worker-thread. During this +// operation, this class will keep the worker alive. +class WorkerProxyToMainThreadRunnable : public Runnable +{ +protected: + explicit WorkerProxyToMainThreadRunnable(WorkerPrivate* aWorkerPrivate); + + virtual ~WorkerProxyToMainThreadRunnable(); + + // First this method is called on the main-thread. + virtual void RunOnMainThread() = 0; + + // After this second method is called on the worker-thread. + virtual void RunBackOnWorkerThread() = 0; + +public: + bool Dispatch(); + +private: + NS_IMETHOD Run() override; + + void PostDispatchOnMainThread(); + + bool HoldWorker(); + void ReleaseWorker(); + +protected: + WorkerPrivate* mWorkerPrivate; + UniquePtr<WorkerHolder> mWorkerHolder; +}; + +// Class for checking API exposure. This totally violates the "MUST" in the +// comments on WorkerMainThreadRunnable::Dispatch, because API exposure checks +// can't throw. Maybe we should change it so they _could_ throw. But for now +// we are bad people and should be ashamed of ourselves. Let's hope none of +// them happen while a worker is shutting down. +// +// Do NOT copy what this class is doing elsewhere. Just don't. +class WorkerCheckAPIExposureOnMainThreadRunnable + : public WorkerMainThreadRunnable +{ +public: + explicit + WorkerCheckAPIExposureOnMainThreadRunnable(WorkerPrivate* aWorkerPrivate); + virtual + ~WorkerCheckAPIExposureOnMainThreadRunnable(); + + // Returns whether the dispatch succeeded. If this returns false, the API + // should not be exposed. + bool Dispatch(); +}; + +// This runnable is used to stop a sync loop and it's meant to be used on the +// main-thread only. As sync loops keep the busy count incremented as long as +// they run this runnable does not modify the busy count +// in any way. +class MainThreadStopSyncLoopRunnable : public WorkerSyncRunnable +{ + bool mResult; + +public: + // Passing null for aSyncLoopTarget is not allowed. + MainThreadStopSyncLoopRunnable( + WorkerPrivate* aWorkerPrivate, + already_AddRefed<nsIEventTarget>&& aSyncLoopTarget, + bool aResult); + + // By default StopSyncLoopRunnables cannot be canceled since they could leave + // a sync loop spinning forever. + nsresult + Cancel() override; + +protected: + virtual ~MainThreadStopSyncLoopRunnable() + { } + +private: + virtual bool + PreDispatch(WorkerPrivate* aWorkerPrivate) override final + { + AssertIsOnMainThread(); + return true; + } + + virtual void + PostDispatch(WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override; + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override; + + virtual bool + DispatchInternal() override final; +}; + +END_WORKERS_NAMESPACE + +#endif // mozilla_dom_workers_workerrunnable_h__ diff --git a/dom/workers/WorkerScope.cpp b/dom/workers/WorkerScope.cpp new file mode 100644 index 000000000..d9a987b62 --- /dev/null +++ b/dom/workers/WorkerScope.cpp @@ -0,0 +1,978 @@ +/* -*- 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 "WorkerScope.h" + +#include "jsapi.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/Console.h" +#include "mozilla/dom/DedicatedWorkerGlobalScopeBinding.h" +#include "mozilla/dom/Fetch.h" +#include "mozilla/dom/FunctionBinding.h" +#include "mozilla/dom/IDBFactory.h" +#include "mozilla/dom/ImageBitmap.h" +#include "mozilla/dom/ImageBitmapBinding.h" +#include "mozilla/dom/Performance.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseWorkerProxy.h" +#include "mozilla/dom/ServiceWorkerGlobalScopeBinding.h" +#include "mozilla/dom/SharedWorkerGlobalScopeBinding.h" +#include "mozilla/dom/SimpleGlobalObject.h" +#include "mozilla/dom/WorkerDebuggerGlobalScopeBinding.h" +#include "mozilla/dom/WorkerGlobalScopeBinding.h" +#include "mozilla/dom/WorkerLocation.h" +#include "mozilla/dom/WorkerNavigator.h" +#include "mozilla/dom/cache/CacheStorage.h" +#include "mozilla/Services.h" +#include "nsServiceManagerUtils.h" + +#include "nsIDocument.h" +#include "nsIServiceWorkerManager.h" +#include "nsIScriptTimeoutHandler.h" + +#ifdef ANDROID +#include <android/log.h> +#endif + +#include "Crypto.h" +#include "Principal.h" +#include "RuntimeService.h" +#include "ScriptLoader.h" +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" +#include "ServiceWorkerClients.h" +#include "ServiceWorkerManager.h" +#include "ServiceWorkerRegistration.h" + +#ifdef XP_WIN +#undef PostMessage +#endif + +extern already_AddRefed<nsIScriptTimeoutHandler> +NS_CreateJSTimeoutHandler(JSContext* aCx, + mozilla::dom::workers::WorkerPrivate* aWorkerPrivate, + mozilla::dom::Function& aFunction, + const mozilla::dom::Sequence<JS::Value>& aArguments, + mozilla::ErrorResult& aError); + +extern already_AddRefed<nsIScriptTimeoutHandler> +NS_CreateJSTimeoutHandler(JSContext* aCx, + mozilla::dom::workers::WorkerPrivate* aWorkerPrivate, + const nsAString& aExpression); + +using namespace mozilla; +using namespace mozilla::dom; +USING_WORKERS_NAMESPACE + +using mozilla::dom::cache::CacheStorage; +using mozilla::ipc::PrincipalInfo; + +WorkerGlobalScope::WorkerGlobalScope(WorkerPrivate* aWorkerPrivate) +: mWindowInteractionsAllowed(0) +, mWorkerPrivate(aWorkerPrivate) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); +} + +WorkerGlobalScope::~WorkerGlobalScope() +{ + mWorkerPrivate->AssertIsOnWorkerThread(); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(WorkerGlobalScope) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WorkerGlobalScope, + DOMEventTargetHelper) + tmp->mWorkerPrivate->AssertIsOnWorkerThread(); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConsole) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCrypto) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPerformance) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLocation) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNavigator) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIndexedDB) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCacheStorage) + tmp->TraverseHostObjectURIs(cb); + tmp->mWorkerPrivate->TraverseTimeouts(cb); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WorkerGlobalScope, + DOMEventTargetHelper) + tmp->mWorkerPrivate->AssertIsOnWorkerThread(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsole) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCrypto) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPerformance) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mLocation) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mNavigator) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mIndexedDB) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCacheStorage) + tmp->UnlinkHostObjectURIs(); + tmp->mWorkerPrivate->UnlinkTimeouts(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(WorkerGlobalScope, + DOMEventTargetHelper) + tmp->mWorkerPrivate->AssertIsOnWorkerThread(); +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_ADDREF_INHERITED(WorkerGlobalScope, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(WorkerGlobalScope, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkerGlobalScope) + NS_INTERFACE_MAP_ENTRY(nsIGlobalObject) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +JSObject* +WorkerGlobalScope::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + MOZ_CRASH("We should never get here!"); +} + +Console* +WorkerGlobalScope::GetConsole(ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (!mConsole) { + mConsole = Console::Create(nullptr, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + + return mConsole; +} + +Crypto* +WorkerGlobalScope::GetCrypto(ErrorResult& aError) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (!mCrypto) { + mCrypto = new Crypto(); + mCrypto->Init(this); + } + + return mCrypto; +} + +already_AddRefed<CacheStorage> +WorkerGlobalScope::GetCaches(ErrorResult& aRv) +{ + if (!mCacheStorage) { + MOZ_ASSERT(mWorkerPrivate); + mCacheStorage = CacheStorage::CreateOnWorker(cache::DEFAULT_NAMESPACE, this, + mWorkerPrivate, aRv); + } + + RefPtr<CacheStorage> ref = mCacheStorage; + return ref.forget(); +} + +bool +WorkerGlobalScope::IsSecureContext() const +{ + bool globalSecure = + JS_GetIsSecureContext(js::GetObjectCompartment(GetWrapperPreserveColor())); + MOZ_ASSERT(globalSecure == mWorkerPrivate->IsSecureContext()); + return globalSecure; +} + +already_AddRefed<WorkerLocation> +WorkerGlobalScope::Location() +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (!mLocation) { + WorkerPrivate::LocationInfo& info = mWorkerPrivate->GetLocationInfo(); + + mLocation = WorkerLocation::Create(info); + MOZ_ASSERT(mLocation); + } + + RefPtr<WorkerLocation> location = mLocation; + return location.forget(); +} + +already_AddRefed<WorkerNavigator> +WorkerGlobalScope::Navigator() +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (!mNavigator) { + mNavigator = WorkerNavigator::Create(mWorkerPrivate->OnLine()); + MOZ_ASSERT(mNavigator); + } + + RefPtr<WorkerNavigator> navigator = mNavigator; + return navigator.forget(); +} + +already_AddRefed<WorkerNavigator> +WorkerGlobalScope::GetExistingNavigator() const +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<WorkerNavigator> navigator = mNavigator; + return navigator.forget(); +} + +void +WorkerGlobalScope::Close(JSContext* aCx, ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (mWorkerPrivate->IsServiceWorker()) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + } else { + mWorkerPrivate->CloseInternal(aCx); + } +} + +OnErrorEventHandlerNonNull* +WorkerGlobalScope::GetOnerror() +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + EventListenerManager* elm = GetExistingListenerManager(); + return elm ? elm->GetOnErrorEventHandler() : nullptr; +} + +void +WorkerGlobalScope::SetOnerror(OnErrorEventHandlerNonNull* aHandler) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + EventListenerManager* elm = GetOrCreateListenerManager(); + if (elm) { + elm->SetEventHandler(aHandler); + } +} + +void +WorkerGlobalScope::ImportScripts(const Sequence<nsString>& aScriptURLs, + ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + scriptloader::Load(mWorkerPrivate, aScriptURLs, WorkerScript, aRv); +} + +int32_t +WorkerGlobalScope::SetTimeout(JSContext* aCx, + Function& aHandler, + const int32_t aTimeout, + const Sequence<JS::Value>& aArguments, + ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + nsCOMPtr<nsIScriptTimeoutHandler> handler = + NS_CreateJSTimeoutHandler(aCx, mWorkerPrivate, aHandler, aArguments, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return 0; + } + + return mWorkerPrivate->SetTimeout(aCx, handler, aTimeout, false, aRv); +} + +int32_t +WorkerGlobalScope::SetTimeout(JSContext* aCx, + const nsAString& aHandler, + const int32_t aTimeout, + const Sequence<JS::Value>& /* unused */, + ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + nsCOMPtr<nsIScriptTimeoutHandler> handler = + NS_CreateJSTimeoutHandler(aCx, mWorkerPrivate, aHandler); + return mWorkerPrivate->SetTimeout(aCx, handler, aTimeout, false, aRv); +} + +void +WorkerGlobalScope::ClearTimeout(int32_t aHandle) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + mWorkerPrivate->ClearTimeout(aHandle); +} + +int32_t +WorkerGlobalScope::SetInterval(JSContext* aCx, + Function& aHandler, + const Optional<int32_t>& aTimeout, + const Sequence<JS::Value>& aArguments, + ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + bool isInterval = aTimeout.WasPassed(); + int32_t timeout = aTimeout.WasPassed() ? aTimeout.Value() : 0; + + nsCOMPtr<nsIScriptTimeoutHandler> handler = + NS_CreateJSTimeoutHandler(aCx, mWorkerPrivate, aHandler, aArguments, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return 0; + } + + return mWorkerPrivate->SetTimeout(aCx, handler, timeout, isInterval, aRv); +} + +int32_t +WorkerGlobalScope::SetInterval(JSContext* aCx, + const nsAString& aHandler, + const Optional<int32_t>& aTimeout, + const Sequence<JS::Value>& /* unused */, + ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + Sequence<JS::Value> dummy; + + bool isInterval = aTimeout.WasPassed(); + int32_t timeout = aTimeout.WasPassed() ? aTimeout.Value() : 0; + + nsCOMPtr<nsIScriptTimeoutHandler> handler = + NS_CreateJSTimeoutHandler(aCx, mWorkerPrivate, aHandler); + return mWorkerPrivate->SetTimeout(aCx, handler, timeout, isInterval, aRv); +} + +void +WorkerGlobalScope::ClearInterval(int32_t aHandle) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + mWorkerPrivate->ClearTimeout(aHandle); +} + +void +WorkerGlobalScope::Atob(const nsAString& aAtob, nsAString& aOutput, ErrorResult& aRv) const +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + aRv = nsContentUtils::Atob(aAtob, aOutput); +} + +void +WorkerGlobalScope::Btoa(const nsAString& aBtoa, nsAString& aOutput, ErrorResult& aRv) const +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + aRv = nsContentUtils::Btoa(aBtoa, aOutput); +} + +void +WorkerGlobalScope::Dump(const Optional<nsAString>& aString) const +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (!aString.WasPassed()) { + return; + } + +#if !(defined(DEBUG) || defined(MOZ_ENABLE_JS_DUMP)) + if (!mWorkerPrivate->DumpEnabled()) { + return; + } +#endif + + NS_ConvertUTF16toUTF8 str(aString.Value()); + + MOZ_LOG(nsContentUtils::DOMDumpLog(), LogLevel::Debug, ("[Worker.Dump] %s", str.get())); +#ifdef ANDROID + __android_log_print(ANDROID_LOG_INFO, "Gecko", "%s", str.get()); +#endif + fputs(str.get(), stdout); + fflush(stdout); +} + +Performance* +WorkerGlobalScope::GetPerformance() +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + if (!mPerformance) { + mPerformance = Performance::CreateForWorker(mWorkerPrivate); + } + + return mPerformance; +} + +already_AddRefed<Promise> +WorkerGlobalScope::Fetch(const RequestOrUSVString& aInput, + const RequestInit& aInit, ErrorResult& aRv) +{ + return FetchRequest(this, aInput, aInit, aRv); +} + +already_AddRefed<IDBFactory> +WorkerGlobalScope::GetIndexedDB(ErrorResult& aErrorResult) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<IDBFactory> indexedDB = mIndexedDB; + + if (!indexedDB) { + if (!mWorkerPrivate->IsStorageAllowed()) { + NS_WARNING("IndexedDB is not allowed in this worker!"); + aErrorResult = NS_ERROR_DOM_SECURITY_ERR; + return nullptr; + } + + JSContext* cx = mWorkerPrivate->GetJSContext(); + MOZ_ASSERT(cx); + + JS::Rooted<JSObject*> owningObject(cx, GetGlobalJSObject()); + MOZ_ASSERT(owningObject); + + const PrincipalInfo& principalInfo = mWorkerPrivate->GetPrincipalInfo(); + + nsresult rv = + IDBFactory::CreateForWorker(cx, + owningObject, + principalInfo, + mWorkerPrivate->WindowID(), + getter_AddRefs(indexedDB)); + if (NS_WARN_IF(NS_FAILED(rv))) { + aErrorResult = rv; + return nullptr; + } + + mIndexedDB = indexedDB; + } + + return indexedDB.forget(); +} + +already_AddRefed<Promise> +WorkerGlobalScope::CreateImageBitmap(const ImageBitmapSource& aImage, + ErrorResult& aRv) +{ + if (aImage.IsArrayBuffer() || aImage.IsArrayBufferView()) { + aRv.Throw(NS_ERROR_NOT_IMPLEMENTED); + return nullptr; + } + + return ImageBitmap::Create(this, aImage, Nothing(), aRv); +} + +already_AddRefed<Promise> +WorkerGlobalScope::CreateImageBitmap(const ImageBitmapSource& aImage, + int32_t aSx, int32_t aSy, int32_t aSw, int32_t aSh, + ErrorResult& aRv) +{ + if (aImage.IsArrayBuffer() || aImage.IsArrayBufferView()) { + aRv.Throw(NS_ERROR_NOT_IMPLEMENTED); + return nullptr; + } + + return ImageBitmap::Create(this, aImage, Some(gfx::IntRect(aSx, aSy, aSw, aSh)), aRv); +} + +already_AddRefed<mozilla::dom::Promise> +WorkerGlobalScope::CreateImageBitmap(const ImageBitmapSource& aImage, + int32_t aOffset, int32_t aLength, + ImageBitmapFormat aFormat, + const Sequence<ChannelPixelLayout>& aLayout, + ErrorResult& aRv) +{ + JSContext* cx = GetCurrentThreadJSContext(); + MOZ_ASSERT(cx); + + if (!ImageBitmap::ExtensionsEnabled(cx, nullptr)) { + aRv.Throw(NS_ERROR_TYPE_ERR); + return nullptr; + } + + if (aImage.IsArrayBuffer() || aImage.IsArrayBufferView()) { + return ImageBitmap::Create(this, aImage, aOffset, aLength, aFormat, aLayout, + aRv); + } else { + aRv.Throw(NS_ERROR_TYPE_ERR); + return nullptr; + } +} + +DedicatedWorkerGlobalScope::DedicatedWorkerGlobalScope(WorkerPrivate* aWorkerPrivate) +: WorkerGlobalScope(aWorkerPrivate) +{ +} + +bool +DedicatedWorkerGlobalScope::WrapGlobalObject(JSContext* aCx, + JS::MutableHandle<JSObject*> aReflector) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(!mWorkerPrivate->IsSharedWorker()); + + JS::CompartmentOptions options; + mWorkerPrivate->CopyJSCompartmentOptions(options); + + const bool usesSystemPrincipal = mWorkerPrivate->UsesSystemPrincipal(); + + // Note that xpc::ShouldDiscardSystemSource() and + // xpc::ExtraWarningsForSystemJS() read prefs that are cached on the main + // thread. This is benignly racey. + const bool discardSource = usesSystemPrincipal && + xpc::ShouldDiscardSystemSource(); + const bool extraWarnings = usesSystemPrincipal && + xpc::ExtraWarningsForSystemJS(); + + JS::CompartmentBehaviors& behaviors = options.behaviors(); + behaviors.setDiscardSource(discardSource) + .extraWarningsOverride().set(extraWarnings); + + const bool sharedMemoryEnabled = xpc::SharedMemoryEnabled(); + + JS::CompartmentCreationOptions& creationOptions = options.creationOptions(); + creationOptions.setSharedMemoryAndAtomicsEnabled(sharedMemoryEnabled); + + return DedicatedWorkerGlobalScopeBinding::Wrap(aCx, this, this, + options, + GetWorkerPrincipal(), + true, aReflector); +} + +void +DedicatedWorkerGlobalScope::PostMessage(JSContext* aCx, + JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + mWorkerPrivate->PostMessageToParent(aCx, aMessage, aTransferable, aRv); +} + +SharedWorkerGlobalScope::SharedWorkerGlobalScope(WorkerPrivate* aWorkerPrivate, + const nsCString& aName) +: WorkerGlobalScope(aWorkerPrivate), mName(aName) +{ +} + +bool +SharedWorkerGlobalScope::WrapGlobalObject(JSContext* aCx, + JS::MutableHandle<JSObject*> aReflector) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(mWorkerPrivate->IsSharedWorker()); + + JS::CompartmentOptions options; + mWorkerPrivate->CopyJSCompartmentOptions(options); + + return SharedWorkerGlobalScopeBinding::Wrap(aCx, this, this, options, + GetWorkerPrincipal(), + true, aReflector); +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ServiceWorkerGlobalScope, WorkerGlobalScope, + mClients, mRegistration) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(ServiceWorkerGlobalScope) +NS_INTERFACE_MAP_END_INHERITING(WorkerGlobalScope) + +NS_IMPL_ADDREF_INHERITED(ServiceWorkerGlobalScope, WorkerGlobalScope) +NS_IMPL_RELEASE_INHERITED(ServiceWorkerGlobalScope, WorkerGlobalScope) + +ServiceWorkerGlobalScope::ServiceWorkerGlobalScope(WorkerPrivate* aWorkerPrivate, + const nsACString& aScope) + : WorkerGlobalScope(aWorkerPrivate), + mScope(NS_ConvertUTF8toUTF16(aScope)) +{ +} + +ServiceWorkerGlobalScope::~ServiceWorkerGlobalScope() +{ +} + +bool +ServiceWorkerGlobalScope::WrapGlobalObject(JSContext* aCx, + JS::MutableHandle<JSObject*> aReflector) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(mWorkerPrivate->IsServiceWorker()); + + JS::CompartmentOptions options; + mWorkerPrivate->CopyJSCompartmentOptions(options); + + return ServiceWorkerGlobalScopeBinding::Wrap(aCx, this, this, options, + GetWorkerPrincipal(), + true, aReflector); +} + +ServiceWorkerClients* +ServiceWorkerGlobalScope::Clients() +{ + if (!mClients) { + mClients = new ServiceWorkerClients(this); + } + + return mClients; +} + +ServiceWorkerRegistration* +ServiceWorkerGlobalScope::Registration() +{ + if (!mRegistration) { + mRegistration = + ServiceWorkerRegistration::CreateForWorker(mWorkerPrivate, mScope); + } + + return mRegistration; +} + +namespace { + +class SkipWaitingResultRunnable final : public WorkerRunnable +{ + RefPtr<PromiseWorkerProxy> mPromiseProxy; + +public: + SkipWaitingResultRunnable(WorkerPrivate* aWorkerPrivate, + PromiseWorkerProxy* aPromiseProxy) + : WorkerRunnable(aWorkerPrivate) + , mPromiseProxy(aPromiseProxy) + { + AssertIsOnMainThread(); + } + + virtual bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<Promise> promise = mPromiseProxy->WorkerPromise(); + promise->MaybeResolveWithUndefined(); + + // Release the reference on the worker thread. + mPromiseProxy->CleanUp(); + + return true; + } +}; + +class WorkerScopeSkipWaitingRunnable final : public Runnable +{ + RefPtr<PromiseWorkerProxy> mPromiseProxy; + nsCString mScope; + +public: + WorkerScopeSkipWaitingRunnable(PromiseWorkerProxy* aPromiseProxy, + const nsCString& aScope) + : mPromiseProxy(aPromiseProxy) + , mScope(aScope) + { + MOZ_ASSERT(aPromiseProxy); + } + + NS_IMETHOD + Run() override + { + AssertIsOnMainThread(); + + MutexAutoLock lock(mPromiseProxy->Lock()); + if (mPromiseProxy->CleanedUp()) { + return NS_OK; + } + + WorkerPrivate* workerPrivate = mPromiseProxy->GetWorkerPrivate(); + MOZ_DIAGNOSTIC_ASSERT(workerPrivate); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->SetSkipWaitingFlag(workerPrivate->GetPrincipal(), mScope, + workerPrivate->ServiceWorkerID()); + } + + RefPtr<SkipWaitingResultRunnable> runnable = + new SkipWaitingResultRunnable(workerPrivate, mPromiseProxy); + + if (!runnable->Dispatch()) { + NS_WARNING("Failed to dispatch SkipWaitingResultRunnable to the worker."); + } + return NS_OK; + } +}; + +} // namespace + +already_AddRefed<Promise> +ServiceWorkerGlobalScope::SkipWaiting(ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(mWorkerPrivate->IsServiceWorker()); + + RefPtr<Promise> promise = Promise::Create(this, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + RefPtr<PromiseWorkerProxy> promiseProxy = + PromiseWorkerProxy::Create(mWorkerPrivate, promise); + if (!promiseProxy) { + promise->MaybeResolveWithUndefined(); + return promise.forget(); + } + + RefPtr<WorkerScopeSkipWaitingRunnable> runnable = + new WorkerScopeSkipWaitingRunnable(promiseProxy, + NS_ConvertUTF16toUTF8(mScope)); + + MOZ_ALWAYS_SUCCEEDS(mWorkerPrivate->DispatchToMainThread(runnable.forget())); + return promise.forget(); +} + +bool +ServiceWorkerGlobalScope::OpenWindowEnabled(JSContext* aCx, JSObject* aObj) +{ + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + worker->AssertIsOnWorkerThread(); + return worker->OpenWindowEnabled(); +} + +WorkerDebuggerGlobalScope::WorkerDebuggerGlobalScope( + WorkerPrivate* aWorkerPrivate) +: mWorkerPrivate(aWorkerPrivate) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); +} + +WorkerDebuggerGlobalScope::~WorkerDebuggerGlobalScope() +{ + mWorkerPrivate->AssertIsOnWorkerThread(); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(WorkerDebuggerGlobalScope) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WorkerDebuggerGlobalScope, + DOMEventTargetHelper) + tmp->mWorkerPrivate->AssertIsOnWorkerThread(); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConsole) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WorkerDebuggerGlobalScope, + DOMEventTargetHelper) + tmp->mWorkerPrivate->AssertIsOnWorkerThread(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsole) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(WorkerDebuggerGlobalScope, + DOMEventTargetHelper) + tmp->mWorkerPrivate->AssertIsOnWorkerThread(); +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_ADDREF_INHERITED(WorkerDebuggerGlobalScope, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(WorkerDebuggerGlobalScope, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkerDebuggerGlobalScope) + NS_INTERFACE_MAP_ENTRY(nsIGlobalObject) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +bool +WorkerDebuggerGlobalScope::WrapGlobalObject(JSContext* aCx, + JS::MutableHandle<JSObject*> aReflector) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + JS::CompartmentOptions options; + mWorkerPrivate->CopyJSCompartmentOptions(options); + + return WorkerDebuggerGlobalScopeBinding::Wrap(aCx, this, this, options, + GetWorkerPrincipal(), true, + aReflector); +} + +void +WorkerDebuggerGlobalScope::GetGlobal(JSContext* aCx, + JS::MutableHandle<JSObject*> aGlobal, + ErrorResult& aRv) +{ + WorkerGlobalScope* scope = mWorkerPrivate->GetOrCreateGlobalScope(aCx); + if (!scope) { + aRv.Throw(NS_ERROR_FAILURE); + } + + aGlobal.set(scope->GetWrapper()); +} + +void +WorkerDebuggerGlobalScope::CreateSandbox(JSContext* aCx, const nsAString& aName, + JS::Handle<JSObject*> aPrototype, + JS::MutableHandle<JSObject*> aResult, + ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + aResult.set(nullptr); + + JS::Rooted<JS::Value> protoVal(aCx); + protoVal.setObjectOrNull(aPrototype); + JS::Rooted<JSObject*> sandbox(aCx, + SimpleGlobalObject::Create(SimpleGlobalObject::GlobalType::WorkerDebuggerSandbox, + protoVal)); + + if (!sandbox) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + if (!JS_WrapObject(aCx, &sandbox)) { + aRv.NoteJSContextException(aCx); + return; + } + + aResult.set(sandbox); +} + +void +WorkerDebuggerGlobalScope::LoadSubScript(JSContext* aCx, + const nsAString& aURL, + const Optional<JS::Handle<JSObject*>>& aSandbox, + ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + Maybe<JSAutoCompartment> ac; + if (aSandbox.WasPassed()) { + JS::Rooted<JSObject*> sandbox(aCx, js::CheckedUnwrap(aSandbox.Value())); + if (!IsDebuggerSandbox(sandbox)) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + ac.emplace(aCx, sandbox); + } + + nsTArray<nsString> urls; + urls.AppendElement(aURL); + scriptloader::Load(mWorkerPrivate, urls, DebuggerScript, aRv); +} + +void +WorkerDebuggerGlobalScope::EnterEventLoop() +{ + mWorkerPrivate->EnterDebuggerEventLoop(); +} + +void +WorkerDebuggerGlobalScope::LeaveEventLoop() +{ + mWorkerPrivate->LeaveDebuggerEventLoop(); +} + +void +WorkerDebuggerGlobalScope::PostMessage(const nsAString& aMessage) +{ + mWorkerPrivate->PostMessageToDebugger(aMessage); +} + +void +WorkerDebuggerGlobalScope::SetImmediate(Function& aHandler, ErrorResult& aRv) +{ + mWorkerPrivate->SetDebuggerImmediate(aHandler, aRv); +} + +void +WorkerDebuggerGlobalScope::ReportError(JSContext* aCx, + const nsAString& aMessage) +{ + JS::AutoFilename chars; + uint32_t lineno = 0; + JS::DescribeScriptedCaller(aCx, &chars, &lineno); + nsString filename(NS_ConvertUTF8toUTF16(chars.get())); + mWorkerPrivate->ReportErrorToDebugger(filename, lineno, aMessage); +} + +void +WorkerDebuggerGlobalScope::RetrieveConsoleEvents(JSContext* aCx, + nsTArray<JS::Value>& aEvents, + ErrorResult& aRv) +{ + WorkerGlobalScope* scope = mWorkerPrivate->GetOrCreateGlobalScope(aCx); + if (!scope) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr<Console> console = scope->GetConsole(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + console->RetrieveConsoleEvents(aCx, aEvents, aRv); +} + +void +WorkerDebuggerGlobalScope::SetConsoleEventHandler(JSContext* aCx, + AnyCallback* aHandler, + ErrorResult& aRv) +{ + WorkerGlobalScope* scope = mWorkerPrivate->GetOrCreateGlobalScope(aCx); + if (!scope) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr<Console> console = scope->GetConsole(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + console->SetConsoleEventHandler(aHandler); +} + +Console* +WorkerDebuggerGlobalScope::GetConsole(ErrorResult& aRv) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + // Debugger console has its own console object. + if (!mConsole) { + mConsole = Console::Create(nullptr, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + + return mConsole; +} + +void +WorkerDebuggerGlobalScope::Dump(JSContext* aCx, + const Optional<nsAString>& aString) const +{ + WorkerGlobalScope* scope = mWorkerPrivate->GetOrCreateGlobalScope(aCx); + if (scope) { + scope->Dump(aString); + } +} + +BEGIN_WORKERS_NAMESPACE + +bool +IsWorkerGlobal(JSObject* object) +{ + return IS_INSTANCE_OF(WorkerGlobalScope, object); +} + +bool +IsDebuggerGlobal(JSObject* object) +{ + return IS_INSTANCE_OF(WorkerDebuggerGlobalScope, object); +} + +bool +IsDebuggerSandbox(JSObject* object) +{ + return SimpleGlobalObject::SimpleGlobalType(object) == + SimpleGlobalObject::GlobalType::WorkerDebuggerSandbox; +} + +bool +GetterOnlyJSNative(JSContext* aCx, unsigned aArgc, JS::Value* aVp) +{ + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, JSMSG_GETTER_ONLY); + return false; +} + +END_WORKERS_NAMESPACE diff --git a/dom/workers/WorkerScope.h b/dom/workers/WorkerScope.h new file mode 100644 index 000000000..53d0a578e --- /dev/null +++ b/dom/workers/WorkerScope.h @@ -0,0 +1,389 @@ +/* -*- 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_workerscope_h__ +#define mozilla_dom_workerscope_h__ + +#include "Workers.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/Headers.h" +#include "mozilla/dom/RequestBinding.h" +#include "nsWeakReference.h" +#include "mozilla/dom/ImageBitmapSource.h" + +namespace mozilla { +namespace dom { + +class AnyCallback; +struct ChannelPixelLayout; +class Console; +class Crypto; +class Function; +class IDBFactory; +enum class ImageBitmapFormat : uint32_t; +class Performance; +class Promise; +class RequestOrUSVString; +class ServiceWorkerRegistration; +class WorkerLocation; +class WorkerNavigator; + +namespace cache { + +class CacheStorage; + +} // namespace cache + +namespace workers { + +class ServiceWorkerClients; +class WorkerPrivate; + +} // namespace workers + +class WorkerGlobalScope : public DOMEventTargetHelper, + public nsIGlobalObject, + public nsSupportsWeakReference +{ + typedef mozilla::dom::IDBFactory IDBFactory; + + RefPtr<Console> mConsole; + RefPtr<Crypto> mCrypto; + RefPtr<WorkerLocation> mLocation; + RefPtr<WorkerNavigator> mNavigator; + RefPtr<Performance> mPerformance; + RefPtr<IDBFactory> mIndexedDB; + RefPtr<cache::CacheStorage> mCacheStorage; + + uint32_t mWindowInteractionsAllowed; + +protected: + typedef mozilla::dom::workers::WorkerPrivate WorkerPrivate; + WorkerPrivate* mWorkerPrivate; + + explicit WorkerGlobalScope(WorkerPrivate* aWorkerPrivate); + virtual ~WorkerGlobalScope(); + +public: + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + virtual bool + WrapGlobalObject(JSContext* aCx, JS::MutableHandle<JSObject*> aReflector) = 0; + + virtual JSObject* + GetGlobalJSObject(void) override + { + return GetWrapper(); + } + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(WorkerGlobalScope, + DOMEventTargetHelper) + + WorkerGlobalScope* + Self() + { + return this; + } + + Console* + GetConsole(ErrorResult& aRv); + + Console* + GetConsoleIfExists() const + { + return mConsole; + } + + Crypto* + GetCrypto(ErrorResult& aError); + + already_AddRefed<WorkerLocation> + Location(); + + already_AddRefed<WorkerNavigator> + Navigator(); + + already_AddRefed<WorkerNavigator> + GetExistingNavigator() const; + + void + Close(JSContext* aCx, ErrorResult& aRv); + + OnErrorEventHandlerNonNull* + GetOnerror(); + void + SetOnerror(OnErrorEventHandlerNonNull* aHandler); + + void + ImportScripts(const Sequence<nsString>& aScriptURLs, ErrorResult& aRv); + + int32_t + SetTimeout(JSContext* aCx, Function& aHandler, const int32_t aTimeout, + const Sequence<JS::Value>& aArguments, ErrorResult& aRv); + int32_t + SetTimeout(JSContext* aCx, const nsAString& aHandler, const int32_t aTimeout, + const Sequence<JS::Value>& /* unused */, ErrorResult& aRv); + void + ClearTimeout(int32_t aHandle); + int32_t + SetInterval(JSContext* aCx, Function& aHandler, + const Optional<int32_t>& aTimeout, + const Sequence<JS::Value>& aArguments, ErrorResult& aRv); + int32_t + SetInterval(JSContext* aCx, const nsAString& aHandler, + const Optional<int32_t>& aTimeout, + const Sequence<JS::Value>& /* unused */, ErrorResult& aRv); + void + ClearInterval(int32_t aHandle); + + void + Atob(const nsAString& aAtob, nsAString& aOutput, ErrorResult& aRv) const; + void + Btoa(const nsAString& aBtoa, nsAString& aOutput, ErrorResult& aRv) const; + + IMPL_EVENT_HANDLER(online) + IMPL_EVENT_HANDLER(offline) + + void + Dump(const Optional<nsAString>& aString) const; + + Performance* GetPerformance(); + + already_AddRefed<Promise> + Fetch(const RequestOrUSVString& aInput, const RequestInit& aInit, ErrorResult& aRv); + + already_AddRefed<IDBFactory> + GetIndexedDB(ErrorResult& aErrorResult); + + already_AddRefed<cache::CacheStorage> + GetCaches(ErrorResult& aRv); + + bool IsSecureContext() const; + + already_AddRefed<Promise> + CreateImageBitmap(const ImageBitmapSource& aImage, ErrorResult& aRv); + + already_AddRefed<Promise> + CreateImageBitmap(const ImageBitmapSource& aImage, + int32_t aSx, int32_t aSy, int32_t aSw, int32_t aSh, + ErrorResult& aRv); + + already_AddRefed<mozilla::dom::Promise> + CreateImageBitmap(const ImageBitmapSource& aImage, + int32_t aOffset, int32_t aLength, + mozilla::dom::ImageBitmapFormat aFormat, + const mozilla::dom::Sequence<mozilla::dom::ChannelPixelLayout>& aLayout, + mozilla::ErrorResult& aRv); + + bool + WindowInteractionAllowed() const + { + return mWindowInteractionsAllowed > 0; + } + + void + AllowWindowInteraction() + { + mWindowInteractionsAllowed++; + } + + void + ConsumeWindowInteraction() + { + MOZ_ASSERT(mWindowInteractionsAllowed > 0); + mWindowInteractionsAllowed--; + } +}; + +class DedicatedWorkerGlobalScope final : public WorkerGlobalScope +{ + ~DedicatedWorkerGlobalScope() { } + +public: + explicit DedicatedWorkerGlobalScope(WorkerPrivate* aWorkerPrivate); + + virtual bool + WrapGlobalObject(JSContext* aCx, + JS::MutableHandle<JSObject*> aReflector) override; + + void + PostMessage(JSContext* aCx, JS::Handle<JS::Value> aMessage, + const Optional<Sequence<JS::Value>>& aTransferable, + ErrorResult& aRv); + + IMPL_EVENT_HANDLER(message) +}; + +class SharedWorkerGlobalScope final : public WorkerGlobalScope +{ + const nsCString mName; + + ~SharedWorkerGlobalScope() { } + +public: + SharedWorkerGlobalScope(WorkerPrivate* aWorkerPrivate, + const nsCString& aName); + + virtual bool + WrapGlobalObject(JSContext* aCx, + JS::MutableHandle<JSObject*> aReflector) override; + + void GetName(DOMString& aName) const + { + aName.AsAString() = NS_ConvertUTF8toUTF16(mName); + } + + IMPL_EVENT_HANDLER(connect) +}; + +class ServiceWorkerGlobalScope final : public WorkerGlobalScope +{ + const nsString mScope; + RefPtr<workers::ServiceWorkerClients> mClients; + RefPtr<ServiceWorkerRegistration> mRegistration; + + ~ServiceWorkerGlobalScope(); + +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ServiceWorkerGlobalScope, + WorkerGlobalScope) + IMPL_EVENT_HANDLER(notificationclick) + IMPL_EVENT_HANDLER(notificationclose) + + ServiceWorkerGlobalScope(WorkerPrivate* aWorkerPrivate, const nsACString& aScope); + + virtual bool + WrapGlobalObject(JSContext* aCx, + JS::MutableHandle<JSObject*> aReflector) override; + + static bool + OpenWindowEnabled(JSContext* aCx, JSObject* aObj); + + void + GetScope(nsString& aScope) const + { + aScope = mScope; + } + + workers::ServiceWorkerClients* + Clients(); + + ServiceWorkerRegistration* + Registration(); + + already_AddRefed<Promise> + SkipWaiting(ErrorResult& aRv); + + IMPL_EVENT_HANDLER(activate) + IMPL_EVENT_HANDLER(fetch) + IMPL_EVENT_HANDLER(install) + IMPL_EVENT_HANDLER(message) + + IMPL_EVENT_HANDLER(push) + IMPL_EVENT_HANDLER(pushsubscriptionchange) + +}; + +class WorkerDebuggerGlobalScope final : public DOMEventTargetHelper, + public nsIGlobalObject +{ + typedef mozilla::dom::workers::WorkerPrivate WorkerPrivate; + + WorkerPrivate* mWorkerPrivate; + RefPtr<Console> mConsole; + +public: + explicit WorkerDebuggerGlobalScope(WorkerPrivate* aWorkerPrivate); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(WorkerDebuggerGlobalScope, + DOMEventTargetHelper) + + virtual JSObject* + WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override + { + MOZ_CRASH("Shouldn't get here!"); + } + + virtual bool + WrapGlobalObject(JSContext* aCx, + JS::MutableHandle<JSObject*> aReflector); + + virtual JSObject* + GetGlobalJSObject(void) override + { + return GetWrapper(); + } + + void + GetGlobal(JSContext* aCx, JS::MutableHandle<JSObject*> aGlobal, + ErrorResult& aRv); + + void + CreateSandbox(JSContext* aCx, const nsAString& aName, + JS::Handle<JSObject*> aPrototype, + JS::MutableHandle<JSObject*> aResult, + ErrorResult& aRv); + + void + LoadSubScript(JSContext* aCx, const nsAString& aURL, + const Optional<JS::Handle<JSObject*>>& aSandbox, + ErrorResult& aRv); + + void + EnterEventLoop(); + + void + LeaveEventLoop(); + + void + PostMessage(const nsAString& aMessage); + + IMPL_EVENT_HANDLER(message) + + void + SetImmediate(Function& aHandler, ErrorResult& aRv); + + void + ReportError(JSContext* aCx, const nsAString& aMessage); + + void + RetrieveConsoleEvents(JSContext* aCx, nsTArray<JS::Value>& aEvents, + ErrorResult& aRv); + + void + SetConsoleEventHandler(JSContext* aCx, AnyCallback* aHandler, + ErrorResult& aRv); + + Console* + GetConsole(ErrorResult& aRv); + + Console* + GetConsoleIfExists() const + { + return mConsole; + } + + void + Dump(JSContext* aCx, const Optional<nsAString>& aString) const; + +private: + virtual ~WorkerDebuggerGlobalScope(); +}; + +} // namespace dom +} // namespace mozilla + +inline nsISupports* +ToSupports(mozilla::dom::WorkerGlobalScope* aScope) +{ + return static_cast<nsIDOMEventTarget*>(aScope); +} + +#endif /* mozilla_dom_workerscope_h__ */ diff --git a/dom/workers/WorkerThread.cpp b/dom/workers/WorkerThread.cpp new file mode 100644 index 000000000..7a7cb7ac3 --- /dev/null +++ b/dom/workers/WorkerThread.cpp @@ -0,0 +1,355 @@ +/* -*- 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 "WorkerThread.h" + +#include "mozilla/Assertions.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "nsIThreadInternal.h" +#include "WorkerPrivate.h" +#include "WorkerRunnable.h" + +#ifdef DEBUG +#include "nsThreadManager.h" +#endif + +namespace mozilla { +namespace dom { +namespace workers { + +using namespace mozilla::ipc; + +namespace { + +// The C stack size. We use the same stack size on all platforms for +// consistency. +const uint32_t kWorkerStackSize = 256 * sizeof(size_t) * 1024; + +} // namespace + +WorkerThreadFriendKey::WorkerThreadFriendKey() +{ + MOZ_COUNT_CTOR(WorkerThreadFriendKey); +} + +WorkerThreadFriendKey::~WorkerThreadFriendKey() +{ + MOZ_COUNT_DTOR(WorkerThreadFriendKey); +} + +class WorkerThread::Observer final + : public nsIThreadObserver +{ + WorkerPrivate* mWorkerPrivate; + +public: + explicit Observer(WorkerPrivate* aWorkerPrivate) + : mWorkerPrivate(aWorkerPrivate) + { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + } + + NS_DECL_THREADSAFE_ISUPPORTS + +private: + ~Observer() + { + mWorkerPrivate->AssertIsOnWorkerThread(); + } + + NS_DECL_NSITHREADOBSERVER +}; + +WorkerThread::WorkerThread() + : nsThread(nsThread::NOT_MAIN_THREAD, kWorkerStackSize) + , mWorkerPrivateCondVar(mLock, "WorkerThread::mWorkerPrivateCondVar") + , mWorkerPrivate(nullptr) + , mOtherThreadsDispatchingViaEventTarget(0) +#ifdef DEBUG + , mAcceptingNonWorkerRunnables(true) +#endif +{ +} + +WorkerThread::~WorkerThread() +{ + MOZ_ASSERT(!mWorkerPrivate); + MOZ_ASSERT(!mOtherThreadsDispatchingViaEventTarget); + MOZ_ASSERT(mAcceptingNonWorkerRunnables); +} + +// static +already_AddRefed<WorkerThread> +WorkerThread::Create(const WorkerThreadFriendKey& /* aKey */) +{ + RefPtr<WorkerThread> thread = new WorkerThread(); + if (NS_FAILED(thread->Init())) { + NS_WARNING("Failed to create new thread!"); + return nullptr; + } + + return thread.forget(); +} + +void +WorkerThread::SetWorker(const WorkerThreadFriendKey& /* aKey */, + WorkerPrivate* aWorkerPrivate) +{ + MOZ_ASSERT(PR_GetCurrentThread() == mThread); + + if (aWorkerPrivate) { + { + MutexAutoLock lock(mLock); + + MOZ_ASSERT(!mWorkerPrivate); + MOZ_ASSERT(mAcceptingNonWorkerRunnables); + + mWorkerPrivate = aWorkerPrivate; +#ifdef DEBUG + mAcceptingNonWorkerRunnables = false; +#endif + } + + mObserver = new Observer(aWorkerPrivate); + MOZ_ALWAYS_SUCCEEDS(AddObserver(mObserver)); + } else { + MOZ_ALWAYS_SUCCEEDS(RemoveObserver(mObserver)); + mObserver = nullptr; + + { + MutexAutoLock lock(mLock); + + MOZ_ASSERT(mWorkerPrivate); + MOZ_ASSERT(!mAcceptingNonWorkerRunnables); + MOZ_ASSERT(!mOtherThreadsDispatchingViaEventTarget, + "XPCOM Dispatch hapenning at the same time our thread is " + "being unset! This should not be possible!"); + + while (mOtherThreadsDispatchingViaEventTarget) { + mWorkerPrivateCondVar.Wait(); + } + +#ifdef DEBUG + mAcceptingNonWorkerRunnables = true; +#endif + mWorkerPrivate = nullptr; + } + } +} + +nsresult +WorkerThread::DispatchPrimaryRunnable(const WorkerThreadFriendKey& /* aKey */, + already_AddRefed<nsIRunnable> aRunnable) +{ + nsCOMPtr<nsIRunnable> runnable(aRunnable); + +#ifdef DEBUG + MOZ_ASSERT(PR_GetCurrentThread() != mThread); + MOZ_ASSERT(runnable); + { + MutexAutoLock lock(mLock); + + MOZ_ASSERT(!mWorkerPrivate); + MOZ_ASSERT(mAcceptingNonWorkerRunnables); + } +#endif + + nsresult rv = nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +WorkerThread::DispatchAnyThread(const WorkerThreadFriendKey& /* aKey */, + already_AddRefed<WorkerRunnable> aWorkerRunnable) +{ + // May be called on any thread! + +#ifdef DEBUG + { + const bool onWorkerThread = PR_GetCurrentThread() == mThread; + { + MutexAutoLock lock(mLock); + + MOZ_ASSERT(mWorkerPrivate); + MOZ_ASSERT(!mAcceptingNonWorkerRunnables); + + if (onWorkerThread) { + mWorkerPrivate->AssertIsOnWorkerThread(); + } + } + } +#endif + nsCOMPtr<nsIRunnable> runnable(aWorkerRunnable); + + nsresult rv = nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // We don't need to notify the worker's condition variable here because we're + // being called from worker-controlled code and it will make sure to wake up + // the worker thread if needed. + + return NS_OK; +} + +NS_IMPL_ISUPPORTS_INHERITED0(WorkerThread, nsThread) + +NS_IMETHODIMP +WorkerThread::DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) +{ + nsCOMPtr<nsIRunnable> runnable(aRunnable); + return Dispatch(runnable.forget(), aFlags); +} + +NS_IMETHODIMP +WorkerThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable, uint32_t aFlags) +{ + // May be called on any thread! + nsCOMPtr<nsIRunnable> runnable(aRunnable); // in case we exit early + + // Workers only support asynchronous dispatch. + if (NS_WARN_IF(aFlags != NS_DISPATCH_NORMAL)) { + return NS_ERROR_UNEXPECTED; + } + + const bool onWorkerThread = PR_GetCurrentThread() == mThread; + +#ifdef DEBUG + if (runnable && !onWorkerThread) { + nsCOMPtr<nsICancelableRunnable> cancelable = do_QueryInterface(runnable); + + { + MutexAutoLock lock(mLock); + + // Only enforce cancelable runnables after we've started the worker loop. + if (!mAcceptingNonWorkerRunnables) { + MOZ_ASSERT(cancelable, + "Only nsICancelableRunnable may be dispatched to a worker!"); + } + } + } +#endif + + WorkerPrivate* workerPrivate = nullptr; + if (onWorkerThread) { + // No need to lock here because it is only modified on this thread. + MOZ_ASSERT(mWorkerPrivate); + mWorkerPrivate->AssertIsOnWorkerThread(); + + workerPrivate = mWorkerPrivate; + } else { + MutexAutoLock lock(mLock); + + MOZ_ASSERT(mOtherThreadsDispatchingViaEventTarget < UINT32_MAX); + + if (mWorkerPrivate) { + workerPrivate = mWorkerPrivate; + + // Incrementing this counter will make the worker thread sleep if it + // somehow tries to unset mWorkerPrivate while we're using it. + mOtherThreadsDispatchingViaEventTarget++; + } + } + + nsresult rv; + if (runnable && onWorkerThread) { + RefPtr<WorkerRunnable> workerRunnable = workerPrivate->MaybeWrapAsWorkerRunnable(runnable.forget()); + rv = nsThread::Dispatch(workerRunnable.forget(), NS_DISPATCH_NORMAL); + } else { + rv = nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); + } + + if (!onWorkerThread && workerPrivate) { + // We need to wake the worker thread if we're not already on the right + // thread and the dispatch succeeded. + if (NS_SUCCEEDED(rv)) { + MutexAutoLock workerLock(workerPrivate->mMutex); + + workerPrivate->mCondVar.Notify(); + } + + // Now unset our waiting flag. + { + MutexAutoLock lock(mLock); + + MOZ_ASSERT(mOtherThreadsDispatchingViaEventTarget); + + if (!--mOtherThreadsDispatchingViaEventTarget) { + mWorkerPrivateCondVar.Notify(); + } + } + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +NS_IMETHODIMP +WorkerThread::DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +uint32_t +WorkerThread::RecursionDepth(const WorkerThreadFriendKey& /* aKey */) const +{ + MOZ_ASSERT(PR_GetCurrentThread() == mThread); + + return mNestedEventLoopDepth; +} + +NS_IMPL_ISUPPORTS(WorkerThread::Observer, nsIThreadObserver) + +NS_IMETHODIMP +WorkerThread::Observer::OnDispatchedEvent(nsIThreadInternal* /* aThread */) +{ + MOZ_CRASH("OnDispatchedEvent() should never be called!"); +} + +NS_IMETHODIMP +WorkerThread::Observer::OnProcessNextEvent(nsIThreadInternal* /* aThread */, + bool aMayWait) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + // If the PBackground child is not created yet, then we must permit + // blocking event processing to support + // BackgroundChild::SynchronouslyCreateForCurrentThread(). If this occurs + // then we are spinning on the event queue at the start of + // PrimaryWorkerRunnable::Run() and don't want to process the event in + // mWorkerPrivate yet. + if (aMayWait) { + MOZ_ASSERT(CycleCollectedJSContext::Get()->RecursionDepth() == 2); + MOZ_ASSERT(!BackgroundChild::GetForCurrentThread()); + return NS_OK; + } + + mWorkerPrivate->OnProcessNextEvent(); + return NS_OK; +} + +NS_IMETHODIMP +WorkerThread::Observer::AfterProcessNextEvent(nsIThreadInternal* /* aThread */, + bool /* aEventWasProcessed */) +{ + mWorkerPrivate->AssertIsOnWorkerThread(); + + mWorkerPrivate->AfterProcessNextEvent(); + return NS_OK; +} + +} // namespace workers +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/WorkerThread.h b/dom/workers/WorkerThread.h new file mode 100644 index 000000000..f1287023d --- /dev/null +++ b/dom/workers/WorkerThread.h @@ -0,0 +1,110 @@ +/* -*- 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_workers_WorkerThread_h__ +#define mozilla_dom_workers_WorkerThread_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/CondVar.h" +#include "mozilla/DebugOnly.h" +#include "nsISupportsImpl.h" +#include "mozilla/RefPtr.h" +#include "nsThread.h" + +class nsIRunnable; + +namespace mozilla { +namespace dom { +namespace workers { + +class RuntimeService; +class WorkerPrivate; +template <class> class WorkerPrivateParent; +class WorkerRunnable; + +// This class lets us restrict the public methods that can be called on +// WorkerThread to RuntimeService and WorkerPrivate without letting them gain +// full access to private methods (as would happen if they were simply friends). +class WorkerThreadFriendKey +{ + friend class RuntimeService; + friend class WorkerPrivate; + friend class WorkerPrivateParent<WorkerPrivate>; + + WorkerThreadFriendKey(); + ~WorkerThreadFriendKey(); +}; + +class WorkerThread final + : public nsThread +{ + class Observer; + + CondVar mWorkerPrivateCondVar; + + // Protected by nsThread::mLock. + WorkerPrivate* mWorkerPrivate; + + // Only touched on the target thread. + RefPtr<Observer> mObserver; + + // Protected by nsThread::mLock and waited on with mWorkerPrivateCondVar. + uint32_t mOtherThreadsDispatchingViaEventTarget; + +#ifdef DEBUG + // Protected by nsThread::mLock. + bool mAcceptingNonWorkerRunnables; +#endif + +public: + static already_AddRefed<WorkerThread> + Create(const WorkerThreadFriendKey& aKey); + + void + SetWorker(const WorkerThreadFriendKey& aKey, WorkerPrivate* aWorkerPrivate); + + nsresult + DispatchPrimaryRunnable(const WorkerThreadFriendKey& aKey, + already_AddRefed<nsIRunnable> aRunnable); + + nsresult + DispatchAnyThread(const WorkerThreadFriendKey& aKey, + already_AddRefed<WorkerRunnable> aWorkerRunnable); + + uint32_t + RecursionDepth(const WorkerThreadFriendKey& aKey) const; + + // Required for MinGW build #1336527 to handle compiler bug: + // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=79582 + NS_IMETHOD + RegisterIdlePeriod(already_AddRefed<nsIIdlePeriod> aIdlePeriod) override + { + return nsThread::RegisterIdlePeriod(already_AddRefed<nsIIdlePeriod>(aIdlePeriod.take())); + } + + NS_DECL_ISUPPORTS_INHERITED + +private: + WorkerThread(); + ~WorkerThread(); + + // This should only be called by consumers that have an + // nsIEventTarget/nsIThread pointer. + NS_IMETHOD + Dispatch(already_AddRefed<nsIRunnable> aRunnable, uint32_t aFlags) override; + + NS_IMETHOD + DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) override; + + NS_IMETHOD + DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t) override; +}; + +} // namespace workers +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_workers_WorkerThread_h__ diff --git a/dom/workers/Workers.h b/dom/workers/Workers.h new file mode 100644 index 000000000..89e2ccfca --- /dev/null +++ b/dom/workers/Workers.h @@ -0,0 +1,383 @@ +/* -*- 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_workers_workers_h__ +#define mozilla_dom_workers_workers_h__ + +#include "jsapi.h" +#include "mozilla/Attributes.h" +#include "mozilla/Mutex.h" +#include <stdint.h> +#include "nsAutoPtr.h" +#include "nsCOMPtr.h" +#include "nsDebug.h" +#include "nsString.h" +#include "nsTArray.h" + +#include "nsILoadContext.h" +#include "nsIWeakReferenceUtils.h" +#include "nsIInterfaceRequestor.h" +#include "mozilla/dom/ChannelInfo.h" +#include "mozilla/net/ReferrerPolicy.h" + +#define BEGIN_WORKERS_NAMESPACE \ + namespace mozilla { namespace dom { namespace workers { +#define END_WORKERS_NAMESPACE \ + } /* namespace workers */ } /* namespace dom */ } /* namespace mozilla */ +#define USING_WORKERS_NAMESPACE \ + using namespace mozilla::dom::workers; + +#define WORKERS_SHUTDOWN_TOPIC "web-workers-shutdown" + +class nsIContentSecurityPolicy; +class nsIScriptContext; +class nsIGlobalObject; +class nsPIDOMWindowInner; +class nsIPrincipal; +class nsILoadGroup; +class nsITabChild; +class nsIChannel; +class nsIRunnable; +class nsIURI; + +namespace mozilla { +namespace ipc { +class PrincipalInfo; +} // namespace ipc + +namespace dom { +// If you change this, the corresponding list in nsIWorkerDebugger.idl needs to +// be updated too. +enum WorkerType +{ + WorkerTypeDedicated, + WorkerTypeShared, + WorkerTypeService +}; + +} // namespace dom +} // namespace mozilla + +BEGIN_WORKERS_NAMESPACE + +class WorkerPrivate; + +struct PrivatizableBase +{ }; + +#ifdef DEBUG +void +AssertIsOnMainThread(); +#else +inline void +AssertIsOnMainThread() +{ } +#endif + +struct JSSettings +{ + enum { + // All the GC parameters that we support. + JSSettings_JSGC_MAX_BYTES = 0, + JSSettings_JSGC_MAX_MALLOC_BYTES, + JSSettings_JSGC_HIGH_FREQUENCY_TIME_LIMIT, + JSSettings_JSGC_LOW_FREQUENCY_HEAP_GROWTH, + JSSettings_JSGC_HIGH_FREQUENCY_HEAP_GROWTH_MIN, + JSSettings_JSGC_HIGH_FREQUENCY_HEAP_GROWTH_MAX, + JSSettings_JSGC_HIGH_FREQUENCY_LOW_LIMIT, + JSSettings_JSGC_HIGH_FREQUENCY_HIGH_LIMIT, + JSSettings_JSGC_ALLOCATION_THRESHOLD, + JSSettings_JSGC_SLICE_TIME_BUDGET, + JSSettings_JSGC_DYNAMIC_HEAP_GROWTH, + JSSettings_JSGC_DYNAMIC_MARK_SLICE, + JSSettings_JSGC_REFRESH_FRAME_SLICES, + // JSGC_MODE not supported + + // This must be last so that we get an accurate count. + kGCSettingsArraySize + }; + + struct JSGCSetting + { + JSGCParamKey key; + uint32_t value; + + JSGCSetting() + : key(static_cast<JSGCParamKey>(-1)), value(0) + { } + + bool + IsSet() const + { + return key != static_cast<JSGCParamKey>(-1); + } + + void + Unset() + { + key = static_cast<JSGCParamKey>(-1); + value = 0; + } + }; + + // There are several settings that we know we need so it makes sense to + // preallocate here. + typedef JSGCSetting JSGCSettingsArray[kGCSettingsArraySize]; + + // Settings that change based on chrome/content context. + struct JSContentChromeSettings + { + JS::CompartmentOptions compartmentOptions; + int32_t maxScriptRuntime; + + JSContentChromeSettings() + : compartmentOptions(), maxScriptRuntime(0) + { } + }; + + JSContentChromeSettings chrome; + JSContentChromeSettings content; + JSGCSettingsArray gcSettings; + JS::ContextOptions contextOptions; + +#ifdef JS_GC_ZEAL + uint8_t gcZeal; + uint32_t gcZealFrequency; +#endif + + JSSettings() +#ifdef JS_GC_ZEAL + : gcZeal(0), gcZealFrequency(0) +#endif + { + for (uint32_t index = 0; index < ArrayLength(gcSettings); index++) { + new (gcSettings + index) JSGCSetting(); + } + } + + bool + ApplyGCSetting(JSGCParamKey aKey, uint32_t aValue) + { + JSSettings::JSGCSetting* firstEmptySetting = nullptr; + JSSettings::JSGCSetting* foundSetting = nullptr; + + for (uint32_t index = 0; index < ArrayLength(gcSettings); index++) { + JSSettings::JSGCSetting& setting = gcSettings[index]; + if (setting.key == aKey) { + foundSetting = &setting; + break; + } + if (!firstEmptySetting && !setting.IsSet()) { + firstEmptySetting = &setting; + } + } + + if (aValue) { + if (!foundSetting) { + foundSetting = firstEmptySetting; + if (!foundSetting) { + NS_ERROR("Not enough space for this value!"); + return false; + } + } + foundSetting->key = aKey; + foundSetting->value = aValue; + return true; + } + + if (foundSetting) { + foundSetting->Unset(); + return true; + } + + return false; + } +}; + +enum WorkerPreference +{ +#define WORKER_SIMPLE_PREF(name, getter, NAME) WORKERPREF_ ## NAME, +#define WORKER_PREF(name, callback) +#include "mozilla/dom/WorkerPrefs.h" +#undef WORKER_SIMPLE_PREF +#undef WORKER_PREF + WORKERPREF_COUNT +}; + +// Implemented in WorkerPrivate.cpp + +struct WorkerLoadInfo +{ + // All of these should be released in WorkerPrivateParent::ForgetMainThreadObjects. + nsCOMPtr<nsIURI> mBaseURI; + nsCOMPtr<nsIURI> mResolvedScriptURI; + nsCOMPtr<nsIPrincipal> mPrincipal; + nsCOMPtr<nsIScriptContext> mScriptContext; + nsCOMPtr<nsPIDOMWindowInner> mWindow; + nsCOMPtr<nsIContentSecurityPolicy> mCSP; + nsCOMPtr<nsIChannel> mChannel; + nsCOMPtr<nsILoadGroup> mLoadGroup; + + // mLoadFailedAsyncRunnable will execute on main thread if script loading + // fails during script loading. If script loading is never started due to + // a synchronous error, then the runnable is never executed. The runnable + // is guaranteed to be released on the main thread. + nsCOMPtr<nsIRunnable> mLoadFailedAsyncRunnable; + + class InterfaceRequestor final : public nsIInterfaceRequestor + { + NS_DECL_ISUPPORTS + + public: + InterfaceRequestor(nsIPrincipal* aPrincipal, nsILoadGroup* aLoadGroup); + void MaybeAddTabChild(nsILoadGroup* aLoadGroup); + NS_IMETHOD GetInterface(const nsIID& aIID, void** aSink) override; + + private: + ~InterfaceRequestor() { } + + already_AddRefed<nsITabChild> GetAnyLiveTabChild(); + + nsCOMPtr<nsILoadContext> mLoadContext; + nsCOMPtr<nsIInterfaceRequestor> mOuterRequestor; + + // Array of weak references to nsITabChild. We do not want to keep TabChild + // actors alive for long after their ActorDestroy() methods are called. + nsTArray<nsWeakPtr> mTabChildList; + }; + + // Only set if we have a custom overriden load group + RefPtr<InterfaceRequestor> mInterfaceRequestor; + + nsAutoPtr<mozilla::ipc::PrincipalInfo> mPrincipalInfo; + nsCString mDomain; + + nsString mServiceWorkerCacheName; + + ChannelInfo mChannelInfo; + + uint64_t mWindowID; + uint64_t mServiceWorkerID; + + net::ReferrerPolicy mReferrerPolicy; + bool mFromWindow; + bool mEvalAllowed; + bool mReportCSPViolations; + bool mXHRParamsAllowed; + bool mPrincipalIsSystem; + bool mStorageAllowed; + bool mServiceWorkersTestingInWindow; + PrincipalOriginAttributes mOriginAttributes; + + WorkerLoadInfo(); + ~WorkerLoadInfo(); + + void StealFrom(WorkerLoadInfo& aOther); +}; + +// All of these are implemented in RuntimeService.cpp + +void +CancelWorkersForWindow(nsPIDOMWindowInner* aWindow); + +void +FreezeWorkersForWindow(nsPIDOMWindowInner* aWindow); + +void +ThawWorkersForWindow(nsPIDOMWindowInner* aWindow); + +void +SuspendWorkersForWindow(nsPIDOMWindowInner* aWindow); + +void +ResumeWorkersForWindow(nsPIDOMWindowInner* aWindow); + +// A class that can be used with WorkerCrossThreadDispatcher to run a +// bit of C++ code on the worker thread. +class WorkerTask +{ +protected: + WorkerTask() + { } + + virtual ~WorkerTask() + { } + +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WorkerTask) + + // The return value here has the same semantics as the return value + // of WorkerRunnable::WorkerRun. + virtual bool + RunTask(JSContext* aCx) = 0; +}; + +class WorkerCrossThreadDispatcher +{ + friend class WorkerPrivate; + + // Must be acquired *before* the WorkerPrivate's mutex, when they're both + // held. + Mutex mMutex; + WorkerPrivate* mWorkerPrivate; + +private: + // Only created by WorkerPrivate. + explicit WorkerCrossThreadDispatcher(WorkerPrivate* aWorkerPrivate); + + // Only called by WorkerPrivate. + void + Forget() + { + MutexAutoLock lock(mMutex); + mWorkerPrivate = nullptr; + } + + ~WorkerCrossThreadDispatcher() {} + +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WorkerCrossThreadDispatcher) + + // Generically useful function for running a bit of C++ code on the worker + // thread. + bool + PostTask(WorkerTask* aTask); +}; + +WorkerCrossThreadDispatcher* +GetWorkerCrossThreadDispatcher(JSContext* aCx, const JS::Value& aWorker); + +// Random unique constant to facilitate JSPrincipal debugging +const uint32_t kJSPrincipalsDebugToken = 0x7e2df9d2; + +namespace exceptions { + +// Implemented in Exceptions.cpp +void +ThrowDOMExceptionForNSResult(JSContext* aCx, nsresult aNSResult); + +} // namespace exceptions + +bool +IsWorkerGlobal(JSObject* global); + +bool +IsDebuggerGlobal(JSObject* global); + +bool +IsDebuggerSandbox(JSObject* object); + +// Throws the JSMSG_GETTER_ONLY exception. This shouldn't be used going +// forward -- getter-only properties should just use JS_PSG for the setter +// (implying no setter at all), which will not throw when set in non-strict +// code but will in strict code. Old code should use this only for temporary +// compatibility reasons. +extern bool +GetterOnlyJSNative(JSContext* aCx, unsigned aArgc, JS::Value* aVp); + +END_WORKERS_NAMESPACE + +#endif // mozilla_dom_workers_workers_h__ diff --git a/dom/workers/moz.build b/dom/workers/moz.build new file mode 100644 index 000000000..4f4b52e4a --- /dev/null +++ b/dom/workers/moz.build @@ -0,0 +1,131 @@ +# -*- 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/. + +# Public stuff. +EXPORTS.mozilla.dom += [ + 'FileReaderSync.h', + 'ServiceWorkerCommon.h', + 'ServiceWorkerContainer.h', + 'ServiceWorkerEvents.h', + 'ServiceWorkerRegistrar.h', + 'ServiceWorkerRegistration.h', + 'WorkerLocation.h', + 'WorkerNavigator.h', + 'WorkerPrefs.h', + 'WorkerPrivate.h', + 'WorkerRunnable.h', + 'WorkerScope.h', +] + +EXPORTS.mozilla.dom.workers += [ + 'RuntimeService.h', + 'ServiceWorkerInfo.h', + 'ServiceWorkerManager.h', + 'ServiceWorkerRegistrationInfo.h', + 'WorkerDebuggerManager.h', + 'Workers.h', +] + +# Stuff needed for the bindings, not really public though. +EXPORTS.mozilla.dom.workers.bindings += [ + 'ServiceWorker.h', + 'ServiceWorkerClient.h', + 'ServiceWorkerClients.h', + 'ServiceWorkerWindowClient.h', + 'SharedWorker.h', + 'WorkerHolder.h', +] + +XPIDL_MODULE = 'dom_workers' + +XPIDL_SOURCES += [ + 'nsIWorkerDebugger.idl', + 'nsIWorkerDebuggerManager.idl', +] + +UNIFIED_SOURCES += [ + 'ChromeWorkerScope.cpp', + 'FileReaderSync.cpp', + 'Principal.cpp', + 'RegisterBindings.cpp', + 'RuntimeService.cpp', + 'ScriptLoader.cpp', + 'ServiceWorker.cpp', + 'ServiceWorkerClient.cpp', + 'ServiceWorkerClients.cpp', + 'ServiceWorkerContainer.cpp', + 'ServiceWorkerEvents.cpp', + 'ServiceWorkerInfo.cpp', + 'ServiceWorkerJob.cpp', + 'ServiceWorkerJobQueue.cpp', + 'ServiceWorkerManager.cpp', + 'ServiceWorkerManagerChild.cpp', + 'ServiceWorkerManagerParent.cpp', + 'ServiceWorkerManagerService.cpp', + 'ServiceWorkerPrivate.cpp', + 'ServiceWorkerRegisterJob.cpp', + 'ServiceWorkerRegistrar.cpp', + 'ServiceWorkerRegistration.cpp', + 'ServiceWorkerRegistrationInfo.cpp', + 'ServiceWorkerScriptCache.cpp', + 'ServiceWorkerUnregisterJob.cpp', + 'ServiceWorkerUpdateJob.cpp', + 'ServiceWorkerWindowClient.cpp', + 'SharedWorker.cpp', + 'WorkerDebuggerManager.cpp', + 'WorkerHolder.cpp', + 'WorkerLocation.cpp', + 'WorkerNavigator.cpp', + 'WorkerPrivate.cpp', + 'WorkerRunnable.cpp', + 'WorkerScope.cpp', + 'WorkerThread.cpp', +] + +IPDL_SOURCES += [ + 'PServiceWorkerManager.ipdl', + 'ServiceWorkerRegistrarTypes.ipdlh', +] + +LOCAL_INCLUDES += [ + '../base', + '../system', + '/dom/base', + '/xpcom/build', + '/xpcom/threads', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' + +TEST_DIRS += [ + 'test/extensions/bootstrap', + 'test/extensions/traditional', +] + +MOCHITEST_MANIFESTS += [ + 'test/mochitest.ini', + 'test/serviceworkers/mochitest.ini', +] + +MOCHITEST_CHROME_MANIFESTS += [ + 'test/chrome.ini', + 'test/serviceworkers/chrome.ini' +] + +BROWSER_CHROME_MANIFESTS += [ + 'test/serviceworkers/browser.ini', +] + +XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini'] + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] + +TEST_DIRS += ['test/gtest'] + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] diff --git a/dom/workers/nsIWorkerDebugger.idl b/dom/workers/nsIWorkerDebugger.idl new file mode 100644 index 000000000..f8144f3da --- /dev/null +++ b/dom/workers/nsIWorkerDebugger.idl @@ -0,0 +1,50 @@ +#include "nsISupports.idl" + +interface mozIDOMWindow; +interface nsIPrincipal; + +[scriptable, uuid(9cf3b48e-361d-486a-8917-55cf8d00bb41)] +interface nsIWorkerDebuggerListener : nsISupports +{ + void onClose(); + + void onError(in DOMString filename, in unsigned long lineno, + in DOMString message); + + void onMessage(in DOMString message); +}; + +[scriptable, builtinclass, uuid(22f93aa3-8a05-46be-87e0-fa93bf8a8eff)] +interface nsIWorkerDebugger : nsISupports +{ + const unsigned long TYPE_DEDICATED = 0; + const unsigned long TYPE_SHARED = 1; + const unsigned long TYPE_SERVICE = 2; + + readonly attribute bool isClosed; + + readonly attribute bool isChrome; + + readonly attribute bool isInitialized; + + readonly attribute nsIWorkerDebugger parent; + + readonly attribute unsigned long type; + + readonly attribute DOMString url; + + readonly attribute mozIDOMWindow window; + + readonly attribute nsIPrincipal principal; + + readonly attribute unsigned long serviceWorkerID; + + void initialize(in DOMString url); + + [binaryname(PostMessageMoz)] + void postMessage(in DOMString message); + + void addListener(in nsIWorkerDebuggerListener listener); + + void removeListener(in nsIWorkerDebuggerListener listener); +}; diff --git a/dom/workers/nsIWorkerDebuggerManager.idl b/dom/workers/nsIWorkerDebuggerManager.idl new file mode 100644 index 000000000..f7a0fb309 --- /dev/null +++ b/dom/workers/nsIWorkerDebuggerManager.idl @@ -0,0 +1,22 @@ +#include "nsISupports.idl" + +interface nsISimpleEnumerator; +interface nsIWorkerDebugger; + +[scriptable, uuid(d2aa74ee-6b98-4d5d-8173-4e23422daf1e)] +interface nsIWorkerDebuggerManagerListener : nsISupports +{ + void onRegister(in nsIWorkerDebugger debugger); + + void onUnregister(in nsIWorkerDebugger debugger); +}; + +[scriptable, builtinclass, uuid(056d7918-dc86-452a-b4e6-86da3405f015)] +interface nsIWorkerDebuggerManager : nsISupports +{ + nsISimpleEnumerator getWorkerDebuggerEnumerator(); + + void addListener(in nsIWorkerDebuggerManagerListener listener); + + void removeListener(in nsIWorkerDebuggerManagerListener listener); +}; diff --git a/dom/workers/test/404_server.sjs b/dom/workers/test/404_server.sjs new file mode 100644 index 000000000..f5dff1abb --- /dev/null +++ b/dom/workers/test/404_server.sjs @@ -0,0 +1,10 @@ +function handleRequest(request, response) +{ + response.setStatusLine(request.httpVersion, 404, "Not found"); + + // Any valid JS. + if (request.queryString == 'js') { + response.setHeader("Content-Type", "text/javascript", false); + response.write('4 + 4'); + } +} diff --git a/dom/workers/test/WorkerDebugger.console_childWorker.js b/dom/workers/test/WorkerDebugger.console_childWorker.js new file mode 100644 index 000000000..8cee6809e --- /dev/null +++ b/dom/workers/test/WorkerDebugger.console_childWorker.js @@ -0,0 +1,3 @@ +"use strict"; + +self.onmessage = function () {}; diff --git a/dom/workers/test/WorkerDebugger.console_debugger.js b/dom/workers/test/WorkerDebugger.console_debugger.js new file mode 100644 index 000000000..662bd3520 --- /dev/null +++ b/dom/workers/test/WorkerDebugger.console_debugger.js @@ -0,0 +1,41 @@ +"use strict" + +function ok(a, msg) { + postMessage(JSON.stringify({ type: 'status', what: !!a, msg: msg })); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function finish() { + postMessage(JSON.stringify({ type: 'finish' })); +} + +function magic() { + console.log("Hello from the debugger script!"); + + var foo = retrieveConsoleEvents(); + ok(Array.isArray(foo), "We received an array."); + ok(foo.length >= 2, "At least 2 messages."); + + is(foo[0].arguments[0], "Can you see this console message?", "First message ok."); + is(foo[1].arguments[0], "Can you see this second console message?", "Second message ok."); + + setConsoleEventHandler(function(consoleData) { + is(consoleData.arguments[0], "Random message.", "Random message ok!"); + + // The consoleEventHandler can be null. + setConsoleEventHandler(null); + + finish(); + }); +} + +this.onmessage = function (event) { + switch (event.data) { + case "do magic": + magic(); + break; + } +}; diff --git a/dom/workers/test/WorkerDebugger.console_worker.js b/dom/workers/test/WorkerDebugger.console_worker.js new file mode 100644 index 000000000..2db43a3d7 --- /dev/null +++ b/dom/workers/test/WorkerDebugger.console_worker.js @@ -0,0 +1,10 @@ +"use strict"; + +console.log("Can you see this console message?"); +console.warn("Can you see this second console message?"); + +var worker = new Worker("WorkerDebugger.console_childWorker.js"); + +setInterval(function() { + console.log("Random message."); +}, 200); diff --git a/dom/workers/test/WorkerDebugger.initialize_childWorker.js b/dom/workers/test/WorkerDebugger.initialize_childWorker.js new file mode 100644 index 000000000..a85764bd9 --- /dev/null +++ b/dom/workers/test/WorkerDebugger.initialize_childWorker.js @@ -0,0 +1,6 @@ +"use strict"; + +self.onmessage = function () {}; + +debugger; +postMessage("worker"); diff --git a/dom/workers/test/WorkerDebugger.initialize_debugger.js b/dom/workers/test/WorkerDebugger.initialize_debugger.js new file mode 100644 index 000000000..f52e95b15 --- /dev/null +++ b/dom/workers/test/WorkerDebugger.initialize_debugger.js @@ -0,0 +1,6 @@ +"use strict"; + +var dbg = new Debugger(global); +dbg.onDebuggerStatement = function (frame) { + frame.eval("postMessage('debugger');"); +}; diff --git a/dom/workers/test/WorkerDebugger.initialize_worker.js b/dom/workers/test/WorkerDebugger.initialize_worker.js new file mode 100644 index 000000000..5a24efd3a --- /dev/null +++ b/dom/workers/test/WorkerDebugger.initialize_worker.js @@ -0,0 +1,9 @@ +"use strict"; + +var worker = new Worker("WorkerDebugger.initialize_childWorker.js"); +worker.onmessage = function (event) { + postMessage("child:" + event.data); +}; + +debugger; +postMessage("worker"); diff --git a/dom/workers/test/WorkerDebugger.postMessage_childWorker.js b/dom/workers/test/WorkerDebugger.postMessage_childWorker.js new file mode 100644 index 000000000..8cee6809e --- /dev/null +++ b/dom/workers/test/WorkerDebugger.postMessage_childWorker.js @@ -0,0 +1,3 @@ +"use strict"; + +self.onmessage = function () {}; diff --git a/dom/workers/test/WorkerDebugger.postMessage_debugger.js b/dom/workers/test/WorkerDebugger.postMessage_debugger.js new file mode 100644 index 000000000..4a231b7ab --- /dev/null +++ b/dom/workers/test/WorkerDebugger.postMessage_debugger.js @@ -0,0 +1,9 @@ +"use strict" + +this.onmessage = function (event) { + switch (event.data) { + case "ping": + postMessage("pong"); + break; + } +}; diff --git a/dom/workers/test/WorkerDebugger.postMessage_worker.js b/dom/workers/test/WorkerDebugger.postMessage_worker.js new file mode 100644 index 000000000..8ddf6cf86 --- /dev/null +++ b/dom/workers/test/WorkerDebugger.postMessage_worker.js @@ -0,0 +1,3 @@ +"use strict"; + +var worker = new Worker("WorkerDebugger.postMessage_childWorker.js"); diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_debugger.js b/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_debugger.js new file mode 100644 index 000000000..908c9f316 --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_debugger.js @@ -0,0 +1,9 @@ +"use strict"; + +const SANDBOX_URL = "WorkerDebuggerGlobalScope.createSandbox_sandbox.js"; + +var prototype = { + self: this, +}; +var sandbox = createSandbox(SANDBOX_URL, prototype); +loadSubScript(SANDBOX_URL, sandbox); diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_sandbox.js b/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_sandbox.js new file mode 100644 index 000000000..f94d65062 --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_sandbox.js @@ -0,0 +1,9 @@ +"use strict"; + +self.addEventListener("message", function(event) { + switch (event.data) { + case "ping": + self.postMessage("pong"); + break; + } +}); diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_worker.js b/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_worker.js new file mode 100644 index 000000000..8cee6809e --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.createSandbox_worker.js @@ -0,0 +1,3 @@ +"use strict"; + +self.onmessage = function () {}; diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_childWorker.js b/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_childWorker.js new file mode 100644 index 000000000..b76f45a4d --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_childWorker.js @@ -0,0 +1,14 @@ +"use strict"; + +function f() { + debugger; +} + +self.onmessage = function (event) { + switch (event.data) { + case "ping": + debugger; + postMessage("pong"); + break; + }; +}; diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_debugger.js b/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_debugger.js new file mode 100644 index 000000000..dcd4dbecd --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_debugger.js @@ -0,0 +1,29 @@ +"use strict"; + +var frames = []; + +var dbg = new Debugger(global); +dbg.onDebuggerStatement = function (frame) { + frames.push(frame); + postMessage("paused"); + enterEventLoop(); + frames.pop(); + postMessage("resumed"); +}; + +this.onmessage = function (event) { + switch (event.data) { + case "eval": + frames[frames.length - 1].eval("f()"); + postMessage("evalled"); + break; + + case "ping": + postMessage("pong"); + break; + + case "resume": + leaveEventLoop(); + break; + }; +}; diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_worker.js b/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_worker.js new file mode 100644 index 000000000..c43738516 --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.enterEventLoop_worker.js @@ -0,0 +1,25 @@ +"use strict"; + +function f() { + debugger; +} + +var worker = new Worker("WorkerDebuggerGlobalScope.enterEventLoop_childWorker.js"); + +worker.onmessage = function (event) { + postMessage("child:" + event.data); +}; + +self.onmessage = function (event) { + var message = event.data; + if (message.indexOf(":") >= 0) { + worker.postMessage(message.split(":")[1]); + return; + } + switch (message) { + case "ping": + debugger; + postMessage("pong"); + break; + }; +}; diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.reportError_childWorker.js b/dom/workers/test/WorkerDebuggerGlobalScope.reportError_childWorker.js new file mode 100644 index 000000000..823e7c477 --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.reportError_childWorker.js @@ -0,0 +1,5 @@ +"use strict"; + +self.onerror = function () { + postMessage("error"); +} diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.reportError_debugger.js b/dom/workers/test/WorkerDebuggerGlobalScope.reportError_debugger.js new file mode 100644 index 000000000..67ea08de5 --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.reportError_debugger.js @@ -0,0 +1,12 @@ +"use strict"; + +this.onmessage = function (event) { + switch (event.data) { + case "report": + reportError("reported"); + break; + case "throw": + throw new Error("thrown"); + break; + } +}; diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.reportError_worker.js b/dom/workers/test/WorkerDebuggerGlobalScope.reportError_worker.js new file mode 100644 index 000000000..67ccfc2ca --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.reportError_worker.js @@ -0,0 +1,11 @@ +"use strict"; + +var worker = new Worker("WorkerDebuggerGlobalScope.reportError_childWorker.js"); + +worker.onmessage = function (event) { + postMessage("child:" + event.data); +}; + +self.onerror = function () { + postMessage("error"); +}; diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.setImmediate_debugger.js b/dom/workers/test/WorkerDebuggerGlobalScope.setImmediate_debugger.js new file mode 100644 index 000000000..b5075c70f --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.setImmediate_debugger.js @@ -0,0 +1,12 @@ +"use strict"; + +this.onmessage = function (event) { + switch (event.data) { + case "ping": + setImmediate(function () { + postMessage("pong1"); + }); + postMessage("pong2"); + break; + } +}; diff --git a/dom/workers/test/WorkerDebuggerGlobalScope.setImmediate_worker.js b/dom/workers/test/WorkerDebuggerGlobalScope.setImmediate_worker.js new file mode 100644 index 000000000..5a72b0f24 --- /dev/null +++ b/dom/workers/test/WorkerDebuggerGlobalScope.setImmediate_worker.js @@ -0,0 +1,3 @@ +"use strict" + +self.onmessage = function () {}; diff --git a/dom/workers/test/WorkerDebuggerManager_childWorker.js b/dom/workers/test/WorkerDebuggerManager_childWorker.js new file mode 100644 index 000000000..8cee6809e --- /dev/null +++ b/dom/workers/test/WorkerDebuggerManager_childWorker.js @@ -0,0 +1,3 @@ +"use strict"; + +self.onmessage = function () {}; diff --git a/dom/workers/test/WorkerDebuggerManager_worker.js b/dom/workers/test/WorkerDebuggerManager_worker.js new file mode 100644 index 000000000..0737d17eb --- /dev/null +++ b/dom/workers/test/WorkerDebuggerManager_worker.js @@ -0,0 +1,3 @@ +"use strict"; + +var worker = new Worker("WorkerDebuggerManager_childWorker.js"); diff --git a/dom/workers/test/WorkerDebugger_childWorker.js b/dom/workers/test/WorkerDebugger_childWorker.js new file mode 100644 index 000000000..8cee6809e --- /dev/null +++ b/dom/workers/test/WorkerDebugger_childWorker.js @@ -0,0 +1,3 @@ +"use strict"; + +self.onmessage = function () {}; diff --git a/dom/workers/test/WorkerDebugger_frozen_iframe1.html b/dom/workers/test/WorkerDebugger_frozen_iframe1.html new file mode 100644 index 000000000..591923121 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_frozen_iframe1.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + var worker = new Worker("WorkerDebugger_frozen_worker1.js"); + worker.onmessage = function () { + parent.postMessage("ready", "*"); + }; + </script> + </head> + <body> + This is page 1. + </body> +<html> diff --git a/dom/workers/test/WorkerDebugger_frozen_iframe2.html b/dom/workers/test/WorkerDebugger_frozen_iframe2.html new file mode 100644 index 000000000..96d5c56eb --- /dev/null +++ b/dom/workers/test/WorkerDebugger_frozen_iframe2.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + var worker = new Worker("WorkerDebugger_frozen_worker2.js"); + worker.onmessage = function () { + parent.postMessage("ready", "*"); + }; + </script> + </head> + <body> + This is page 2. + </body> +<html> diff --git a/dom/workers/test/WorkerDebugger_frozen_worker1.js b/dom/workers/test/WorkerDebugger_frozen_worker1.js new file mode 100644 index 000000000..371d2c064 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_frozen_worker1.js @@ -0,0 +1,5 @@ +"use strict"; + +onmessage = function () {}; + +postMessage("ready"); diff --git a/dom/workers/test/WorkerDebugger_frozen_worker2.js b/dom/workers/test/WorkerDebugger_frozen_worker2.js new file mode 100644 index 000000000..371d2c064 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_frozen_worker2.js @@ -0,0 +1,5 @@ +"use strict"; + +onmessage = function () {}; + +postMessage("ready"); diff --git a/dom/workers/test/WorkerDebugger_promise_debugger.js b/dom/workers/test/WorkerDebugger_promise_debugger.js new file mode 100644 index 000000000..7d7eaf532 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_promise_debugger.js @@ -0,0 +1,30 @@ +"use strict"; + +var self = this; + +self.onmessage = function (event) { + if (event.data !== "resolve") { + return; + } + // This then-handler should be executed inside the top-level event loop, + // within the context of the debugger's global. + Promise.resolve().then(function () { + var dbg = new Debugger(global); + dbg.onDebuggerStatement = function () { + self.onmessage = function (event) { + if (event.data !== "resume") { + return; + } + // This then-handler should be executed inside the nested event loop, + // within the context of the debugger's global. + Promise.resolve().then(function () { + postMessage("resumed"); + leaveEventLoop(); + }); + }; + postMessage("paused"); + enterEventLoop(); + }; + postMessage("resolved"); + }); +}; diff --git a/dom/workers/test/WorkerDebugger_promise_worker.js b/dom/workers/test/WorkerDebugger_promise_worker.js new file mode 100644 index 000000000..a77737af5 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_promise_worker.js @@ -0,0 +1,25 @@ +"use strict"; + +self.onmessage = function (event) { + if (event.data !== "resolve") { + return; + } + // This then-handler should be executed inside the top-level event loop, + // within the context of the worker's global. + Promise.resolve().then(function () { + self.onmessage = function (event) { + if (event.data !== "pause") { + return; + } + // This then-handler should be executed inside the top-level event loop, + // within the context of the worker's global. Because the debugger + // statement here below enters a nested event loop, the then-handler + // should not be executed until the debugger statement returns. + Promise.resolve().then(function () { + postMessage("resumed"); + }); + debugger; + } + postMessage("resolved"); + }); +}; diff --git a/dom/workers/test/WorkerDebugger_sharedWorker.js b/dom/workers/test/WorkerDebugger_sharedWorker.js new file mode 100644 index 000000000..5ad97d4c5 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_sharedWorker.js @@ -0,0 +1,11 @@ +"use strict"; + +self.onconnect = function (event) { + event.ports[0].onmessage = function (event) { + switch (event.data) { + case "close": + close(); + break; + } + }; +}; diff --git a/dom/workers/test/WorkerDebugger_suspended_debugger.js b/dom/workers/test/WorkerDebugger_suspended_debugger.js new file mode 100644 index 000000000..2ed4e16c4 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_suspended_debugger.js @@ -0,0 +1,6 @@ +"use strict"; + +var dbg = new Debugger(global); +dbg.onDebuggerStatement = function (frame) { + postMessage("debugger"); +}; diff --git a/dom/workers/test/WorkerDebugger_suspended_worker.js b/dom/workers/test/WorkerDebugger_suspended_worker.js new file mode 100644 index 000000000..c096be7e4 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_suspended_worker.js @@ -0,0 +1,6 @@ +"use strict"; + +self.onmessage = function () { + postMessage("worker"); + debugger; +}; diff --git a/dom/workers/test/WorkerDebugger_worker.js b/dom/workers/test/WorkerDebugger_worker.js new file mode 100644 index 000000000..f301ac1e8 --- /dev/null +++ b/dom/workers/test/WorkerDebugger_worker.js @@ -0,0 +1,8 @@ +"use strict"; + +var worker = new Worker("WorkerDebugger_childWorker.js"); +self.onmessage = function (event) { + postMessage("child:" + event.data); +}; +debugger; +postMessage("worker"); diff --git a/dom/workers/test/WorkerTest.jsm b/dom/workers/test/WorkerTest.jsm new file mode 100644 index 000000000..86431b7f8 --- /dev/null +++ b/dom/workers/test/WorkerTest.jsm @@ -0,0 +1,17 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +this.EXPORTED_SYMBOLS = [ + "WorkerTest" +]; + +this.WorkerTest = { + go: function(message, messageCallback, errorCallback) { + let worker = new ChromeWorker("WorkerTest_worker.js"); + worker.onmessage = messageCallback; + worker.onerror = errorCallback; + worker.postMessage(message); + return worker; + } +}; diff --git a/dom/workers/test/WorkerTest_badworker.js b/dom/workers/test/WorkerTest_badworker.js new file mode 100644 index 000000000..7a1df54bd --- /dev/null +++ b/dom/workers/test/WorkerTest_badworker.js @@ -0,0 +1,7 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function(event) { + throw "Shouldn't be able to read this!"; +} diff --git a/dom/workers/test/WorkerTest_subworker.js b/dom/workers/test/WorkerTest_subworker.js new file mode 100644 index 000000000..3e9242ed6 --- /dev/null +++ b/dom/workers/test/WorkerTest_subworker.js @@ -0,0 +1,43 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function(event) { + let chromeURL = event.data.replace("test_chromeWorkerJSM.xul", + "WorkerTest_badworker.js"); + + let mochitestURL = event.data.replace("test_chromeWorkerJSM.xul", + "WorkerTest_badworker.js") + .replace("chrome://mochitests/content/chrome", + "http://mochi.test:8888/tests"); + + // We should be able to XHR to anything we want, including a chrome URL. + let xhr = new XMLHttpRequest(); + xhr.open("GET", mochitestURL, false); + xhr.send(); + + if (!xhr.responseText) { + throw "Can't load script file via XHR!"; + } + + // We shouldn't be able to make a ChromeWorker to a non-chrome URL. + let worker = new ChromeWorker(mochitestURL); + worker.onmessage = function(event) { + throw event.data; + }; + worker.onerror = function(event) { + event.preventDefault(); + + // And we shouldn't be able to make a regular Worker to a non-chrome URL. + worker = new Worker(mochitestURL); + worker.onmessage = function(event) { + throw event.data; + }; + worker.onerror = function(event) { + event.preventDefault(); + postMessage("Done"); + }; + worker.postMessage("Hi"); + }; + worker.postMessage("Hi"); +}; diff --git a/dom/workers/test/WorkerTest_worker.js b/dom/workers/test/WorkerTest_worker.js new file mode 100644 index 000000000..445408f8f --- /dev/null +++ b/dom/workers/test/WorkerTest_worker.js @@ -0,0 +1,11 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function(event) { + let worker = new ChromeWorker("WorkerTest_subworker.js"); + worker.onmessage = function(event) { + postMessage(event.data); + } + worker.postMessage(event.data); +} diff --git a/dom/workers/test/atob_worker.js b/dom/workers/test/atob_worker.js new file mode 100644 index 000000000..680ca904b --- /dev/null +++ b/dom/workers/test/atob_worker.js @@ -0,0 +1,46 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var data = [ -1, 0, 1, 1.5, /* null ,*/ undefined, true, false, "foo", + "123456789012345", "1234567890123456", "12345678901234567"]; + +var str = ""; +for (var i = 0; i < 30; i++) { + data.push(str); + str += i % 2 ? "b" : "a"; +} + +onmessage = function(event) { + data.forEach(function(string) { + var encoded = btoa(string); + postMessage({ type: "btoa", value: encoded }); + postMessage({ type: "atob", value: atob(encoded) }); + }); + + var threw; + try { + atob(); + } + catch(e) { + threw = true; + } + + if (!threw) { + throw "atob didn't throw when called without an argument!"; + } + threw = false; + + try { + btoa(); + } + catch(e) { + threw = true; + } + + if (!threw) { + throw "btoa didn't throw when called without an argument!"; + } + + postMessage({ type: "done" }); +} diff --git a/dom/workers/test/browser.ini b/dom/workers/test/browser.ini new file mode 100644 index 000000000..ae1e27d13 --- /dev/null +++ b/dom/workers/test/browser.ini @@ -0,0 +1,12 @@ +[DEFAULT] +support-files = + bug1047663_tab.html + bug1047663_worker.sjs + frame_script.js + head.js + !/dom/base/test/file_empty.html + !/dom/base/test/file_bug945152.jar + +[browser_bug1047663.js] +[browser_bug1104623.js] +run-if = buildapp == 'browser' diff --git a/dom/workers/test/browser_bug1047663.js b/dom/workers/test/browser_bug1047663.js new file mode 100644 index 000000000..8fa2c5358 --- /dev/null +++ b/dom/workers/test/browser_bug1047663.js @@ -0,0 +1,50 @@ +/* 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/. */ +"use strict"; + +const TAB_URL = EXAMPLE_URL + "bug1047663_tab.html"; +const WORKER_URL = EXAMPLE_URL + "bug1047663_worker.sjs"; + +function test() { + waitForExplicitFinish(); + + Task.spawn(function* () { + let tab = yield addTab(TAB_URL); + + // Create a worker. Post a message to it, and check the reply. Since the + // server side JavaScript file returns the first source for the first + // request, the reply should be "one". If the reply is correct, terminate + // the worker. + yield createWorkerInTab(tab, WORKER_URL); + let message = yield postMessageToWorkerInTab(tab, WORKER_URL, "ping"); + is(message, "one"); + yield terminateWorkerInTab(tab, WORKER_URL); + + // Create a second worker with the same URL. Post a message to it, and check + // the reply. The server side JavaScript file returns the second source for + // all subsequent requests, but since the cache is still enabled, the reply + // should still be "one". If the reply is correct, terminate the worker. + yield createWorkerInTab(tab, WORKER_URL); + message = yield postMessageToWorkerInTab(tab, WORKER_URL, "ping"); + is(message, "one"); + yield terminateWorkerInTab(tab, WORKER_URL); + + // Disable the cache in this tab. This should also disable the cache for all + // workers in this tab. + yield disableCacheInTab(tab); + + // Create a third worker with the same URL. Post a message to it, and check + // the reply. Since the server side JavaScript file returns the second + // source for all subsequent requests, and the cache is now disabled, the + // reply should now be "two". If the reply is correct, terminate the worker. + yield createWorkerInTab(tab, WORKER_URL); + message = yield postMessageToWorkerInTab(tab, WORKER_URL, "ping"); + is(message, "two"); + yield terminateWorkerInTab(tab, WORKER_URL); + + removeTab(tab); + + finish(); + }); +} diff --git a/dom/workers/test/browser_bug1104623.js b/dom/workers/test/browser_bug1104623.js new file mode 100644 index 000000000..64a8eeb9c --- /dev/null +++ b/dom/workers/test/browser_bug1104623.js @@ -0,0 +1,53 @@ +/* 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/. */ + +function whenBrowserLoaded(aBrowser, aCallback) { + aBrowser.addEventListener("load", function onLoad(event) { + if (event.target == aBrowser.contentDocument) { + aBrowser.removeEventListener("load", onLoad, true); + executeSoon(aCallback); + } + }, true); +} + +function test() { + waitForExplicitFinish(); + + let testURL = "chrome://mochitests/content/chrome/dom/base/test/file_empty.html"; + + let tab = gBrowser.addTab(testURL); + gBrowser.selectedTab = tab; + + whenBrowserLoaded(tab.linkedBrowser, function() { + let doc = tab.linkedBrowser.contentDocument; + let contentWin = tab.linkedBrowser.contentWindow; + + let blob = new contentWin.Blob(['onmessage = function() { postMessage(true); }']); + ok(blob, "Blob has been created"); + + let blobURL = contentWin.URL.createObjectURL(blob); + ok(blobURL, "Blob URL has been created"); + + let worker = new contentWin.Worker(blobURL); + ok(worker, "Worker has been created"); + + worker.onerror = function(error) { + ok(false, "Worker.onerror:" + error.message); + worker.terminate(); + contentWin.URL.revokeObjectURL(blob); + gBrowser.removeTab(tab); + executeSoon(finish); + } + + worker.onmessage = function() { + ok(true, "Worker.onmessage"); + worker.terminate(); + contentWin.URL.revokeObjectURL(blob); + gBrowser.removeTab(tab); + executeSoon(finish); + } + + worker.postMessage(true); + }); +} diff --git a/dom/workers/test/bug1014466_data1.txt b/dom/workers/test/bug1014466_data1.txt new file mode 100644 index 000000000..a32a4347a --- /dev/null +++ b/dom/workers/test/bug1014466_data1.txt @@ -0,0 +1 @@ +1234567890 diff --git a/dom/workers/test/bug1014466_data2.txt b/dom/workers/test/bug1014466_data2.txt new file mode 100644 index 000000000..4d40154ce --- /dev/null +++ b/dom/workers/test/bug1014466_data2.txt @@ -0,0 +1 @@ +ABCDEFGH diff --git a/dom/workers/test/bug1014466_worker.js b/dom/workers/test/bug1014466_worker.js new file mode 100644 index 000000000..beb324f0f --- /dev/null +++ b/dom/workers/test/bug1014466_worker.js @@ -0,0 +1,64 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function ok(a, msg) { + postMessage({type: "status", status: !!a, msg: msg }); +} + +onmessage = function(event) { + + function getResponse(url) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, false); + xhr.send(); + return xhr.responseText; + } + + const testFile1 = "bug1014466_data1.txt"; + const testFile2 = "bug1014466_data2.txt"; + const testData1 = getResponse(testFile1); + const testData2 = getResponse(testFile2); + + var response_count = 0; + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (xhr.readyState == xhr.DONE && xhr.status == 200) { + response_count++; + switch (response_count) { + case 1: + ok(xhr.responseText == testData1, "Check data 1"); + test_data2(); + break; + case 2: + ok(xhr.responseText == testData2, "Check data 2"); + postMessage({type: "finish" }); + break; + default: + ok(false, "Unexpected response received"); + postMessage({type: "finish" }); + break; + } + } + } + xhr.onerror = function(event) { + ok(false, "Got an error event: " + event); + postMessage({type: "finish" }); + } + + function test_data1() { + xhr.open("GET", testFile1, true); + xhr.responseType = "text"; + xhr.send(); + } + + function test_data2() { + xhr.abort(); + xhr.open("GET", testFile2, true); + xhr.responseType = "text"; + xhr.send(); + } + + test_data1(); +} diff --git a/dom/workers/test/bug1020226_frame.html b/dom/workers/test/bug1020226_frame.html new file mode 100644 index 000000000..7f810d893 --- /dev/null +++ b/dom/workers/test/bug1020226_frame.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1020226 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1020226</title> +</head> +<body> + +<script type="application/javascript"> + var worker = new Worker("bug1020226_worker.js"); + worker.onmessage = function(e) { + window.parent.postMessage("loaded", "*"); + } +</script> +</body> +</html> + diff --git a/dom/workers/test/bug1020226_worker.js b/dom/workers/test/bug1020226_worker.js new file mode 100644 index 000000000..868b1a2f2 --- /dev/null +++ b/dom/workers/test/bug1020226_worker.js @@ -0,0 +1,12 @@ +var p = new Promise(function(resolve, reject) { + // This causes a runnable to be queued. + reject(new Error()); + postMessage("loaded"); + + // This prevents that runnable from running until the window calls terminate(), + // at which point the worker goes into the Canceling state and then an + // HoldWorker() is attempted, which fails, which used to result in + // multiple calls to the error reporter, one after the worker's context had + // been GCed. + while (true); +}); diff --git a/dom/workers/test/bug1047663_tab.html b/dom/workers/test/bug1047663_tab.html new file mode 100644 index 000000000..62ab9be7d --- /dev/null +++ b/dom/workers/test/bug1047663_tab.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"/> + </head> + <body> + </body> +</html> diff --git a/dom/workers/test/bug1047663_worker.sjs b/dom/workers/test/bug1047663_worker.sjs new file mode 100644 index 000000000..a39bf4474 --- /dev/null +++ b/dom/workers/test/bug1047663_worker.sjs @@ -0,0 +1,40 @@ +/* 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/. */ +"use strict"; + +const WORKER_1 = ` + "use strict"; + + self.onmessage = function () { + postMessage("one"); + }; +`; + +const WORKER_2 = ` + "use strict"; + + self.onmessage = function () { + postMessage("two"); + }; +`; + +function handleRequest(request, response) { + let count = getState("count"); + if (count === "") { + count = "1"; + } + + // This header is necessary for the cache to trigger. + response.setHeader("Cache-control", "max-age=3600"); + + // If this is the first request, return the first source. + if (count === "1") { + response.write(WORKER_1); + setState("count", "2"); + } + // For all subsequent requests, return the second source. + else { + response.write(WORKER_2); + } +} diff --git a/dom/workers/test/bug1060621_worker.js b/dom/workers/test/bug1060621_worker.js new file mode 100644 index 000000000..a0fcd3f60 --- /dev/null +++ b/dom/workers/test/bug1060621_worker.js @@ -0,0 +1,2 @@ +navigator.foobar = 42; +postMessage('done'); diff --git a/dom/workers/test/bug1062920_worker.js b/dom/workers/test/bug1062920_worker.js new file mode 100644 index 000000000..d3df38870 --- /dev/null +++ b/dom/workers/test/bug1062920_worker.js @@ -0,0 +1,6 @@ +postMessage({ appCodeName: navigator.appCodeName, + appName: navigator.appName, + appVersion: navigator.appVersion, + platform: navigator.platform, + userAgent: navigator.userAgent, + product: navigator.product }); diff --git a/dom/workers/test/bug1063538_worker.js b/dom/workers/test/bug1063538_worker.js new file mode 100644 index 000000000..dc53dd289 --- /dev/null +++ b/dom/workers/test/bug1063538_worker.js @@ -0,0 +1,25 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var gJar = "jar:http://example.org/tests/dom/base/test/file_bug945152.jar!/data_big.txt"; +var xhr = new XMLHttpRequest({mozAnon: true, mozSystem: true}); +var progressFired = false; + +xhr.onloadend = function(e) { + postMessage({type: 'finish', progressFired: progressFired }); + self.close(); +}; + +xhr.onprogress = function(e) { + if (e.loaded > 0) { + progressFired = true; + xhr.abort(); + } +}; + +onmessage = function(e) { + xhr.open("GET", gJar, true); + xhr.send(); +} diff --git a/dom/workers/test/bug1104064_worker.js b/dom/workers/test/bug1104064_worker.js new file mode 100644 index 000000000..5e627c86f --- /dev/null +++ b/dom/workers/test/bug1104064_worker.js @@ -0,0 +1,10 @@ +onmessage = function() { + var counter = 0; + var id = setInterval(function() { + ++counter; + if (counter == 2) { + clearInterval(id); + postMessage('done'); + } + }, 0); +} diff --git a/dom/workers/test/bug1132395_sharedWorker.js b/dom/workers/test/bug1132395_sharedWorker.js new file mode 100644 index 000000000..988eea106 --- /dev/null +++ b/dom/workers/test/bug1132395_sharedWorker.js @@ -0,0 +1,12 @@ +dump("SW created\n"); +onconnect = function(evt) { + dump("SW onconnect\n"); + evt.ports[0].onmessage = function(e) { + dump("SW onmessage\n"); + var blob = new Blob(['123'], { type: 'text/plain' }); + dump("SW blob created\n"); + var url = URL.createObjectURL(blob); + dump("SW url created: " + url + "\n"); + evt.ports[0].postMessage('alive \\o/'); + }; +} diff --git a/dom/workers/test/bug1132924_worker.js b/dom/workers/test/bug1132924_worker.js new file mode 100644 index 000000000..cabd40686 --- /dev/null +++ b/dom/workers/test/bug1132924_worker.js @@ -0,0 +1,10 @@ +onmessage = function() { + var a = new XMLHttpRequest(); + a.open('GET', 'empty.html', false); + a.onreadystatechange = function() { + if (a.readyState == 4) { + postMessage(a.response); + } + } + a.send(null); +} diff --git a/dom/workers/test/bug978260_worker.js b/dom/workers/test/bug978260_worker.js new file mode 100644 index 000000000..126b9c901 --- /dev/null +++ b/dom/workers/test/bug978260_worker.js @@ -0,0 +1,7 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// Tell the main thread we're here. +postMessage("loaded"); diff --git a/dom/workers/test/bug998474_worker.js b/dom/workers/test/bug998474_worker.js new file mode 100644 index 000000000..a14ed2810 --- /dev/null +++ b/dom/workers/test/bug998474_worker.js @@ -0,0 +1,6 @@ +self.addEventListener("connect", function(e) { + var port = e.ports[0]; + port.onmessage = function(e) { + port.postMessage(eval(e.data)); + }; +}); diff --git a/dom/workers/test/chrome.ini b/dom/workers/test/chrome.ini new file mode 100644 index 000000000..c01a20b2b --- /dev/null +++ b/dom/workers/test/chrome.ini @@ -0,0 +1,86 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + WorkerDebugger.console_childWorker.js + WorkerDebugger.console_debugger.js + WorkerDebugger.console_worker.js + WorkerDebugger.initialize_childWorker.js + WorkerDebugger.initialize_debugger.js + WorkerDebugger.initialize_worker.js + WorkerDebugger.postMessage_childWorker.js + WorkerDebugger.postMessage_debugger.js + WorkerDebugger.postMessage_worker.js + WorkerDebuggerGlobalScope.createSandbox_debugger.js + WorkerDebuggerGlobalScope.createSandbox_sandbox.js + WorkerDebuggerGlobalScope.createSandbox_worker.js + WorkerDebuggerGlobalScope.enterEventLoop_childWorker.js + WorkerDebuggerGlobalScope.enterEventLoop_debugger.js + WorkerDebuggerGlobalScope.enterEventLoop_worker.js + WorkerDebuggerGlobalScope.reportError_childWorker.js + WorkerDebuggerGlobalScope.reportError_debugger.js + WorkerDebuggerGlobalScope.reportError_worker.js + WorkerDebuggerGlobalScope.setImmediate_debugger.js + WorkerDebuggerGlobalScope.setImmediate_worker.js + WorkerDebuggerManager_childWorker.js + WorkerDebuggerManager_worker.js + WorkerDebugger_childWorker.js + WorkerDebugger_frozen_iframe1.html + WorkerDebugger_frozen_iframe2.html + WorkerDebugger_frozen_worker1.js + WorkerDebugger_frozen_worker2.js + WorkerDebugger_promise_debugger.js + WorkerDebugger_promise_worker.js + WorkerDebugger_sharedWorker.js + WorkerDebugger_suspended_debugger.js + WorkerDebugger_suspended_worker.js + WorkerDebugger_worker.js + WorkerTest.jsm + WorkerTest_subworker.js + WorkerTest_worker.js + bug1062920_worker.js + chromeWorker_subworker.js + chromeWorker_worker.js + dom_worker_helper.js + empty.html + fileBlobSubWorker_worker.js + fileBlob_worker.js + filePosting_worker.js + fileReadSlice_worker.js + fileReaderSyncErrors_worker.js + fileReaderSync_worker.js + fileSlice_worker.js + fileSubWorker_worker.js + file_worker.js + sharedWorker_privateBrowsing.js + workersDisabled_worker.js + +[test_WorkerDebugger.initialize.xul] +[test_WorkerDebugger.postMessage.xul] +[test_WorkerDebugger.xul] +[test_WorkerDebuggerGlobalScope.createSandbox.xul] +[test_WorkerDebuggerGlobalScope.enterEventLoop.xul] +[test_WorkerDebuggerGlobalScope.reportError.xul] +skip-if = (os == 'linux') # Bug 1244697 +[test_WorkerDebuggerGlobalScope.setImmediate.xul] +[test_WorkerDebuggerManager.xul] +skip-if = (os == 'linux') # Bug 1244409 +[test_WorkerDebugger_console.xul] +[test_WorkerDebugger_frozen.xul] +[test_WorkerDebugger_promise.xul] +[test_WorkerDebugger_suspended.xul] +[test_chromeWorker.xul] +[test_chromeWorkerJSM.xul] +[test_extension.xul] +[test_extensionBootstrap.xul] +[test_file.xul] +[test_fileBlobPosting.xul] +[test_fileBlobSubWorker.xul] +[test_filePosting.xul] +[test_fileReadSlice.xul] +[test_fileReaderSync.xul] +[test_fileReaderSyncErrors.xul] +[test_fileSlice.xul] +[test_fileSubWorker.xul] +[test_workersDisabled.xul] +[test_bug1062920.xul] +[test_sharedWorker_privateBrowsing.html] diff --git a/dom/workers/test/chromeWorker_subworker.js b/dom/workers/test/chromeWorker_subworker.js new file mode 100644 index 000000000..5ad1a9923 --- /dev/null +++ b/dom/workers/test/chromeWorker_subworker.js @@ -0,0 +1,7 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function(event) { + postMessage("Done!"); +}; diff --git a/dom/workers/test/chromeWorker_worker.js b/dom/workers/test/chromeWorker_worker.js new file mode 100644 index 000000000..5738a9ae5 --- /dev/null +++ b/dom/workers/test/chromeWorker_worker.js @@ -0,0 +1,20 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +if (!("ctypes" in self)) { + throw "No ctypes!"; +} + +// Go ahead and verify that the ctypes lazy getter actually works. +if (ctypes.toString() != "[object ctypes]") { + throw "Bad ctypes object: " + ctypes.toString(); +} + +onmessage = function(event) { + let worker = new ChromeWorker("chromeWorker_subworker.js"); + worker.onmessage = function(event) { + postMessage(event.data); + } + worker.postMessage(event.data); +} diff --git a/dom/workers/test/clearTimeouts_worker.js b/dom/workers/test/clearTimeouts_worker.js new file mode 100644 index 000000000..b471515b3 --- /dev/null +++ b/dom/workers/test/clearTimeouts_worker.js @@ -0,0 +1,12 @@ +var count = 0; +function timerFunction() { + if (++count == 30) { + close(); + postMessage("ready"); + while (true) { } + } +} + +for (var i = 0; i < 10; i++) { + setInterval(timerFunction, 500); +} diff --git a/dom/workers/test/consoleReplaceable_worker.js b/dom/workers/test/consoleReplaceable_worker.js new file mode 100644 index 000000000..aaf104af1 --- /dev/null +++ b/dom/workers/test/consoleReplaceable_worker.js @@ -0,0 +1,16 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +onmessage = function(event) { + postMessage({event: 'console exists', status: !!console, last : false}); + var logCalled = false; + console.log = function() { + logCalled = true; + } + console.log("foo"); + postMessage({event: 'console.log is replaceable', status: logCalled, last: false}); + console = 42; + postMessage({event: 'console is replaceable', status: console === 42, last : true}); +} diff --git a/dom/workers/test/console_worker.js b/dom/workers/test/console_worker.js new file mode 100644 index 000000000..6b5f9d8a1 --- /dev/null +++ b/dom/workers/test/console_worker.js @@ -0,0 +1,109 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +onmessage = function(event) { + // TEST: does console exist? + postMessage({event: 'console exists', status: !!console, last : false}); + + postMessage({event: 'console is the same object', status: console === console, last: false}); + + postMessage({event: 'trace without function', status: true, last : false}); + + for (var i = 0; i < 10; ++i) { + console.log(i, i, i); + } + + function trace1() { + function trace2() { + function trace3() { + console.trace("trace " + i); + } + trace3(); + } + trace2(); + } + trace1(); + + foobar585956c = function(a) { + console.trace(); + return a+"c"; + }; + + function foobar585956b(a) { + return foobar585956c(a+"b"); + } + + function foobar585956a(omg) { + return foobar585956b(omg + "a"); + } + + function foobar646025(omg) { + console.log(omg, "o", "d"); + } + + function startTimer(timer) { + console.time(timer); + } + + function stopTimer(timer) { + console.timeEnd(timer); + } + + function timeStamp(label) { + console.timeStamp(label); + } + + function testGroups() { + console.groupCollapsed("a", "group"); + console.group("b", "group"); + console.groupEnd("b", "group"); + } + + foobar585956a('omg'); + foobar646025('omg'); + timeStamp(); + timeStamp('foo'); + testGroups(); + startTimer('foo'); + setTimeout(function() { + stopTimer('foo'); + nextSteps(event); + }, 10); +} + +function nextSteps(event) { + + function namelessTimer() { + console.time(); + console.timeEnd(); + } + + namelessTimer(); + + var str = "Test Message." + console.log(str); + console.info(str); + console.warn(str); + console.error(str); + console.exception(str); + console.assert(true, str); + console.assert(false, str); + console.profile(str); + console.profileEnd(str); + console.timeStamp(); + console.clear(); + postMessage({event: '4 messages', status: true, last : false}); + + // Recursive: + if (event.data == true) { + var worker = new Worker('console_worker.js'); + worker.onmessage = function(event) { + postMessage(event.data); + } + worker.postMessage(false); + } else { + postMessage({event: 'bye bye', status: true, last : true}); + } +} diff --git a/dom/workers/test/content_worker.js b/dom/workers/test/content_worker.js new file mode 100644 index 000000000..7e092ec8c --- /dev/null +++ b/dom/workers/test/content_worker.js @@ -0,0 +1,12 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var props = { + 'ctypes': 1, + 'OS': 1 +}; +for (var prop in props) { + postMessage({ "prop": prop, "value": self[prop] }); +} +postMessage({ "testfinished": 1 }); diff --git a/dom/workers/test/crashtests/1153636.html b/dom/workers/test/crashtests/1153636.html new file mode 100644 index 000000000..6ad0d550f --- /dev/null +++ b/dom/workers/test/crashtests/1153636.html @@ -0,0 +1,5 @@ +<script> + +new Worker("data:text/javascript;charset=UTF-8,self.addEventListener('',function(){},false);"); + +</script> diff --git a/dom/workers/test/crashtests/1158031.html b/dom/workers/test/crashtests/1158031.html new file mode 100644 index 000000000..6d896bc46 --- /dev/null +++ b/dom/workers/test/crashtests/1158031.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script> + +function boom() +{ + var w = new Worker("data:text/javascript;charset=UTF-8,"); + w.postMessage(new Blob([], {})); +} + +</script> +<body onload="boom();"></body> diff --git a/dom/workers/test/crashtests/1228456.html b/dom/workers/test/crashtests/1228456.html new file mode 100644 index 000000000..6d1f0f0a7 --- /dev/null +++ b/dom/workers/test/crashtests/1228456.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + +function boom() +{ + var w; + for (var i = 0; i < 99; ++i) { + w = new SharedWorker("data:text/javascript;charset=UTF-8," + encodeURIComponent(i + ";")); + } + w.port.postMessage(""); +} + +</script> +<body onload="boom();"></body> diff --git a/dom/workers/test/crashtests/779707.html b/dom/workers/test/crashtests/779707.html new file mode 100644 index 000000000..97a8113da --- /dev/null +++ b/dom/workers/test/crashtests/779707.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var x = new XMLHttpRequest(); + x.open('GET', "data:text/plain,2", false); + x.send(); + + new Worker("data:text/javascript,3"); +} + +</script> +</head> + +<body onload="boom();"></body> +</html> diff --git a/dom/workers/test/crashtests/943516.html b/dom/workers/test/crashtests/943516.html new file mode 100644 index 000000000..5f4667850 --- /dev/null +++ b/dom/workers/test/crashtests/943516.html @@ -0,0 +1,10 @@ +<!-- +Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE html> +<script> +// Using a DOM bindings object as a weak map key should not crash when attempting to +// call the preserve wrapper callback. +new Worker("data:text/javascript;charset=UTF-8,(new WeakMap()).set(self, 0);") +</script> diff --git a/dom/workers/test/crashtests/crashtests.list b/dom/workers/test/crashtests/crashtests.list new file mode 100644 index 000000000..a7518d3c2 --- /dev/null +++ b/dom/workers/test/crashtests/crashtests.list @@ -0,0 +1,5 @@ +load 779707.html +load 943516.html +load 1153636.html +load 1158031.html +load 1228456.html diff --git a/dom/workers/test/csp_worker.js b/dom/workers/test/csp_worker.js new file mode 100644 index 000000000..63b3adeaf --- /dev/null +++ b/dom/workers/test/csp_worker.js @@ -0,0 +1,28 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function(event) { + if (event.data.do == "eval") { + var res; + try { + res = eval("40+2"); + } + catch(ex) { + res = ex+""; + } + postMessage(res); + } + else if (event.data.do == "nest") { + var worker = new Worker(event.data.uri); + if (--event.data.level) { + worker.postMessage(event.data); + } + else { + worker.postMessage({ do: "eval" }); + } + worker.onmessage = (e) => { + postMessage(e.data); + } + } +} diff --git a/dom/workers/test/csp_worker.js^headers^ b/dom/workers/test/csp_worker.js^headers^ new file mode 100644 index 000000000..7b835bf2a --- /dev/null +++ b/dom/workers/test/csp_worker.js^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: default-src 'self' blob: ; script-src 'unsafe-eval' diff --git a/dom/workers/test/dom_worker_helper.js b/dom/workers/test/dom_worker_helper.js new file mode 100644 index 000000000..52e802ac7 --- /dev/null +++ b/dom/workers/test/dom_worker_helper.js @@ -0,0 +1,176 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/AddonManager.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +const wdm = Cc["@mozilla.org/dom/workers/workerdebuggermanager;1"]. + getService(Ci.nsIWorkerDebuggerManager); + +const BASE_URL = "chrome://mochitests/content/chrome/dom/workers/test/"; + +var gRemainingTests = 0; + +function waitForWorkerFinish() { + if (gRemainingTests == 0) { + SimpleTest.waitForExplicitFinish(); + } + ++gRemainingTests; +} + +function finish() { + --gRemainingTests; + if (gRemainingTests == 0) { + SimpleTest.finish(); + } +} + +function assertThrows(fun, message) { + let throws = false; + try { + fun(); + } catch (e) { + throws = true; + } + ok(throws, message); +} + +function* generateDebuggers() { + let e = wdm.getWorkerDebuggerEnumerator(); + while (e.hasMoreElements()) { + let dbg = e.getNext().QueryInterface(Ci.nsIWorkerDebugger); + yield dbg; + } +} + +function findDebugger(url) { + for (let dbg of generateDebuggers()) { + if (dbg.url === url) { + return dbg; + } + } + return null; +} + +function waitForRegister(url, dbgUrl) { + return new Promise(function (resolve) { + wdm.addListener({ + onRegister: function (dbg) { + dump("FAK " + dbg.url + "\n"); + if (dbg.url !== url) { + return; + } + ok(true, "Debugger with url " + url + " should be registered."); + wdm.removeListener(this); + if (dbgUrl) { + info("Initializing worker debugger with url " + url + "."); + dbg.initialize(dbgUrl); + } + resolve(dbg); + } + }); + }); +} + +function waitForUnregister(url) { + return new Promise(function (resolve) { + wdm.addListener({ + onUnregister: function (dbg) { + if (dbg.url !== url) { + return; + } + ok(true, "Debugger with url " + url + " should be unregistered."); + wdm.removeListener(this); + resolve(); + } + }); + }); +} + +function waitForDebuggerClose(dbg) { + return new Promise(function (resolve) { + dbg.addListener({ + onClose: function () { + ok(true, "Debugger should be closed."); + dbg.removeListener(this); + resolve(); + } + }); + }); +} + +function waitForDebuggerError(dbg) { + return new Promise(function (resolve) { + dbg.addListener({ + onError: function (filename, lineno, message) { + dbg.removeListener(this); + resolve(new Error(message, filename, lineno)); + } + }); + }); +} + +function waitForDebuggerMessage(dbg, message) { + return new Promise(function (resolve) { + dbg.addListener({ + onMessage: function (message1) { + if (message !== message1) { + return; + } + ok(true, "Should receive " + message + " message from debugger."); + dbg.removeListener(this); + resolve(); + } + }); + }); +} + +function waitForWindowMessage(window, message) { + return new Promise(function (resolve) { + let onmessage = function (event) { + if (event.data !== event.data) { + return; + } + window.removeEventListener("message", onmessage, false); + resolve(); + }; + window.addEventListener("message", onmessage, false); + }); +} + +function waitForWorkerMessage(worker, message) { + return new Promise(function (resolve) { + worker.addEventListener("message", function onmessage(event) { + if (event.data !== message) { + return; + } + ok(true, "Should receive " + message + " message from worker."); + worker.removeEventListener("message", onmessage); + resolve(); + }); + }); +} + +function waitForMultiple(promises) { + return new Promise(function (resolve) { + let values = []; + for (let i = 0; i < promises.length; ++i) { + let index = i; + promises[i].then(function (value) { + is(index + 1, values.length + 1, + "Promise " + (values.length + 1) + " out of " + promises.length + + " should be resolved."); + values.push(value); + if (values.length === promises.length) { + resolve(values); + } + }); + } + }); +}; diff --git a/dom/workers/test/empty.html b/dom/workers/test/empty.html new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/dom/workers/test/empty.html diff --git a/dom/workers/test/errorPropagation_iframe.html b/dom/workers/test/errorPropagation_iframe.html new file mode 100644 index 000000000..c5f688487 --- /dev/null +++ b/dom/workers/test/errorPropagation_iframe.html @@ -0,0 +1,55 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <meta charset="utf-8"> + <body> + <script type="text/javascript"> + var worker; + + function start(workerCount, messageCallback) { + var seenWindowError; + window.onerror = function(message, filename, lineno) { + if (!seenWindowError) { + seenWindowError = true; + messageCallback({ + type: "window", + data: { message: message, filename: filename, lineno: lineno } + }); + return true; + } + }; + + worker = new Worker("errorPropagation_worker.js"); + + worker.onmessage = function(event) { + messageCallback(event.data); + }; + + var seenWorkerError; + worker.onerror = function(event) { + if (!seenWorkerError) { + seenWorkerError = true; + messageCallback({ + type: "worker", + data: { + message: event.message, + filename: event.filename, + lineno: event.lineno + } + }); + event.preventDefault(); + } + }; + + worker.postMessage(workerCount); + } + + function stop() { + worker.terminate(); + } + </script> + </body> +</html> diff --git a/dom/workers/test/errorPropagation_worker.js b/dom/workers/test/errorPropagation_worker.js new file mode 100644 index 000000000..84f5916e0 --- /dev/null +++ b/dom/workers/test/errorPropagation_worker.js @@ -0,0 +1,50 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var seenScopeError; +onerror = function(message, filename, lineno) { + if (!seenScopeError) { + seenScopeError = true; + postMessage({ + type: "scope", + data: { message: message, filename: filename, lineno: lineno } + }); + return true; + } +}; + +onmessage = function(event) { + var workerId = parseInt(event.data); + + if (workerId > 1) { + var worker = new Worker("errorPropagation_worker.js"); + + worker.onmessage = function(event) { + postMessage(event.data); + }; + + var seenWorkerError; + worker.onerror = function(event) { + if (!seenWorkerError) { + seenWorkerError = true; + postMessage({ + type: "worker", + data: { + message: event.message, + filename: event.filename, + lineno: event.lineno + } + }); + event.preventDefault(); + } + }; + + worker.postMessage(workerId - 1); + return; + } + + var interval = setInterval(function() { + throw new Error("expectedError"); + }, 100); +}; diff --git a/dom/workers/test/errorwarning_worker.js b/dom/workers/test/errorwarning_worker.js new file mode 100644 index 000000000..1c372104c --- /dev/null +++ b/dom/workers/test/errorwarning_worker.js @@ -0,0 +1,42 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +function errorHandler() { + postMessage({ type: 'error' }); +} + +onmessage = function(event) { + if (event.data.errors) { + try { + // This is an error: + postMessage({ type: 'ignore', value: b.aaa }); + } catch(e) { + errorHandler(); + } + } else { + var a = {}; + // This is a warning: + postMessage({ type: 'ignore', value: a.foo }); + } + + if (event.data.loop != 0) { + var worker = new Worker('errorwarning_worker.js'); + worker.onerror = errorHandler; + worker.postMessage({ loop: event.data.loop - 1, errors: event.data.errors }); + + worker.onmessage = function(e) { + postMessage(e.data); + } + + } else { + postMessage({ type: 'finish' }); + } +} + +onerror = errorHandler; +onerror = onerror; +if (!onerror || onerror != onerror) { + throw "onerror wasn't set properly"; +} diff --git a/dom/workers/test/eventDispatch_worker.js b/dom/workers/test/eventDispatch_worker.js new file mode 100644 index 000000000..915f60c93 --- /dev/null +++ b/dom/workers/test/eventDispatch_worker.js @@ -0,0 +1,67 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +const fakeEventType = "foo"; + +function testEventTarget(event) { + if (event.target !== self) { + throw new Error("Event has a bad target!"); + } + if (event.currentTarget) { + throw new Error("Event has a bad currentTarget!"); + } + postMessage(event.data); +} + +addEventListener(fakeEventType, function(event) { + throw new Error("Trusted event listener received untrusted event!"); +}, false, false); + +addEventListener(fakeEventType, function(event) { + if (event.target !== self || event.currentTarget !== self) { + throw new Error("Fake event has bad target!"); + } + if (event.isTrusted) { + throw new Error("Event should be untrusted!"); + } + event.stopImmediatePropagation(); + postMessage(event.data); +}, false, true); + +addEventListener(fakeEventType, function(event) { + throw new Error("This shouldn't get called because of stopImmediatePropagation."); +}, false, true); + +var count = 0; +onmessage = function(event) { + if (event.target !== self || event.currentTarget !== self) { + throw new Error("Event has bad target!"); + } + + if (!count++) { + var exception; + try { + self.dispatchEvent(event); + } + catch(e) { + exception = e; + } + + if (!exception) { + throw new Error("Recursive dispatch didn't fail!"); + } + + event = new MessageEvent(fakeEventType, { bubbles: event.bubbles, + cancelable: event.cancelable, + data: event.data, + origin: "*", + source: null + }); + self.dispatchEvent(event); + + return; + } + + setTimeout(testEventTarget, 0, event); +}; diff --git a/dom/workers/test/extensions/bootstrap/bootstrap.js b/dom/workers/test/extensions/bootstrap/bootstrap.js new file mode 100644 index 000000000..acb0a204b --- /dev/null +++ b/dom/workers/test/extensions/bootstrap/bootstrap.js @@ -0,0 +1,141 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var Ci = Components.interfaces; +var Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +function testForExpectedSymbols(stage, data) { + const expectedSymbols = [ "Worker", "ChromeWorker" ]; + for (var symbol of expectedSymbols) { + Services.prefs.setBoolPref("workertest.bootstrap." + stage + "." + symbol, + symbol in this); + } +} + +var gWorkerAndCallback = { + _ensureStarted: function() { + if (!this._worker) { + throw new Error("Not yet started!"); + } + }, + + start: function(data) { + if (!this._worker) { + this._worker = new Worker("chrome://workerbootstrap/content/worker.js"); + this._worker.onerror = function(event) { + Cu.reportError(event.message); + event.preventDefault(); + }; + } + }, + + stop: function() { + if (this._worker) { + this._worker.terminate(); + delete this._worker; + } + }, + + set callback(val) { + this._ensureStarted(); + var callback = val.QueryInterface(Ci.nsIObserver); + if (this._callback != callback) { + if (callback) { + this._worker.onmessage = function(event) { + callback.observe(this, event.type, event.data); + }; + this._worker.onerror = function(event) { + callback.observe(this, event.type, event.message); + event.preventDefault(); + }; + } + else { + this._worker.onmessage = null; + this._worker.onerror = null; + } + this._callback = callback; + } + }, + + get callback() { + return this._callback; + }, + + postMessage: function(data) { + this._ensureStarted(); + this._worker.postMessage(data); + }, + + terminate: function() { + this._ensureStarted(); + this._worker.terminate(); + delete this._callback; + } +}; + +function WorkerTestBootstrap() { +} +WorkerTestBootstrap.prototype = { + observe: function(subject, topic, data) { + + gWorkerAndCallback.callback = subject; + + switch (topic) { + case "postMessage": + gWorkerAndCallback.postMessage(data); + break; + + case "terminate": + gWorkerAndCallback.terminate(); + break; + + default: + throw new Error("Unknown worker command"); + } + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) +}; + +var gFactory = { + register: function() { + var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar); + + var classID = Components.ID("{36b5df0b-8dcf-4aa2-9c45-c51d871295f9}"); + var description = "WorkerTestBootstrap"; + var contractID = "@mozilla.org/test/workertestbootstrap;1"; + var factory = XPCOMUtils._getFactory(WorkerTestBootstrap); + + registrar.registerFactory(classID, description, contractID, factory); + + this.unregister = function() { + registrar.unregisterFactory(classID, factory); + delete this.unregister; + }; + } +}; + +function install(data, reason) { + testForExpectedSymbols("install"); +} + +function startup(data, reason) { + testForExpectedSymbols("startup"); + gFactory.register(); + gWorkerAndCallback.start(data); +} + +function shutdown(data, reason) { + testForExpectedSymbols("shutdown"); + gWorkerAndCallback.stop(); + gFactory.unregister(); +} + +function uninstall(data, reason) { + testForExpectedSymbols("uninstall"); +} diff --git a/dom/workers/test/extensions/bootstrap/install.rdf b/dom/workers/test/extensions/bootstrap/install.rdf new file mode 100644 index 000000000..fdd9638cd --- /dev/null +++ b/dom/workers/test/extensions/bootstrap/install.rdf @@ -0,0 +1,31 @@ +<?xml version="1.0"?> + +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + + <Description about="urn:mozilla:install-manifest"> + <em:name>WorkerTestBootstrap</em:name> + <em:description>Worker functions for use in testing.</em:description> + <em:creator>Mozilla</em:creator> + <em:version>2016.03.09</em:version> + <em:id>workerbootstrap-test@mozilla.org</em:id> + <em:type>2</em:type> + <em:bootstrap>true</em:bootstrap> + <em:targetApplication> + <Description> + <!-- Firefox --> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>45.0</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + <em:targetApplication> + <Description> + <!-- Fennec --> + <em:id>{aa3c5121-dab2-40e2-81ca-7ea25febc110}</em:id> + <em:minVersion>45.0</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> diff --git a/dom/workers/test/extensions/bootstrap/jar.mn b/dom/workers/test/extensions/bootstrap/jar.mn new file mode 100644 index 000000000..a9c625103 --- /dev/null +++ b/dom/workers/test/extensions/bootstrap/jar.mn @@ -0,0 +1,3 @@ +workerbootstrap.jar: +% content workerbootstrap %content/ + content/worker.js (worker.js) diff --git a/dom/workers/test/extensions/bootstrap/moz.build b/dom/workers/test/extensions/bootstrap/moz.build new file mode 100644 index 000000000..aec5c249c --- /dev/null +++ b/dom/workers/test/extensions/bootstrap/moz.build @@ -0,0 +1,20 @@ +# -*- 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/. + +XPI_NAME = 'workerbootstrap' + +JAR_MANIFESTS += ['jar.mn'] +USE_EXTENSION_MANIFEST = True +NO_JS_MANIFEST = True + +FINAL_TARGET_FILES += [ + 'bootstrap.js', + 'install.rdf', +] + +TEST_HARNESS_FILES.testing.mochitest.extensions += [ + 'workerbootstrap-test@mozilla.org.xpi', +] diff --git a/dom/workers/test/extensions/bootstrap/worker.js b/dom/workers/test/extensions/bootstrap/worker.js new file mode 100644 index 000000000..7346fc142 --- /dev/null +++ b/dom/workers/test/extensions/bootstrap/worker.js @@ -0,0 +1,7 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function(event) { + postMessage(event.data); +} diff --git a/dom/workers/test/extensions/bootstrap/workerbootstrap-test@mozilla.org.xpi b/dom/workers/test/extensions/bootstrap/workerbootstrap-test@mozilla.org.xpi Binary files differnew file mode 100644 index 000000000..2dab975db --- /dev/null +++ b/dom/workers/test/extensions/bootstrap/workerbootstrap-test@mozilla.org.xpi diff --git a/dom/workers/test/extensions/moz.build b/dom/workers/test/extensions/moz.build new file mode 100644 index 000000000..51cf80fa2 --- /dev/null +++ b/dom/workers/test/extensions/moz.build @@ -0,0 +1,7 @@ +# -*- 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/. + +DIRS += ['bootstrap', 'traditional'] diff --git a/dom/workers/test/extensions/traditional/WorkerTest.js b/dom/workers/test/extensions/traditional/WorkerTest.js new file mode 100644 index 000000000..5890c0d4c --- /dev/null +++ b/dom/workers/test/extensions/traditional/WorkerTest.js @@ -0,0 +1,122 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +var gWorkerAndCallback = { + _worker: null, + _callback: null, + + _ensureStarted: function() { + if (!this._worker) { + throw new Error("Not yet started!"); + } + }, + + start: function() { + if (!this._worker) { + var worker = new Worker("chrome://worker/content/worker.js"); + worker.onerror = function(event) { + Cu.reportError(event.message); + event.preventDefault(); + }; + + this._worker = worker; + } + }, + + stop: function() { + if (this._worker) { + try { + this.terminate(); + } + catch(e) { + Cu.reportError(e); + } + this._worker = null; + } + }, + + set callback(val) { + this._ensureStarted(); + if (val) { + var callback = val.QueryInterface(Ci.nsIWorkerTestCallback); + if (this.callback != callback) { + this._worker.onmessage = function(event) { + callback.onmessage(event.data); + }; + this._worker.onerror = function(event) { + callback.onerror(event.message); + event.preventDefault(); + }; + this._callback = callback; + } + } + else { + this._worker.onmessage = null; + this._worker.onerror = null; + this._callback = null; + } + }, + + get callback() { + return this._callback; + }, + + postMessage: function(data) { + this._ensureStarted(); + this._worker.postMessage(data); + }, + + terminate: function() { + this._ensureStarted(); + this._worker.terminate(); + this.callback = null; + } +}; + +function WorkerTest() { +} +WorkerTest.prototype = { + observe: function(subject, topic, data) { + switch(topic) { + case "profile-after-change": + gWorkerAndCallback.start(); + Services.obs.addObserver(this, "profile-before-change", false); + break; + case "profile-before-change": + gWorkerAndCallback.stop(); + break; + default: + Cu.reportError("Unknown topic: " + topic); + } + }, + + set callback(val) { + gWorkerAndCallback.callback = val; + }, + + get callback() { + return gWorkerAndCallback.callback; + }, + + postMessage: function(message) { + gWorkerAndCallback.postMessage(message); + }, + + terminate: function() { + gWorkerAndCallback.terminate(); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsIWorkerTest]), + classID: Components.ID("{3b52b935-551f-4606-ba4c-decc18b67bfd}") +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WorkerTest]); diff --git a/dom/workers/test/extensions/traditional/WorkerTest.manifest b/dom/workers/test/extensions/traditional/WorkerTest.manifest new file mode 100644 index 000000000..a5a32fb06 --- /dev/null +++ b/dom/workers/test/extensions/traditional/WorkerTest.manifest @@ -0,0 +1,3 @@ +component {3b52b935-551f-4606-ba4c-decc18b67bfd} WorkerTest.js +contract @mozilla.org/test/workertest;1 {3b52b935-551f-4606-ba4c-decc18b67bfd} +category profile-after-change WorkerTest @mozilla.org/test/workertest;1 diff --git a/dom/workers/test/extensions/traditional/install.rdf b/dom/workers/test/extensions/traditional/install.rdf new file mode 100644 index 000000000..00fc0f441 --- /dev/null +++ b/dom/workers/test/extensions/traditional/install.rdf @@ -0,0 +1,30 @@ +<?xml version="1.0"?> + +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + + <Description about="urn:mozilla:install-manifest"> + <em:name>WorkerTest</em:name> + <em:description>Worker functions for use in testing.</em:description> + <em:creator>Mozilla</em:creator> + <em:version>2016.03.09</em:version> + <em:id>worker-test@mozilla.org</em:id> + <em:type>2</em:type> + <em:targetApplication> + <Description> + <!-- Firefox --> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>45.0</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + <em:targetApplication> + <Description> + <!-- Fennec --> + <em:id>{aa3c5121-dab2-40e2-81ca-7ea25febc110}</em:id> + <em:minVersion>45.0</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> diff --git a/dom/workers/test/extensions/traditional/jar.mn b/dom/workers/test/extensions/traditional/jar.mn new file mode 100644 index 000000000..421ee55a0 --- /dev/null +++ b/dom/workers/test/extensions/traditional/jar.mn @@ -0,0 +1,3 @@ +worker.jar: +% content worker %content/ + content/worker.js (worker.js) diff --git a/dom/workers/test/extensions/traditional/moz.build b/dom/workers/test/extensions/traditional/moz.build new file mode 100644 index 000000000..d0920420d --- /dev/null +++ b/dom/workers/test/extensions/traditional/moz.build @@ -0,0 +1,30 @@ +# -*- 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/. + +XPIDL_SOURCES += [ + 'nsIWorkerTest.idl', +] + +XPIDL_MODULE = 'WorkerTest' + +EXTRA_COMPONENTS += [ + 'WorkerTest.js', + 'WorkerTest.manifest', +] + +XPI_NAME = 'worker' + +JAR_MANIFESTS += ['jar.mn'] +USE_EXTENSION_MANIFEST = True +NO_JS_MANIFEST = True + +FINAL_TARGET_FILES += [ + 'install.rdf', +] + +TEST_HARNESS_FILES.testing.mochitest.extensions += [ + 'worker-test@mozilla.org.xpi', +] diff --git a/dom/workers/test/extensions/traditional/nsIWorkerTest.idl b/dom/workers/test/extensions/traditional/nsIWorkerTest.idl new file mode 100644 index 000000000..32a952038 --- /dev/null +++ b/dom/workers/test/extensions/traditional/nsIWorkerTest.idl @@ -0,0 +1,23 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=2 et sw=2 tw=40: */ +/* 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" + +[scriptable, uuid(10f8ebdf-1373-4640-9c34-53dee99f526f)] +interface nsIWorkerTestCallback : nsISupports +{ + void onmessage(in DOMString data); + void onerror(in DOMString data); +}; + +[scriptable, uuid(887a0614-a0f0-4c0e-80e0-cf31e6d4e286)] +interface nsIWorkerTest : nsISupports +{ + void postMessage(in DOMString data); + void terminate(); + + attribute nsIWorkerTestCallback callback; +}; diff --git a/dom/workers/test/extensions/traditional/worker-test@mozilla.org.xpi b/dom/workers/test/extensions/traditional/worker-test@mozilla.org.xpi Binary files differnew file mode 100644 index 000000000..8d2386894 --- /dev/null +++ b/dom/workers/test/extensions/traditional/worker-test@mozilla.org.xpi diff --git a/dom/workers/test/extensions/traditional/worker.js b/dom/workers/test/extensions/traditional/worker.js new file mode 100644 index 000000000..7346fc142 --- /dev/null +++ b/dom/workers/test/extensions/traditional/worker.js @@ -0,0 +1,7 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function(event) { + postMessage(event.data); +} diff --git a/dom/workers/test/fibonacci_worker.js b/dom/workers/test/fibonacci_worker.js new file mode 100644 index 000000000..fa35385e7 --- /dev/null +++ b/dom/workers/test/fibonacci_worker.js @@ -0,0 +1,24 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function(event) { + var n = parseInt(event.data); + + if (n < 2) { + postMessage(n); + return; + } + + var results = []; + for (var i = 1; i <= 2; i++) { + var worker = new Worker("fibonacci_worker.js"); + worker.onmessage = function(event) { + results.push(parseInt(event.data)); + if (results.length == 2) { + postMessage(results[0] + results[1]); + } + }; + worker.postMessage(n - i); + } +} diff --git a/dom/workers/test/fileBlobSubWorker_worker.js b/dom/workers/test/fileBlobSubWorker_worker.js new file mode 100644 index 000000000..2dc8cd12d --- /dev/null +++ b/dom/workers/test/fileBlobSubWorker_worker.js @@ -0,0 +1,17 @@ +/** + * Expects a blob. Returns an object containing the size, type. + * Used to test posting of blob from worker to worker. + */ +onmessage = function(event) { + var worker = new Worker("fileBlob_worker.js"); + + worker.postMessage(event.data); + + worker.onmessage = function(event) { + postMessage(event.data); + } + + worker.onerror = function(event) { + postMessage(undefined); + } +}; diff --git a/dom/workers/test/fileBlob_worker.js b/dom/workers/test/fileBlob_worker.js new file mode 100644 index 000000000..2f7a31714 --- /dev/null +++ b/dom/workers/test/fileBlob_worker.js @@ -0,0 +1,13 @@ +/** + * Expects a blob. Returns an object containing the size, type. + */ +onmessage = function(event) { + var file = event.data; + + var rtnObj = new Object(); + + rtnObj.size = file.size; + rtnObj.type = file.type; + + postMessage(rtnObj); +}; diff --git a/dom/workers/test/filePosting_worker.js b/dom/workers/test/filePosting_worker.js new file mode 100644 index 000000000..2a24b2a40 --- /dev/null +++ b/dom/workers/test/filePosting_worker.js @@ -0,0 +1,3 @@ +onmessage = function(event) { + postMessage(event.data); +}; diff --git a/dom/workers/test/fileReadSlice_worker.js b/dom/workers/test/fileReadSlice_worker.js new file mode 100644 index 000000000..2c10b7d76 --- /dev/null +++ b/dom/workers/test/fileReadSlice_worker.js @@ -0,0 +1,16 @@ +/** + * Expects an object containing a blob, a start index and an end index + * for slicing. Returns the contents of the blob read as text. + */ +onmessage = function(event) { + var blob = event.data.blob; + var start = event.data.start; + var end = event.data.end; + + var slicedBlob = blob.slice(start, end); + + var fileReader = new FileReaderSync(); + var text = fileReader.readAsText(slicedBlob); + + postMessage(text); +}; diff --git a/dom/workers/test/fileReaderSyncErrors_worker.js b/dom/workers/test/fileReaderSyncErrors_worker.js new file mode 100644 index 000000000..f79ecc6f0 --- /dev/null +++ b/dom/workers/test/fileReaderSyncErrors_worker.js @@ -0,0 +1,74 @@ +/** + * Delegates "is" evaluation back to main thread. + */ +function is(actual, expected, message) { + var rtnObj = new Object(); + rtnObj.actual = actual; + rtnObj.expected = expected; + rtnObj.message = message; + postMessage(rtnObj); +} + +/** + * Tries to write to property. + */ +function writeProperty(file, property) { + var oldValue = file[property]; + file[property] = -1; + is(file[property], oldValue, "Property " + property + " should be readonly."); +} + +/** + * Passes junk arguments to FileReaderSync methods and expects an exception to + * be thrown. + */ +function fileReaderJunkArgument(blob) { + var fileReader = new FileReaderSync(); + + try { + fileReader.readAsBinaryString(blob); + is(false, true, "Should have thrown an exception calling readAsBinaryString."); + } catch(ex) { + is(true, true, "Should have thrown an exception."); + } + + try { + fileReader.readAsDataURL(blob); + is(false, true, "Should have thrown an exception calling readAsDataURL."); + } catch(ex) { + is(true, true, "Should have thrown an exception."); + } + + try { + fileReader.readAsArrayBuffer(blob); + is(false, true, "Should have thrown an exception calling readAsArrayBuffer."); + } catch(ex) { + is(true, true, "Should have thrown an exception."); + } + + try { + fileReader.readAsText(blob); + is(false, true, "Should have thrown an exception calling readAsText."); + } catch(ex) { + is(true, true, "Should have thrown an exception."); + } +} + +onmessage = function(event) { + var file = event.data; + + // Test read only properties. + writeProperty(file, "size"); + writeProperty(file, "type"); + writeProperty(file, "name"); + + // Bad types. + fileReaderJunkArgument(undefined); + fileReaderJunkArgument(-1); + fileReaderJunkArgument(1); + fileReaderJunkArgument(new Object()); + fileReaderJunkArgument("hello"); + + // Post undefined to indicate that testing has finished. + postMessage(undefined); +}; diff --git a/dom/workers/test/fileReaderSync_worker.js b/dom/workers/test/fileReaderSync_worker.js new file mode 100644 index 000000000..4a37409d5 --- /dev/null +++ b/dom/workers/test/fileReaderSync_worker.js @@ -0,0 +1,25 @@ +var reader = new FileReaderSync(); + +/** + * Expects an object containing a file and an encoding then uses a + * FileReaderSync to read the file. Returns an object containing the + * file read a binary string, text, url and ArrayBuffer. + */ +onmessage = function(event) { + var file = event.data.file; + var encoding = event.data.encoding; + + var rtnObj = new Object(); + + if (encoding != undefined) { + rtnObj.text = reader.readAsText(file, encoding); + } else { + rtnObj.text = reader.readAsText(file); + } + + rtnObj.bin = reader.readAsBinaryString(file); + rtnObj.url = reader.readAsDataURL(file); + rtnObj.arrayBuffer = reader.readAsArrayBuffer(file); + + postMessage(rtnObj); +}; diff --git a/dom/workers/test/fileSlice_worker.js b/dom/workers/test/fileSlice_worker.js new file mode 100644 index 000000000..d0c6364e2 --- /dev/null +++ b/dom/workers/test/fileSlice_worker.js @@ -0,0 +1,27 @@ +/** + * Expects an object containing a blob, a start offset, an end offset + * and an optional content type to slice the blob. Returns an object + * containing the size and type of the sliced blob. + */ +onmessage = function(event) { + var blob = event.data.blob; + var start = event.data.start; + var end = event.data.end; + var contentType = event.data.contentType; + + var slicedBlob; + if (contentType == undefined && end == undefined) { + slicedBlob = blob.slice(start); + } else if (contentType == undefined) { + slicedBlob = blob.slice(start, end); + } else { + slicedBlob = blob.slice(start, end, contentType); + } + + var rtnObj = new Object(); + + rtnObj.size = slicedBlob.size; + rtnObj.type = slicedBlob.type; + + postMessage(rtnObj); +}; diff --git a/dom/workers/test/fileSubWorker_worker.js b/dom/workers/test/fileSubWorker_worker.js new file mode 100644 index 000000000..21fbc3454 --- /dev/null +++ b/dom/workers/test/fileSubWorker_worker.js @@ -0,0 +1,17 @@ +/** + * Expects a file. Returns an object containing the size, type, name and path + * using another worker. Used to test posting of file from worker to worker. + */ +onmessage = function(event) { + var worker = new Worker("file_worker.js"); + + worker.postMessage(event.data); + + worker.onmessage = function(event) { + postMessage(event.data); + } + + worker.onerror = function(event) { + postMessage(undefined); + } +}; diff --git a/dom/workers/test/file_bug1010784_worker.js b/dom/workers/test/file_bug1010784_worker.js new file mode 100644 index 000000000..239968069 --- /dev/null +++ b/dom/workers/test/file_bug1010784_worker.js @@ -0,0 +1,9 @@ +onmessage = function(event) { + var xhr = new XMLHttpRequest(); + + xhr.open("GET", event.data, false); + xhr.send(); + xhr.open("GET", event.data, false); + xhr.send(); + postMessage("done"); +} diff --git a/dom/workers/test/file_worker.js b/dom/workers/test/file_worker.js new file mode 100644 index 000000000..23233b8ac --- /dev/null +++ b/dom/workers/test/file_worker.js @@ -0,0 +1,16 @@ +/** + * Expects a file. Returns an object containing the size, type, name and path. + */ +onmessage = function(event) { + var file = event.data; + + var rtnObj = new Object(); + + rtnObj.size = file.size; + rtnObj.type = file.type; + rtnObj.name = file.name; + rtnObj.path = file.path; + rtnObj.lastModifiedDate = file.lastModifiedDate; + + postMessage(rtnObj); +}; diff --git a/dom/workers/test/fileapi_chromeScript.js b/dom/workers/test/fileapi_chromeScript.js new file mode 100644 index 000000000..614b556ed --- /dev/null +++ b/dom/workers/test/fileapi_chromeScript.js @@ -0,0 +1,29 @@ +var { classes: Cc, interfaces: Ci, utils: Cu } = Components; +Cu.importGlobalProperties(["File"]); + +var fileNum = 1; + +function createFileWithData(fileData) { + var willDelete = fileData === null; + var dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties); + var testFile = dirSvc.get("ProfD", Ci.nsIFile); + testFile.append("fileAPItestfile" + fileNum); + fileNum++; + var outStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, 0); + if (willDelete) { + fileData = "some irrelevant test data\n"; + } + outStream.write(fileData, fileData.length); + outStream.close(); + var domFile = File.createFromNsIFile(testFile); + if (willDelete) { + testFile.remove(/* recursive: */ false); + } + return domFile; +} + +addMessageListener("files.open", function (message) { + sendAsyncMessage("files.opened", message.map(createFileWithData)); +}); diff --git a/dom/workers/test/foreign.js b/dom/workers/test/foreign.js new file mode 100644 index 000000000..33c982fa8 --- /dev/null +++ b/dom/workers/test/foreign.js @@ -0,0 +1 @@ +response = "bad"; diff --git a/dom/workers/test/frame_script.js b/dom/workers/test/frame_script.js new file mode 100644 index 000000000..ffc384416 --- /dev/null +++ b/dom/workers/test/frame_script.js @@ -0,0 +1,72 @@ +/* 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/. */ +"use strict"; + +const { interfaces: Ci } = Components; + +let workers = {}; + +let methods = { + /** + * Create a worker with the given `url` in this tab. + */ + createWorker: function (url) { + dump("Frame script: creating worker with url '" + url + "'\n"); + + workers[url] = new content.Worker(url); + return Promise.resolve(); + }, + + /** + * Terminate the worker with the given `url` in this tab. + */ + terminateWorker: function (url) { + dump("Frame script: terminating worker with url '" + url + "'\n"); + + workers[url].terminate(); + delete workers[url]; + return Promise.resolve(); + }, + + /** + * Post the given `message` to the worker with the given `url` in this tab. + */ + postMessageToWorker: function (url, message) { + dump("Frame script: posting message to worker with url '" + url + "'\n"); + + let worker = workers[url]; + worker.postMessage(message); + return new Promise(function (resolve) { + worker.onmessage = function (event) { + worker.onmessage = null; + resolve(event.data); + }; + }); + }, + + /** + * Disable the cache for this tab. + */ + disableCache: function () { + docShell.defaultLoadFlags = Ci.nsIRequest.LOAD_BYPASS_CACHE + | Ci.nsIRequest.INHIBIT_CACHING; + } +}; + +addMessageListener("jsonrpc", function (event) { + let { id, method, params } = event.data; + Promise.resolve().then(function () { + return methods[method].apply(undefined, params); + }).then(function (result) { + sendAsyncMessage("jsonrpc", { + id: id, + result: result + }); + }).catch(function (error) { + sendAsyncMessage("jsonrpc", { + id: id, + error: error.toString() + }); + }); +}); diff --git a/dom/workers/test/gtest/TestReadWrite.cpp b/dom/workers/test/gtest/TestReadWrite.cpp new file mode 100644 index 000000000..d59888e24 --- /dev/null +++ b/dom/workers/test/gtest/TestReadWrite.cpp @@ -0,0 +1,499 @@ +/* -*- 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 "gtest/gtest.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/ServiceWorkerRegistrar.h" +#include "mozilla/dom/ServiceWorkerRegistrarTypes.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" + +#include "nsAppDirectoryServiceDefs.h" +#include "nsIFile.h" +#include "nsIOutputStream.h" +#include "nsNetUtil.h" +#include "nsPrintfCString.h" + +using namespace mozilla::dom; +using namespace mozilla::ipc; + +class ServiceWorkerRegistrarTest : public ServiceWorkerRegistrar +{ +public: + ServiceWorkerRegistrarTest() + { +#if defined(DEBUG) || !defined(RELEASE_OR_BETA) + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mProfileDir)); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); +#else + NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mProfileDir)); +#endif + MOZ_DIAGNOSTIC_ASSERT(mProfileDir); + } + + nsresult TestReadData() { return ReadData(); } + nsresult TestWriteData() { return WriteData(); } + void TestDeleteData() { DeleteData(); } + + void TestRegisterServiceWorker(const ServiceWorkerRegistrationData& aData) + { + RegisterServiceWorkerInternal(aData); + } + + nsTArray<ServiceWorkerRegistrationData>& TestGetData() { return mData; } +}; + +already_AddRefed<nsIFile> +GetFile() +{ + nsCOMPtr<nsIFile> file; + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + file->Append(NS_LITERAL_STRING(SERVICEWORKERREGISTRAR_FILE)); + return file.forget(); +} + +bool +CreateFile(const nsACString& aData) +{ + nsCOMPtr<nsIFile> file = GetFile(); + + nsCOMPtr<nsIOutputStream> stream; + nsresult rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), file); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + uint32_t count; + rv = stream->Write(aData.Data(), aData.Length(), &count); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + if (count != aData.Length()) { + return false; + } + + return true; +} + +TEST(ServiceWorkerRegistrar, TestNoFile) +{ + nsCOMPtr<nsIFile> file = GetFile(); + ASSERT_TRUE(file) << "GetFile must return a nsIFIle"; + + bool exists; + nsresult rv = file->Exists(&exists); + ASSERT_EQ(NS_OK, rv) << "nsIFile::Exists cannot fail"; + + if (exists) { + rv = file->Remove(false); + ASSERT_EQ(NS_OK, rv) << "nsIFile::Remove cannot fail"; + } + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)0, data.Length()) << "No data should be found in an empty file"; +} + +TEST(ServiceWorkerRegistrar, TestEmptyFile) +{ + ASSERT_TRUE(CreateFile(EmptyCString())) << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_NE(NS_OK, rv) << "ReadData() should fail if the file is empty"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)0, data.Length()) << "No data should be found in an empty file"; +} + +TEST(ServiceWorkerRegistrar, TestRightVersionFile) +{ + ASSERT_TRUE(CreateFile(NS_LITERAL_CSTRING(SERVICEWORKERREGISTRAR_VERSION "\n"))) << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail when the version is correct"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)0, data.Length()) << "No data should be found in an empty file"; +} + +TEST(ServiceWorkerRegistrar, TestWrongVersionFile) +{ + ASSERT_TRUE(CreateFile(NS_LITERAL_CSTRING(SERVICEWORKERREGISTRAR_VERSION "bla\n"))) << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_NE(NS_OK, rv) << "ReadData() should fail when the version is not correct"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)0, data.Length()) << "No data should be found in an empty file"; +} + +TEST(ServiceWorkerRegistrar, TestReadData) +{ + nsAutoCString buffer(SERVICEWORKERREGISTRAR_VERSION "\n"); + + buffer.Append("^appId=123&inBrowser=1\n"); + buffer.Append("scope 0\ncurrentWorkerURL 0\ncacheName 0\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.Append("\n"); + buffer.Append("scope 1\ncurrentWorkerURL 1\ncacheName 1\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^appId=123&inBrowser=1", suffix0.get()); + ASSERT_STREQ("scope 0", cInfo0.spec().get()); + ASSERT_STREQ("scope 0", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("scope 1", cInfo1.spec().get()); + ASSERT_STREQ("scope 1", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); +} + +TEST(ServiceWorkerRegistrar, TestDeleteData) +{ + ASSERT_TRUE(CreateFile(NS_LITERAL_CSTRING("Foobar"))) << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + swr->TestDeleteData(); + + nsCOMPtr<nsIFile> file = GetFile(); + + bool exists; + nsresult rv = file->Exists(&exists); + ASSERT_EQ(NS_OK, rv) << "nsIFile::Exists cannot fail"; + + ASSERT_FALSE(exists) << "The file should not exist after a DeleteData()."; +} + +TEST(ServiceWorkerRegistrar, TestWriteData) +{ + { + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + for (int i = 0; i < 10; ++i) { + ServiceWorkerRegistrationData reg; + + reg.scope() = nsPrintfCString("scope write %d", i); + reg.currentWorkerURL() = nsPrintfCString("currentWorkerURL write %d", i); + reg.cacheName() = + NS_ConvertUTF8toUTF16(nsPrintfCString("cacheName write %d", i)); + + nsAutoCString spec; + spec.AppendPrintf("spec write %d", i); + reg.principal() = + mozilla::ipc::ContentPrincipalInfo(mozilla::PrincipalOriginAttributes(i, i % 2), + mozilla::void_t(), spec); + + swr->TestRegisterServiceWorker(reg); + } + + nsresult rv = swr->TestWriteData(); + ASSERT_EQ(NS_OK, rv) << "WriteData() should not fail"; + } + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)10, data.Length()) << "10 entries should be found"; + + for (int i = 0; i < 10; ++i) { + nsAutoCString test; + + ASSERT_EQ(data[i].principal().type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo); + const mozilla::ipc::ContentPrincipalInfo& cInfo = data[i].principal(); + + mozilla::PrincipalOriginAttributes attrs(i, i % 2); + nsAutoCString suffix, expectSuffix; + attrs.CreateSuffix(expectSuffix); + cInfo.attrs().CreateSuffix(suffix); + + ASSERT_STREQ(expectSuffix.get(), suffix.get()); + + test.AppendPrintf("scope write %d", i); + ASSERT_STREQ(test.get(), cInfo.spec().get()); + + test.Truncate(); + test.AppendPrintf("scope write %d", i); + ASSERT_STREQ(test.get(), data[i].scope().get()); + + test.Truncate(); + test.AppendPrintf("currentWorkerURL write %d", i); + ASSERT_STREQ(test.get(), data[i].currentWorkerURL().get()); + + test.Truncate(); + test.AppendPrintf("cacheName write %d", i); + ASSERT_STREQ(test.get(), NS_ConvertUTF16toUTF8(data[i].cacheName()).get()); + } +} + +TEST(ServiceWorkerRegistrar, TestVersion2Migration) +{ + nsAutoCString buffer("2" "\n"); + + buffer.Append("^appId=123&inBrowser=1\n"); + buffer.Append("spec 0\nscope 0\nscriptSpec 0\ncurrentWorkerURL 0\nactiveCache 0\nwaitingCache 0\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.Append("\n"); + buffer.Append("spec 1\nscope 1\nscriptSpec 1\ncurrentWorkerURL 1\nactiveCache 1\nwaitingCache 1\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^appId=123&inBrowser=1", suffix0.get()); + ASSERT_STREQ("scope 0", cInfo0.spec().get()); + ASSERT_STREQ("scope 0", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_STREQ("activeCache 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("scope 1", cInfo1.spec().get()); + ASSERT_STREQ("scope 1", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_STREQ("activeCache 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); +} + +TEST(ServiceWorkerRegistrar, TestVersion3Migration) +{ + nsAutoCString buffer("3" "\n"); + + buffer.Append("^appId=123&inBrowser=1\n"); + buffer.Append("spec 0\nscope 0\ncurrentWorkerURL 0\ncacheName 0\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.Append("\n"); + buffer.Append("spec 1\nscope 1\ncurrentWorkerURL 1\ncacheName 1\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^appId=123&inBrowser=1", suffix0.get()); + ASSERT_STREQ("scope 0", cInfo0.spec().get()); + ASSERT_STREQ("scope 0", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("scope 1", cInfo1.spec().get()); + ASSERT_STREQ("scope 1", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); +} + +TEST(ServiceWorkerRegistrar, TestDedupeRead) +{ + nsAutoCString buffer("3" "\n"); + + // unique entries + buffer.Append("^appId=123&inBrowser=1\n"); + buffer.Append("spec 0\nscope 0\ncurrentWorkerURL 0\ncacheName 0\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.Append("\n"); + buffer.Append("spec 1\nscope 1\ncurrentWorkerURL 1\ncacheName 1\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + // dupe entries + buffer.Append("^appId=123&inBrowser=1\n"); + buffer.Append("spec 1\nscope 0\ncurrentWorkerURL 0\ncacheName 0\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.Append("^appId=123&inBrowser=1\n"); + buffer.Append("spec 2\nscope 0\ncurrentWorkerURL 0\ncacheName 0\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.Append("\n"); + buffer.Append("spec 3\nscope 1\ncurrentWorkerURL 1\ncacheName 1\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^appId=123&inBrowser=1", suffix0.get()); + ASSERT_STREQ("scope 0", cInfo0.spec().get()); + ASSERT_STREQ("scope 0", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("scope 1", cInfo1.spec().get()); + ASSERT_STREQ("scope 1", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); +} + +TEST(ServiceWorkerRegistrar, TestDedupeWrite) +{ + { + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + for (int i = 0; i < 10; ++i) { + ServiceWorkerRegistrationData reg; + + reg.scope() = NS_LITERAL_CSTRING("scope write dedupe"); + reg.currentWorkerURL() = nsPrintfCString("currentWorkerURL write %d", i); + reg.cacheName() = + NS_ConvertUTF8toUTF16(nsPrintfCString("cacheName write %d", i)); + + nsAutoCString spec; + spec.AppendPrintf("spec write dedupe/%d", i); + reg.principal() = + mozilla::ipc::ContentPrincipalInfo(mozilla::PrincipalOriginAttributes(0, false), + mozilla::void_t(), spec); + + swr->TestRegisterServiceWorker(reg); + } + + nsresult rv = swr->TestWriteData(); + ASSERT_EQ(NS_OK, rv) << "WriteData() should not fail"; + } + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + // Duplicate entries should be removed. + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)1, data.Length()) << "1 entry should be found"; + + ASSERT_EQ(data[0].principal().type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo); + const mozilla::ipc::ContentPrincipalInfo& cInfo = data[0].principal(); + + mozilla::PrincipalOriginAttributes attrs(0, false); + nsAutoCString suffix, expectSuffix; + attrs.CreateSuffix(expectSuffix); + cInfo.attrs().CreateSuffix(suffix); + + // Last entry passed to RegisterServiceWorkerInternal() should overwrite + // previous values. So expect "9" in values here. + ASSERT_STREQ(expectSuffix.get(), suffix.get()); + ASSERT_STREQ("scope write dedupe", cInfo.spec().get()); + ASSERT_STREQ("scope write dedupe", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL write 9", data[0].currentWorkerURL().get()); + ASSERT_STREQ("cacheName write 9", + NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + + int rv = RUN_ALL_TESTS(); + return rv; +} diff --git a/dom/workers/test/gtest/moz.build b/dom/workers/test/gtest/moz.build new file mode 100644 index 000000000..5f1f185a9 --- /dev/null +++ b/dom/workers/test/gtest/moz.build @@ -0,0 +1,13 @@ +# -*- 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/. + +UNIFIED_SOURCES = [ + 'TestReadWrite.cpp', +] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul-gtest' diff --git a/dom/workers/test/head.js b/dom/workers/test/head.js new file mode 100644 index 000000000..5f0c5c26e --- /dev/null +++ b/dom/workers/test/head.js @@ -0,0 +1,91 @@ +/* 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/. */ +"use strict"; + +const EXAMPLE_URL = "http://example.com/browser/dom/workers/test/"; +const FRAME_SCRIPT_URL = getRootDirectory(gTestPath) + "frame_script.js"; + +/** + * Add a tab with given `url`, and load a frame script in it. Returns a promise + * that will be resolved when the tab finished loading. + */ +function addTab(url) { + let tab = gBrowser.addTab(TAB_URL); + gBrowser.selectedTab = tab; + let linkedBrowser = tab.linkedBrowser; + linkedBrowser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false); + return new Promise(function (resolve) { + linkedBrowser.addEventListener("load", function onload() { + linkedBrowser.removeEventListener("load", onload, true); + resolve(tab); + }, true); + }); +} + +/** + * Remove the given `tab`. + */ +function removeTab(tab) { + gBrowser.removeTab(tab); +} + +let nextId = 0; + +/** + * Send a JSON RPC request to the frame script in the given `tab`, invoking the + * given `method` with the given `params`. Returns a promise that will be + * resolved with the result of the invocation. + */ +function jsonrpc(tab, method, params) { + let currentId = nextId++; + let messageManager = tab.linkedBrowser.messageManager; + messageManager.sendAsyncMessage("jsonrpc", { + id: currentId, + method: method, + params: params + }); + return new Promise(function (resolve, reject) { + messageManager.addMessageListener("jsonrpc", function listener(event) { + let { id, result, error } = event.data; + if (id !== currentId) { + return; + } + messageManager.removeMessageListener("jsonrpc", listener); + if (error) { + reject(error); + return; + } + resolve(result); + }); + }); +} + +/** + * Create a worker with the given `url` in the given `tab`. + */ +function createWorkerInTab(tab, url) { + return jsonrpc(tab, "createWorker", [url]); +} + +/** + * Terminate the worker with the given `url` in the given `tab`. + */ +function terminateWorkerInTab(tab, url) { + return jsonrpc(tab, "terminateWorker", [url]); +} + +/** + * Post the given `message` to the worker with the given `url` in the given + * `tab`. + */ +function postMessageToWorkerInTab(tab, url, message) { + return jsonrpc(tab, "postMessageToWorker", [url, message]); +} + +/** + * Disable the cache in the given `tab`. + */ +function disableCacheInTab(tab) { + return jsonrpc(tab, "disableCache", []); +} diff --git a/dom/workers/test/importForeignScripts_worker.js b/dom/workers/test/importForeignScripts_worker.js new file mode 100644 index 000000000..5faa29c31 --- /dev/null +++ b/dom/workers/test/importForeignScripts_worker.js @@ -0,0 +1,55 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +var target = self; +var response; + +function runTests() { + response = "good"; + try { + importScripts("http://example.org/tests/dom/workers/test/foreign.js"); + } catch(e) { + dump("Got error " + e + " when calling importScripts"); + } + if (response === "good") { + try { + importScripts("redirect_to_foreign.sjs"); + } catch(e) { + dump("Got error " + e + " when calling importScripts"); + } + } + target.postMessage(response); + + // Now, test a nested worker. + if (location.search !== "?nested") { + var worker = new Worker("importForeignScripts_worker.js?nested"); + + worker.onmessage = function(e) { + target.postMessage(e.data); + target.postMessage("finish"); + } + + worker.onerror = function() { + target.postMessage("nested worker error"); + } + + worker.postMessage("start"); + } +} + +onmessage = function(e) { + if (e.data === "start") { + runTests(); + } +}; + +onconnect = function(e) { + target = e.ports[0]; + e.ports[0].onmessage = function(e) { + if (e.data === "start") { + runTests(); + } + }; +}; diff --git a/dom/workers/test/importScripts_3rdParty_worker.js b/dom/workers/test/importScripts_3rdParty_worker.js new file mode 100644 index 000000000..ebf2d3b14 --- /dev/null +++ b/dom/workers/test/importScripts_3rdParty_worker.js @@ -0,0 +1,82 @@ +const workerURL = 'http://mochi.test:8888/tests/dom/workers/test/importScripts_3rdParty_worker.js'; + +onmessage = function(a) { + if (a.data.nested) { + var worker = new Worker(workerURL); + worker.onmessage = function(event) { + postMessage(event.data); + } + + worker.onerror = function(event) { + event.preventDefault(); + postMessage({ error: event instanceof ErrorEvent && + event.filename == workerURL }); + } + + a.data.nested = false; + worker.postMessage(a.data); + return; + } + + // This first URL will use the same origin of this script. + var sameOriginURL = new URL(a.data.url); + var fileName1 = 42; + + // This is cross-origin URL. + var crossOriginURL = new URL(a.data.url); + crossOriginURL.host = 'example.com'; + crossOriginURL.port = 80; + var fileName2 = 42; + + if (a.data.test == 'none') { + importScripts(crossOriginURL.href); + return; + } + + try { + importScripts(sameOriginURL.href); + } catch(e) { + if (!(e instanceof SyntaxError)) { + postMessage({ result: false }); + return; + } + + fileName1 = e.fileName; + } + + if (fileName1 != sameOriginURL.href || !fileName1) { + postMessage({ result: false }); + return; + } + + if (a.data.test == 'try') { + var exception; + try { + importScripts(crossOriginURL.href); + } catch(e) { + fileName2 = e.filename; + exception = e; + } + + postMessage({ result: fileName2 == workerURL && + exception.name == "NetworkError" && + exception.code == DOMException.NETWORK_ERR }); + return; + } + + if (a.data.test == 'eventListener') { + addEventListener("error", function(event) { + event.preventDefault(); + postMessage({result: event instanceof ErrorEvent && + event.filename == workerURL }); + }); + } + + if (a.data.test == 'onerror') { + onerror = function(...args) { + postMessage({result: args[1] == workerURL }); + } + } + + importScripts(crossOriginURL.href); +} diff --git a/dom/workers/test/importScripts_mixedcontent.html b/dom/workers/test/importScripts_mixedcontent.html new file mode 100644 index 000000000..82933b091 --- /dev/null +++ b/dom/workers/test/importScripts_mixedcontent.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE html> +<script> + function ok(cond, msg) { + window.parent.postMessage({status: "ok", data: cond, msg: msg}, "*"); + } + function finish() { + window.parent.postMessage({status: "done"}, "*"); + } + + function testSharedWorker() { + var sw = new SharedWorker("importForeignScripts_worker.js"); + sw.port.onmessage = function(e) { + if (e.data == "finish") { + finish(); + return; + } + ok(e.data === "good", "mixed content for shared workers is correctly blocked"); + }; + + sw.onerror = function() { + ok(false, "Error on shared worker "); + }; + + sw.port.postMessage("start"); + } + + var worker = new Worker("importForeignScripts_worker.js"); + + worker.onmessage = function(e) { + if (e.data == "finish") { + testSharedWorker(); + return; + } + ok(e.data === "good", "mixed content is correctly blocked"); + } + + worker.onerror = function() { + ok(false, "Error on worker"); + } + + worker.postMessage("start"); +</script> diff --git a/dom/workers/test/importScripts_worker.js b/dom/workers/test/importScripts_worker.js new file mode 100644 index 000000000..7176ce838 --- /dev/null +++ b/dom/workers/test/importScripts_worker.js @@ -0,0 +1,64 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// Try no args. This shouldn't do anything. +importScripts(); + +// This caused security exceptions in the past, make sure it doesn't! +var constructor = {}.constructor; + +importScripts("importScripts_worker_imported1.js"); + +// Try to call a function defined in the imported script. +importedScriptFunction(); + +function tryBadScripts() { + var badScripts = [ + // Has a syntax error + "importScripts_worker_imported3.js", + // Throws an exception + "importScripts_worker_imported4.js", + // Shouldn't exist! + "http://example.com/non-existing/importScripts_worker_foo.js", + // Not a valid url + "http://notadomain::notafile aword" + ]; + + for (var i = 0; i < badScripts.length; i++) { + var caughtException = false; + var url = badScripts[i]; + try { + importScripts(url); + } + catch (e) { + caughtException = true; + } + if (!caughtException) { + throw "Bad script didn't throw exception: " + url; + } + } +} + +const url = "data:text/plain,const startResponse = 'started';"; +importScripts(url); + +onmessage = function(event) { + switch (event.data) { + case 'start': + importScripts("importScripts_worker_imported2.js"); + importedScriptFunction2(); + tryBadScripts(); + postMessage(startResponse); + break; + case 'stop': + tryBadScripts(); + postMessage('stopped'); + break; + default: + throw new Error("Bad message: " + event.data); + break; + } +} + +tryBadScripts(); diff --git a/dom/workers/test/importScripts_worker_imported1.js b/dom/workers/test/importScripts_worker_imported1.js new file mode 100644 index 000000000..9c33588c4 --- /dev/null +++ b/dom/workers/test/importScripts_worker_imported1.js @@ -0,0 +1,10 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// This caused security exceptions in the past, make sure it doesn't! +var myConstructor = {}.constructor; + +// Try to call a function defined in the imported script. +function importedScriptFunction() { +} diff --git a/dom/workers/test/importScripts_worker_imported2.js b/dom/workers/test/importScripts_worker_imported2.js new file mode 100644 index 000000000..3aafb60be --- /dev/null +++ b/dom/workers/test/importScripts_worker_imported2.js @@ -0,0 +1,10 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// This caused security exceptions in the past, make sure it doesn't! +var myConstructor2 = {}.constructor; + +// Try to call a function defined in the imported script. +function importedScriptFunction2() { +} diff --git a/dom/workers/test/importScripts_worker_imported3.js b/dom/workers/test/importScripts_worker_imported3.js new file mode 100644 index 000000000..c54be3e5f --- /dev/null +++ b/dom/workers/test/importScripts_worker_imported3.js @@ -0,0 +1,6 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// Deliberate syntax error, should generate a worker exception! +for (var index = 0; index < 100) {} diff --git a/dom/workers/test/importScripts_worker_imported4.js b/dom/workers/test/importScripts_worker_imported4.js new file mode 100644 index 000000000..82f8708c5 --- /dev/null +++ b/dom/workers/test/importScripts_worker_imported4.js @@ -0,0 +1,6 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// Deliberate throw, should generate a worker exception! +throw new Error("Bah!"); diff --git a/dom/workers/test/instanceof_worker.js b/dom/workers/test/instanceof_worker.js new file mode 100644 index 000000000..a98255388 --- /dev/null +++ b/dom/workers/test/instanceof_worker.js @@ -0,0 +1,12 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function(event) { + postMessage({event: "XMLHttpRequest", + status: (new XMLHttpRequest() instanceof XMLHttpRequest), + last: false }); + postMessage({event: "XMLHttpRequestUpload", + status: ((new XMLHttpRequest()).upload instanceof XMLHttpRequestUpload), + last: true }); +} diff --git a/dom/workers/test/json_worker.js b/dom/workers/test/json_worker.js new file mode 100644 index 000000000..f35e14cf2 --- /dev/null +++ b/dom/workers/test/json_worker.js @@ -0,0 +1,338 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var cyclicalObject = {}; +cyclicalObject.foo = cyclicalObject; + +var cyclicalArray = []; +cyclicalArray.push(cyclicalArray); + +function makeCrazyNested(obj, count) { + var innermostobj; + for (var i = 0; i < count; i++) { + obj.foo = { bar: 5 } + innermostobj = obj.foo; + obj = innermostobj; + } + return innermostobj; +} + +var crazyNestedObject = {}; +makeCrazyNested(crazyNestedObject, 100); + +var crazyCyclicalObject = {}; +var innermost = makeCrazyNested(crazyCyclicalObject, 1000); +innermost.baz = crazyCyclicalObject; + +var objectWithSaneGetter = { }; +objectWithSaneGetter.__defineGetter__("foo", function() { return 5; }); + +// We don't walk prototype chains for cloning so this won't actually do much... +function objectWithSaneGetter2() { } +objectWithSaneGetter2.prototype = { + get foo() { + return 5; + } +}; + +const throwingGetterThrownString = "bad"; + +var objectWithThrowingGetter = { }; +objectWithThrowingGetter.__defineGetter__("foo", function() { + throw throwingGetterThrownString; +}); + +var typedArrayWithValues = new Int8Array(5); +for (var index in typedArrayWithValues) { + typedArrayWithValues[index] = index; +} + +var typedArrayWithFunBuffer = new Int8Array(4); +for (var index in typedArrayWithFunBuffer) { + typedArrayWithFunBuffer[index] = 255; +} + +var typedArrayWithFunBuffer2 = new Int32Array(typedArrayWithFunBuffer.buffer); + +var xhr = new XMLHttpRequest(); + +var messages = [ + { + type: "object", + value: { }, + jsonValue: '{}' + }, + { + type: "object", + value: {foo: "bar"}, + jsonValue: '{"foo":"bar"}' + }, + { + type: "object", + value: {foo: "bar", foo2: {bee: "bop"}}, + jsonValue: '{"foo":"bar","foo2":{"bee":"bop"}}' + }, + { + type: "object", + value: {foo: "bar", foo2: {bee: "bop"}, foo3: "baz"}, + jsonValue: '{"foo":"bar","foo2":{"bee":"bop"},"foo3":"baz"}' + }, + { + type: "object", + value: {foo: "bar", foo2: [1,2,3]}, + jsonValue: '{"foo":"bar","foo2":[1,2,3]}' + }, + { + type: "object", + value: cyclicalObject, + }, + { + type: "object", + value: [null, 2, false, cyclicalObject], + }, + { + type: "object", + value: cyclicalArray, + }, + { + type: "object", + value: {foo: 1, bar: cyclicalArray}, + }, + { + type: "object", + value: crazyNestedObject, + jsonValue: JSON.stringify(crazyNestedObject) + }, + { + type: "object", + value: crazyCyclicalObject, + }, + { + type: "object", + value: objectWithSaneGetter, + jsonValue: '{"foo":5}' + }, + { + type: "object", + value: new objectWithSaneGetter2(), + jsonValue: '{}' + }, + { + type: "object", + value: objectWithThrowingGetter, + exception: true + }, + { + type: "object", + array: true, + value: [9, 8, 7], + jsonValue: '[9,8,7]' + }, + { + type: "object", + array: true, + value: [9, false, 10.5, {foo: "bar"}], + jsonValue: '[9,false,10.5,{"foo":"bar"}]' + }, + { + type: "object", + shouldEqual: true, + value: null + }, + { + type: "undefined", + shouldEqual: true, + value: undefined + }, + { + type: "string", + shouldEqual: true, + value: "Hello" + }, + { + type: "string", + shouldEqual: true, + value: JSON.stringify({ foo: "bar" }), + compareValue: '{"foo":"bar"}' + }, + { + type: "number", + shouldEqual: true, + value: 1 + }, + { + type: "number", + shouldEqual: true, + value: 0 + }, + { + type: "number", + shouldEqual: true, + value: -1 + }, + { + type: "number", + shouldEqual: true, + value: 238573459843702923492399923049 + }, + { + type: "number", + shouldEqual: true, + value: -238573459843702923492399923049 + }, + { + type: "number", + shouldEqual: true, + value: 0.25 + }, + { + type: "number", + shouldEqual: true, + value: -0.25 + }, + { + type: "boolean", + shouldEqual: true, + value: true + }, + { + type: "boolean", + shouldEqual: true, + value: false + }, + { + type: "object", + value: function (foo) { return "Bad!"; }, + exception: true + }, + { + type: "number", + isNaN: true, + value: NaN + }, + { + type: "number", + isInfinity: true, + value: Infinity + }, + { + type: "number", + isNegativeInfinity: true, + value: -Infinity + }, + { + type: "object", + value: new Int32Array(10), + jsonValue: '{"0":0,"1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0}' + }, + { + type: "object", + value: new Float32Array(5), + jsonValue: '{"0":0,"1":0,"2":0,"3":0,"4":0}' + }, + { + type: "object", + value: typedArrayWithValues, + jsonValue: '{"0":0,"1":1,"2":2,"3":3,"4":4}' + }, + { + type: "number", + value: typedArrayWithValues[2], + compareValue: 2, + shouldEqual: true + }, + { + type: "object", + value: typedArrayWithValues.buffer, + jsonValue: '{}' + }, + { + type: "object", + value: typedArrayWithFunBuffer2, + jsonValue: '{"0":-1}' + }, + { + type: "object", + value: { foo: typedArrayWithFunBuffer2 }, + jsonValue: '{"foo":{"0":-1}}' + }, + { + type: "object", + value: [ typedArrayWithFunBuffer2 ], + jsonValue: '[{"0":-1}]' + }, + { + type: "object", + value: { foo: function(a) { alert(b); } }, + exception: true + }, + { + type: "object", + value: xhr, + exception: true + }, + { + type: "number", + value: xhr.readyState, + shouldEqual: true + }, + { + type: "object", + value: { xhr: xhr }, + exception: true + }, + { + type: "object", + value: self, + exception: true + }, + { + type: "object", + value: { p: ArrayBuffer.prototype }, + exception: true + }, + { + type: "string", + shouldEqual: true, + value: "testFinished" + } +]; + +for (var index = 0; index < messages.length; index++) { + var message = messages[index]; + if (message.hasOwnProperty("compareValue")) { + continue; + } + if (message.hasOwnProperty("shouldEqual") || + message.hasOwnProperty("shouldCompare")) { + message.compareValue = message.value; + } +} + +onmessage = function(event) { + for (var index = 0; index < messages.length; index++) { + var exception = undefined; + + try { + postMessage(messages[index].value); + } + catch (e) { + if (e instanceof DOMException) { + if (e.code != DOMException.DATA_CLONE_ERR) { + throw "DOMException with the wrong code: " + e.code; + } + } + else if (e != throwingGetterThrownString) { + throw "Exception of the wrong type: " + e; + } + exception = e; + } + + if ((exception !== undefined && !messages[index].exception) || + (exception === undefined && messages[index].exception)) { + throw "Exception inconsistency [index = " + index + ", " + + messages[index].toSource() + "]: " + exception; + } + } +} diff --git a/dom/workers/test/jsversion_worker.js b/dom/workers/test/jsversion_worker.js new file mode 100644 index 000000000..c66b72977 --- /dev/null +++ b/dom/workers/test/jsversion_worker.js @@ -0,0 +1,14 @@ +onmessage = function(evt) { + if (evt.data != 0) { + var worker = new Worker('jsversion_worker.js'); + worker.onmessage = function(evt) { + postMessage(evt.data); + } + + worker.postMessage(evt.data - 1); + return; + } + + let foo = 'bar'; + postMessage(true); +} diff --git a/dom/workers/test/loadEncoding_worker.js b/dom/workers/test/loadEncoding_worker.js new file mode 100644 index 000000000..5e4047844 --- /dev/null +++ b/dom/workers/test/loadEncoding_worker.js @@ -0,0 +1,7 @@ +/* + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +*/ +// Bug 484305 - Load workers as UTF-8. +postMessage({ encoding: "KOI8-R", text: "ðÒÉ×ÅÔ" }); +postMessage({ encoding: "UTF-8", text: "Привет" }); diff --git a/dom/workers/test/location_worker.js b/dom/workers/test/location_worker.js new file mode 100644 index 000000000..dd35ec8a3 --- /dev/null +++ b/dom/workers/test/location_worker.js @@ -0,0 +1,11 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +for (var string in self.location) { + var value = typeof self.location[string] === "function" + ? self.location[string]() + : self.location[string]; + postMessage({ "string": string, "value": value }); +} +postMessage({ "string": "testfinished" }); diff --git a/dom/workers/test/longThread_worker.js b/dom/workers/test/longThread_worker.js new file mode 100644 index 000000000..f132dd8c1 --- /dev/null +++ b/dom/workers/test/longThread_worker.js @@ -0,0 +1,14 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function(event) { + switch (event.data) { + case "start": + for (var i = 0; i < 10000000; i++) { }; + postMessage("done"); + break; + default: + throw "Bad message: " + event.data; + } +}; diff --git a/dom/workers/test/mochitest.ini b/dom/workers/test/mochitest.ini new file mode 100644 index 000000000..d4848819c --- /dev/null +++ b/dom/workers/test/mochitest.ini @@ -0,0 +1,227 @@ +[DEFAULT] +support-files = + WorkerTest_badworker.js + atob_worker.js + bug978260_worker.js + bug1014466_data1.txt + bug1014466_data2.txt + bug1014466_worker.js + bug1020226_worker.js + bug1020226_frame.html + bug998474_worker.js + bug1063538_worker.js + clearTimeouts_worker.js + content_worker.js + console_worker.js + consoleReplaceable_worker.js + csp_worker.js + csp_worker.js^headers^ + 404_server.sjs + errorPropagation_iframe.html + errorPropagation_worker.js + errorwarning_worker.js + eventDispatch_worker.js + fibonacci_worker.js + file_bug1010784_worker.js + foreign.js + importForeignScripts_worker.js + importScripts_mixedcontent.html + importScripts_worker.js + importScripts_worker_imported1.js + importScripts_worker_imported2.js + importScripts_worker_imported3.js + importScripts_worker_imported4.js + instanceof_worker.js + json_worker.js + jsversion_worker.js + loadEncoding_worker.js + location_worker.js + longThread_worker.js + multi_sharedWorker_frame.html + multi_sharedWorker_sharedWorker.js + navigator_languages_worker.js + navigator_worker.js + newError_worker.js + notification_worker.js + notification_worker_child-child.js + notification_worker_child-parent.js + notification_permission_worker.js + onLine_worker.js + onLine_worker_child.js + onLine_worker_head.js + promise_worker.js + recursion_worker.js + recursiveOnerror_worker.js + redirect_to_foreign.sjs + rvals_worker.js + sharedWorker_console.js + sharedWorker_sharedWorker.js + simpleThread_worker.js + suspend_iframe.html + suspend_worker.js + terminate_worker.js + test_csp.html^headers^ + test_csp.js + referrer_worker.html + threadErrors_worker1.js + threadErrors_worker2.js + threadErrors_worker3.js + threadErrors_worker4.js + threadTimeouts_worker.js + throwingOnerror_worker.js + timeoutTracing_worker.js + transferable_worker.js + websocket_basic_worker.js + websocket_loadgroup_worker.js + websocket_worker1.js + websocket_worker2.js + websocket_worker3.js + websocket_worker4.js + websocket_worker5.js + websocket_helpers.js + workersDisabled_worker.js + test_worker_interfaces.js + worker_driver.js + worker_wrapper.js + bug1060621_worker.js + bug1062920_worker.js + webSocket_sharedWorker.js + bug1104064_worker.js + worker_consoleAndBlobs.js + bug1132395_sharedWorker.js + bug1132924_worker.js + empty.html + referrer.sjs + referrer_test_server.sjs + sharedWorker_ports.js + sharedWorker_lifetime.js + worker_referrer.js + websocket_https.html + websocket_https_worker.js + worker_fileReader.js + fileapi_chromeScript.js + importScripts_3rdParty_worker.js + worker_bug1278777.js + worker_setTimeoutWith0.js + worker_bug1301094.js + script_createFile.js + worker_suspended.js + window_suspended.html + !/dom/base/test/file_bug945152.jar + !/dom/base/test/file_websocket_basic_wsh.py + !/dom/base/test/file_websocket_hello_wsh.py + !/dom/base/test/file_websocket_http_resource.txt + !/dom/base/test/file_websocket_permessage_deflate_disabled_wsh.py + !/dom/base/test/file_websocket_permessage_deflate_params_wsh.py + !/dom/base/test/file_websocket_permessage_deflate_rejected_wsh.py + !/dom/base/test/file_websocket_permessage_deflate_wsh.py + !/dom/base/test/file_websocket_wsh.py + !/dom/base/test/websocket_helpers.js + !/dom/base/test/websocket_tests.js + !/dom/tests/mochitest/notification/MockServices.js + !/dom/tests/mochitest/notification/NotificationTest.js + !/dom/xhr/tests/relativeLoad_import.js + !/dom/xhr/tests/relativeLoad_worker.js + !/dom/xhr/tests/relativeLoad_worker2.js + !/dom/xhr/tests/subdir/relativeLoad_sub_worker.js + !/dom/xhr/tests/subdir/relativeLoad_sub_worker2.js + !/dom/xhr/tests/subdir/relativeLoad_sub_import.js + +[test_404.html] +[test_atob.html] +[test_blobConstructor.html] +[test_blobWorkers.html] +[test_bug949946.html] +[test_bug978260.html] +[test_bug998474.html] +[test_bug1002702.html] +[test_bug1010784.html] +[test_bug1014466.html] +[test_bug1020226.html] +[test_bug1036484.html] +[test_bug1060621.html] +[test_bug1062920.html] +[test_bug1063538.html] +[test_bug1104064.html] +[test_bug1132395.html] +skip-if = true # bug 1176225 +[test_bug1132924.html] +[test_chromeWorker.html] +[test_clearTimeouts.html] +[test_console.html] +[test_consoleAndBlobs.html] +[test_consoleReplaceable.html] +[test_consoleSharedWorkers.html] +[test_contentWorker.html] +[test_csp.html] +[test_dataURLWorker.html] +[test_errorPropagation.html] +[test_errorwarning.html] +[test_eventDispatch.html] +[test_fibonacci.html] +[test_importScripts.html] +[test_importScripts_mixedcontent.html] +tags = mcb +[test_instanceof.html] +[test_json.html] +[test_jsversion.html] +[test_loadEncoding.html] +[test_loadError.html] +[test_location.html] +[test_longThread.html] +[test_multi_sharedWorker.html] +[test_multi_sharedWorker_lifetimes.html] +[test_navigator.html] +[test_navigator_languages.html] +[test_newError.html] +[test_notification.html] +[test_notification_child.html] +[test_notification_permission.html] +[test_onLine.html] +[test_promise.html] +[test_promise_resolved_with_string.html] +[test_recursion.html] +[test_recursiveOnerror.html] +[test_resolveWorker.html] +[test_resolveWorker-assignment.html] +[test_rvals.html] +[test_sharedWorker.html] +[test_simpleThread.html] +[test_suspend.html] +[test_terminate.html] +[test_threadErrors.html] +[test_threadTimeouts.html] +[test_throwingOnerror.html] +[test_timeoutTracing.html] +[test_transferable.html] +[test_websocket1.html] +skip-if = toolkit == 'android' #bug 982828 +[test_websocket2.html] +skip-if = toolkit == 'android' #bug 982828 +[test_websocket3.html] +skip-if = toolkit == 'android' #bug 982828 +[test_websocket4.html] +skip-if = toolkit == 'android' #bug 982828 +[test_websocket5.html] +skip-if = toolkit == 'android' #bug 982828 +[test_websocket_basic.html] +skip-if = toolkit == 'android' #bug 982828 +[test_websocket_https.html] +[test_websocket_loadgroup.html] +skip-if = toolkit == 'android' #bug 982828 +[test_webSocket_sharedWorker.html] +skip-if = toolkit == 'android' #bug 982828 +[test_worker_interfaces.html] +[test_workersDisabled.html] +[test_referrer.html] +[test_referrer_header_worker.html] +[test_importScripts_3rdparty.html] +[test_sharedWorker_ports.html] +[test_sharedWorker_lifetime.html] +[test_fileReader.html] +[test_navigator_workers_hardwareConcurrency.html] +[test_bug1278777.html] +[test_setTimeoutWith0.html] +[test_bug1301094.html] +[test_subworkers_suspended.html] +[test_bug1317725.html] diff --git a/dom/workers/test/multi_sharedWorker_frame.html b/dom/workers/test/multi_sharedWorker_frame.html new file mode 100644 index 000000000..6c0dfe591 --- /dev/null +++ b/dom/workers/test/multi_sharedWorker_frame.html @@ -0,0 +1,52 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for SharedWorker</title> + </head> + <body> + <script type="text/javascript;version=1.7"> + "use strict"; + + function debug(message) { + if (typeof(message) != "string") { + throw new Error("debug() only accepts strings!"); + } + parent.postMessage(message, "*"); + } + + let worker; + + window.addEventListener("message", function(event) { + if (!worker) { + worker = new SharedWorker("multi_sharedWorker_sharedWorker.js", + "FrameWorker"); + worker.onerror = function(event) { + debug("Worker error: " + event.message); + event.preventDefault(); + + let data = { + type: "error", + message: event.message, + filename: event.filename, + lineno: event.lineno, + isErrorEvent: event instanceof ErrorEvent + }; + parent.postMessage(data, "*"); + }; + + worker.port.onmessage = function(event) { + debug("Worker message: " + JSON.stringify(event.data)); + parent.postMessage(event.data, "*"); + }; + } + + debug("Posting message: " + JSON.stringify(event.data)); + worker.port.postMessage(event.data); + }); + </script> + </body> +</html> diff --git a/dom/workers/test/multi_sharedWorker_sharedWorker.js b/dom/workers/test/multi_sharedWorker_sharedWorker.js new file mode 100644 index 000000000..47a7ae04f --- /dev/null +++ b/dom/workers/test/multi_sharedWorker_sharedWorker.js @@ -0,0 +1,72 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +if (self.name != "FrameWorker") { + throw new Error("Bad worker name: " + self.name); +} + +var registeredPorts = []; +var errorCount = 0; +var storedData; + +self.onconnect = function(event) { + var port = event.ports[0]; + + if (registeredPorts.length) { + var data = { + type: "connect" + }; + + registeredPorts.forEach(function(registeredPort) { + registeredPort.postMessage(data); + }); + } + + port.onmessage = function(event) { + switch (event.data.command) { + case "start": + break; + + case "error": + throw new Error("Expected"); + + case "store": + storedData = event.data.data; + break; + + case "retrieve": + var data = { + type: "result", + data: storedData + }; + port.postMessage(data); + break; + + default: + throw new Error("Unknown command '" + error.data.command + "'"); + } + }; + + registeredPorts.push(port); +}; + +self.onerror = function(message, filename, lineno) { + if (!errorCount++) { + var data = { + type: "worker-error", + message: message, + filename: filename, + lineno: lineno + }; + + registeredPorts.forEach(function (registeredPort) { + registeredPort.postMessage(data); + }); + + // Prevent the error from propagating the first time only. + return true; + } +}; diff --git a/dom/workers/test/navigator_languages_worker.js b/dom/workers/test/navigator_languages_worker.js new file mode 100644 index 000000000..53aa4d39e --- /dev/null +++ b/dom/workers/test/navigator_languages_worker.js @@ -0,0 +1,11 @@ +var active = true; +onmessage = function(e) { + if (e.data == 'finish') { + active = false; + return; + } + + if (active) { + postMessage(navigator.languages); + } +} diff --git a/dom/workers/test/navigator_worker.js b/dom/workers/test/navigator_worker.js new file mode 100644 index 000000000..63853aef1 --- /dev/null +++ b/dom/workers/test/navigator_worker.js @@ -0,0 +1,79 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// IMPORTANT: Do not change the list below without review from a DOM peer! +var supportedProps = [ + "appCodeName", + "appName", + "appVersion", + "platform", + "product", + "userAgent", + "onLine", + "language", + "languages", + "hardwareConcurrency", + { name: "storage", nightly: true }, +]; + +self.onmessage = function(event) { + if (!event || !event.data) { + return; + } + + startTest(event.data); +}; + +function startTest(channelData) { + // Prepare the interface map showing if a propery should exist in this build. + // For example, if interfaceMap[foo] = true means navigator.foo should exist. + var interfaceMap = {}; + + for (var prop of supportedProps) { + if (typeof(prop) === "string") { + interfaceMap[prop] = true; + continue; + } + + if (prop.nightly === !channelData.isNightly || + prop.release === !channelData.isRelease) { + interfaceMap[prop.name] = false; + continue; + } + + interfaceMap[prop.name] = true; + } + + for (var prop in navigator) { + // Make sure the list is current! + if (!interfaceMap[prop]) { + throw "Navigator has the '" + prop + "' property that isn't in the list!"; + } + } + + var obj; + + for (var prop in interfaceMap) { + // Skip the property that is not supposed to exist in this build. + if (!interfaceMap[prop]) { + continue; + } + + if (typeof navigator[prop] == "undefined") { + throw "Navigator has no '" + prop + "' property!"; + } + + obj = { name: prop }; + obj.value = navigator[prop]; + + postMessage(JSON.stringify(obj)); + } + + obj = { + name: "testFinished" + }; + + postMessage(JSON.stringify(obj)); +} diff --git a/dom/workers/test/newError_worker.js b/dom/workers/test/newError_worker.js new file mode 100644 index 000000000..46e6226f7 --- /dev/null +++ b/dom/workers/test/newError_worker.js @@ -0,0 +1,5 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +throw new Error("foo!"); diff --git a/dom/workers/test/notification_permission_worker.js b/dom/workers/test/notification_permission_worker.js new file mode 100644 index 000000000..f69acaeda --- /dev/null +++ b/dom/workers/test/notification_permission_worker.js @@ -0,0 +1,56 @@ +function info(message) { + dump("INFO: " + message + "\n"); +} + +function ok(test, message) { + postMessage({ type: 'ok', test: test, message: message }); +} + +function is(a, b, message) { + postMessage({ type: 'is', test1: a, test2: b, message: message }); +} + +if (self.Notification) { + var steps = [ + function (done) { + info("Test notification permission"); + ok(typeof Notification === "function", "Notification constructor exists"); + ok(Notification.permission === "denied", "Notification.permission is denied."); + + var n = new Notification("Hello"); + n.onerror = function(e) { + ok(true, "error called due to permission denied."); + done(); + } + }, + ]; + + onmessage = function(e) { + var context = {}; + (function executeRemainingTests(remainingTests) { + if (!remainingTests.length) { + postMessage({type: 'finish'}); + return; + } + + var nextTest = remainingTests.shift(); + var finishTest = executeRemainingTests.bind(null, remainingTests); + var startTest = nextTest.call.bind(nextTest, context, finishTest); + + try { + startTest(); + // if no callback was defined for test function, + // we must manually invoke finish to continue + if (nextTest.length === 0) { + finishTest(); + } + } catch (e) { + ok(false, "Test threw exception! " + nextTest + " " + e); + finishTest(); + } + })(steps); + } +} else { + ok(true, "Notifications are not enabled in workers on the platform."); + postMessage({ type: 'finish' }); +} diff --git a/dom/workers/test/notification_worker.js b/dom/workers/test/notification_worker.js new file mode 100644 index 000000000..cb55f8358 --- /dev/null +++ b/dom/workers/test/notification_worker.js @@ -0,0 +1,93 @@ +function ok(test, message) { + postMessage({ type: 'ok', test: test, message: message }); +} + +function is(a, b, message) { + postMessage({ type: 'is', test1: a, test2: b, message: message }); +} + +if (self.Notification) { + var steps = [ + function () { + ok(typeof Notification === "function", "Notification constructor exists"); + ok(Notification.permission, "Notification.permission exists"); + ok(typeof Notification.requestPermission === "undefined", "Notification.requestPermission should not exist"); + }, + + function (done) { + var options = { + dir: "auto", + lang: "", + body: "This is a notification body", + tag: "sometag", + icon: "icon.png", + data: ["a complex object that should be", { "structured": "cloned" }], + mozbehavior: { vibrationPattern: [30, 200, 30] }, + }; + var notification = new Notification("This is a title", options); + + ok(notification !== undefined, "Notification exists"); + is(notification.onclick, null, "onclick() should be null"); + is(notification.onshow, null, "onshow() should be null"); + is(notification.onerror, null, "onerror() should be null"); + is(notification.onclose, null, "onclose() should be null"); + is(typeof notification.close, "function", "close() should exist"); + + is(notification.dir, options.dir, "auto should get set"); + is(notification.lang, options.lang, "lang should get set"); + is(notification.body, options.body, "body should get set"); + is(notification.tag, options.tag, "tag should get set"); + is(notification.icon, options.icon, "icon should get set"); + is(notification.data[0], "a complex object that should be", "data item 0 should be a matching string"); + is(notification.data[1]["structured"], "cloned", "data item 1 should be a matching object literal"); + + // store notification in test context + this.notification = notification; + + notification.onshow = function () { + ok(true, "onshow handler should be called"); + done(); + }; + }, + + function (done) { + var notification = this.notification; + + notification.onclose = function () { + ok(true, "onclose handler should be called"); + done(); + }; + + notification.close(); + }, + ]; + + onmessage = function(e) { + var context = {}; + (function executeRemainingTests(remainingTests) { + if (!remainingTests.length) { + postMessage({type: 'finish'}); + return; + } + + var nextTest = remainingTests.shift(); + var finishTest = executeRemainingTests.bind(null, remainingTests); + var startTest = nextTest.call.bind(nextTest, context, finishTest); + + try { + startTest(); + // if no callback was defined for test function, + // we must manually invoke finish to continue + if (nextTest.length === 0) { + finishTest(); + } + } catch (e) { + ok(false, "Test threw exception! " + nextTest + " " + e); + finishTest(); + } + })(steps); + } +} else { + ok(true, "Notifications are not enabled in workers on the platform."); + postMessage({ type: 'finish' }); +} diff --git a/dom/workers/test/notification_worker_child-child.js b/dom/workers/test/notification_worker_child-child.js new file mode 100644 index 000000000..829bf43d3 --- /dev/null +++ b/dom/workers/test/notification_worker_child-child.js @@ -0,0 +1,92 @@ +function ok(test, message) { + postMessage({ type: 'ok', test: test, message: message }); +} + +function is(a, b, message) { + postMessage({ type: 'is', test1: a, test2: b, message: message }); +} + +if (self.Notification) { + var steps = [ + function () { + ok(typeof Notification === "function", "Notification constructor exists"); + ok(Notification.permission, "Notification.permission exists"); + ok(typeof Notification.requestPermission === "undefined", "Notification.requestPermission should not exist"); + //ok(typeof Notification.get === "function", "Notification.get exists"); + }, + + function (done) { + + var options = { + dir: "auto", + lang: "", + body: "This is a notification body", + tag: "sometag", + icon: "icon.png" + }; + var notification = new Notification("This is a title", options); + + ok(notification !== undefined, "Notification exists"); + is(notification.onclick, null, "onclick() should be null"); + is(notification.onshow, null, "onshow() should be null"); + is(notification.onerror, null, "onerror() should be null"); + is(notification.onclose, null, "onclose() should be null"); + is(typeof notification.close, "function", "close() should exist"); + + is(notification.dir, options.dir, "auto should get set"); + is(notification.lang, options.lang, "lang should get set"); + is(notification.body, options.body, "body should get set"); + is(notification.tag, options.tag, "tag should get set"); + is(notification.icon, options.icon, "icon should get set"); + + // store notification in test context + this.notification = notification; + + notification.onshow = function () { + ok(true, "onshow handler should be called"); + done(); + }; + }, + + function (done) { + var notification = this.notification; + + notification.onclose = function () { + ok(true, "onclose handler should be called"); + done(); + }; + + notification.close(); + }, + ]; + + onmessage = function(e) { + var context = {}; + (function executeRemainingTests(remainingTests) { + if (!remainingTests.length) { + postMessage({type: 'finish'}); + return; + } + + var nextTest = remainingTests.shift(); + var finishTest = executeRemainingTests.bind(null, remainingTests); + var startTest = nextTest.call.bind(nextTest, context, finishTest); + + try { + startTest(); + // if no callback was defined for test function, + // we must manually invoke finish to continue + if (nextTest.length === 0) { + finishTest(); + } + } catch (e) { + ok(false, "Test threw exception! " + nextTest + " " + e); + finishTest(); + } + })(steps); + } +} else { + ok(true, "Notifications are not enabled in workers on the platform."); + postMessage({ type: 'finish' }); +} + diff --git a/dom/workers/test/notification_worker_child-parent.js b/dom/workers/test/notification_worker_child-parent.js new file mode 100644 index 000000000..bb28c520d --- /dev/null +++ b/dom/workers/test/notification_worker_child-parent.js @@ -0,0 +1,26 @@ +function ok(test, message) { + postMessage({ type: 'ok', test: test, message: message }); +} + +function is(a, b, message) { + postMessage({ type: 'is', test1: a, test2: b, message: message }); +} + +if (self.Notification) { + var child = new Worker("notification_worker_child-child.js"); + child.onerror = function(e) { + ok(false, "Error loading child worker " + e); + postMessage({ type: 'finish' }); + } + + child.onmessage = function(e) { + postMessage(e.data); + } + + onmessage = function(e) { + child.postMessage('start'); + } +} else { + ok(true, "Notifications are not enabled in workers on the platform."); + postMessage({ type: 'finish' }); +} diff --git a/dom/workers/test/onLine_worker.js b/dom/workers/test/onLine_worker.js new file mode 100644 index 000000000..5a302b15e --- /dev/null +++ b/dom/workers/test/onLine_worker.js @@ -0,0 +1,65 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ + */ + +importScripts("onLine_worker_head.js"); + +var N_CHILDREN = 3; +var children = []; +var finishedChildrenCount = 0; +var lastTest = false; + +for (var event of ["online", "offline"]) { + addEventListener(event, + makeHandler( + "addEventListener('%1', ..., false)", + event, 1, "Parent Worker"), + false); +} + +onmessage = function(e) { + if (e.data === 'lastTest') { + children.forEach(function(w) { + w.postMessage({ type: 'lastTest' }); + }); + lastTest = true; + } +} + +function setupChildren(cb) { + var readyCount = 0; + for (var i = 0; i < N_CHILDREN; ++i) { + var w = new Worker("onLine_worker_child.js"); + children.push(w); + + w.onerror = function(e) { + info("Error creating child " + e.message); + } + + w.onmessage = function(e) { + if (e.data.type === 'ready') { + info("Got ready from child"); + readyCount++; + if (readyCount === N_CHILDREN) { + cb(); + } + } else if (e.data.type === 'finished') { + finishedChildrenCount++; + + if (lastTest && finishedChildrenCount === N_CHILDREN) { + postMessage({ type: 'finished' }); + children = []; + close(); + } + } else if (e.data.type === 'ok') { + // Pass on test to page. + postMessage(e.data); + } + } + } +} + +setupChildren(function() { + postMessage({ type: 'ready' }); +}); diff --git a/dom/workers/test/onLine_worker_child.js b/dom/workers/test/onLine_worker_child.js new file mode 100644 index 000000000..dc28fac45 --- /dev/null +++ b/dom/workers/test/onLine_worker_child.js @@ -0,0 +1,75 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ + */ + +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ + */ + +function info(text) { + dump("Test for Bug 925437: worker: " + text + "\n"); +} + +function ok(test, message) { + postMessage({ type: 'ok', test: test, message: message }); +} + +/** + * Returns a handler function for an online/offline event. The returned handler + * ensures the passed event object has expected properties and that the handler + * is called at the right moment (according to the gState variable). + * @param nameTemplate The string identifying the hanlder. '%1' in that + * string will be replaced with the event name. + * @param eventName 'online' or 'offline' + * @param expectedState value of gState at the moment the handler is called. + * The handler increases gState by one before checking + * if it matches expectedState. + */ +function makeHandler(nameTemplate, eventName, expectedState, prefix, custom) { + prefix += ": "; + return function(e) { + var name = nameTemplate.replace(/%1/, eventName); + ok(e.constructor == Event, prefix + "event should be an Event"); + ok(e.type == eventName, prefix + "event type should be " + eventName); + ok(!e.bubbles, prefix + "event should not bubble"); + ok(!e.cancelable, prefix + "event should not be cancelable"); + ok(e.target == self, prefix + "the event target should be the worker scope"); + ok(eventName == 'online' ? navigator.onLine : !navigator.onLine, prefix + "navigator.onLine " + navigator.onLine + " should reflect event " + eventName); + + if (custom) { + custom(); + } + } +} + + + +var lastTest = false; + +function lastTestTest() { + if (lastTest) { + postMessage({ type: 'finished' }); + close(); + } +} + +for (var event of ["online", "offline"]) { + addEventListener(event, + makeHandler( + "addEventListener('%1', ..., false)", + event, 1, "Child Worker", lastTestTest + ), + false); +} + +onmessage = function(e) { + if (e.data.type === 'lastTest') { + lastTest = true; + } else if (e.data.type === 'navigatorState') { + ok(e.data.state === navigator.onLine, "Child and parent navigator state should match"); + } +} + +postMessage({ type: 'ready' }); diff --git a/dom/workers/test/onLine_worker_head.js b/dom/workers/test/onLine_worker_head.js new file mode 100644 index 000000000..4800457d3 --- /dev/null +++ b/dom/workers/test/onLine_worker_head.js @@ -0,0 +1,43 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ + */ + +function info(text) { + dump("Test for Bug 925437: worker: " + text + "\n"); +} + +function ok(test, message) { + postMessage({ type: 'ok', test: test, message: message }); +} + +/** + * Returns a handler function for an online/offline event. The returned handler + * ensures the passed event object has expected properties and that the handler + * is called at the right moment (according to the gState variable). + * @param nameTemplate The string identifying the hanlder. '%1' in that + * string will be replaced with the event name. + * @param eventName 'online' or 'offline' + * @param expectedState value of gState at the moment the handler is called. + * The handler increases gState by one before checking + * if it matches expectedState. + */ +function makeHandler(nameTemplate, eventName, expectedState, prefix, custom) { + prefix += ": "; + return function(e) { + var name = nameTemplate.replace(/%1/, eventName); + ok(e.constructor == Event, prefix + "event should be an Event"); + ok(e.type == eventName, prefix + "event type should be " + eventName); + ok(!e.bubbles, prefix + "event should not bubble"); + ok(!e.cancelable, prefix + "event should not be cancelable"); + ok(e.target == self, prefix + "the event target should be the worker scope"); + ok(eventName == 'online' ? navigator.onLine : !navigator.onLine, prefix + "navigator.onLine " + navigator.onLine + " should reflect event " + eventName); + + if (custom) { + custom(); + } + } +} + + + diff --git a/dom/workers/test/promise_worker.js b/dom/workers/test/promise_worker.js new file mode 100644 index 000000000..5b0a8478b --- /dev/null +++ b/dom/workers/test/promise_worker.js @@ -0,0 +1,856 @@ +function ok(a, msg) { + dump("OK: " + !!a + " => " + a + " " + msg + "\n"); + postMessage({type: 'status', status: !!a, msg: a + ": " + msg }); +} + +function todo(a, msg) { + dump("TODO: " + !a + " => " + a + " " + msg + "\n"); + postMessage({type: 'status', status: !a, msg: a + ": " + msg }); +} + +function is(a, b, msg) { + dump("IS: " + (a===b) + " => " + a + " | " + b + " " + msg + "\n"); + postMessage({type: 'status', status: a === b, msg: a + " === " + b + ": " + msg }); +} + +function isnot(a, b, msg) { + dump("ISNOT: " + (a!==b) + " => " + a + " | " + b + " " + msg + "\n"); + postMessage({type: 'status', status: a !== b, msg: a + " !== " + b + ": " + msg }); +} + +function promiseResolve() { + ok(Promise, "Promise object should exist"); + + var promise = new Promise(function(resolve, reject) { + ok(resolve, "Promise.resolve exists"); + ok(reject, "Promise.reject exists"); + + resolve(42); + }).then(function(what) { + ok(true, "Then - resolveCb has been called"); + is(what, 42, "ResolveCb received 42"); + runTest(); + }, function() { + ok(false, "Then - rejectCb has been called"); + runTest(); + }); +} + +function promiseResolveNoArg() { + var promise = new Promise(function(resolve, reject) { + ok(resolve, "Promise.resolve exists"); + ok(reject, "Promise.reject exists"); + + resolve(); + }).then(function(what) { + ok(true, "Then - resolveCb has been called"); + is(what, undefined, "ResolveCb received undefined"); + runTest(); + }, function() { + ok(false, "Then - rejectCb has been called"); + runTest(); + }); +} + +function promiseRejectNoHandler() { + // This test only checks that the code that reports unhandled errors in the + // Promises implementation does not crash or leak. + var promise = new Promise(function(res, rej) { + noSuchMethod(); + }); + runTest(); +} + +function promiseReject() { + var promise = new Promise(function(resolve, reject) { + reject(42); + }).then(function(what) { + ok(false, "Then - resolveCb has been called"); + runTest(); + }, function(what) { + ok(true, "Then - rejectCb has been called"); + is(what, 42, "RejectCb received 42"); + runTest(); + }); +} + +function promiseRejectNoArg() { + var promise = new Promise(function(resolve, reject) { + reject(); + }).then(function(what) { + ok(false, "Then - resolveCb has been called"); + runTest(); + }, function(what) { + ok(true, "Then - rejectCb has been called"); + is(what, undefined, "RejectCb received undefined"); + runTest(); + }); +} + +function promiseException() { + var promise = new Promise(function(resolve, reject) { + throw 42; + }).then(function(what) { + ok(false, "Then - resolveCb has been called"); + runTest(); + }, function(what) { + ok(true, "Then - rejectCb has been called"); + is(what, 42, "RejectCb received 42"); + runTest(); + }); +} + +function promiseAsync_TimeoutResolveThen() { + var handlerExecuted = false; + + setTimeout(function() { + ok(handlerExecuted, "Handler should have been called before the timeout."); + + // Allow other assertions to run so the test could fail before the next one. + setTimeout(runTest, 0); + }, 0); + + Promise.resolve().then(function() { + handlerExecuted = true; + }); + + ok(!handlerExecuted, "Handlers are not called before 'then' returns."); +} + +function promiseAsync_ResolveTimeoutThen() { + var handlerExecuted = false; + + var promise = Promise.resolve(); + + setTimeout(function() { + ok(handlerExecuted, "Handler should have been called before the timeout."); + + // Allow other assertions to run so the test could fail before the next one. + setTimeout(runTest, 0); + }, 0); + + promise.then(function() { + handlerExecuted = true; + }); + + ok(!handlerExecuted, "Handlers are not called before 'then' returns."); +} + +function promiseAsync_ResolveThenTimeout() { + var handlerExecuted = false; + + Promise.resolve().then(function() { + handlerExecuted = true; + }); + + setTimeout(function() { + ok(handlerExecuted, "Handler should have been called before the timeout."); + + // Allow other assertions to run so the test could fail before the next one. + setTimeout(runTest, 0); + }, 0); + + ok(!handlerExecuted, "Handlers are not called before 'then' returns."); +} + +function promiseAsync_SyncXHRAndImportScripts() +{ + var handlerExecuted = false; + + Promise.resolve().then(function() { + handlerExecuted = true; + + // Allow other assertions to run so the test could fail before the next one. + setTimeout(runTest, 0); + }); + + ok(!handlerExecuted, "Handlers are not called until the next microtask."); + + var xhr = new XMLHttpRequest(); + xhr.open("GET", "testXHR.txt", false); + xhr.send(null); + + ok(!handlerExecuted, "Sync XHR should not trigger microtask execution."); + + importScripts("../../../dom/xhr/tests/relativeLoad_import.js"); + + ok(!handlerExecuted, "importScripts should not trigger microtask execution."); +} + +function promiseDoubleThen() { + var steps = 0; + var promise = new Promise(function(r1, r2) { + r1(42); + }); + + promise.then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 42, "Value == 42"); + steps++; + }, function(what) { + ok(false, "Then.reject has been called"); + }); + + promise.then(function(what) { + ok(true, "Then.resolve has been called"); + is(steps, 1, "Then.resolve - step == 1"); + is(what, 42, "Value == 42"); + runTest(); + }, function(what) { + ok(false, "Then.reject has been called"); + }); +} + +function promiseThenException() { + var promise = new Promise(function(resolve, reject) { + resolve(42); + }); + + promise.then(function(what) { + ok(true, "Then.resolve has been called"); + throw "booh"; + }).catch(function(e) { + ok(true, "Catch has been called!"); + runTest(); + }); +} + +function promiseThenCatchThen() { + var promise = new Promise(function(resolve, reject) { + resolve(42); + }); + + var promise2 = promise.then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 42, "Value == 42"); + return what + 1; + }, function(what) { + ok(false, "Then.reject has been called"); + }); + + isnot(promise, promise2, "These 2 promise objs are different"); + + promise2.then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 43, "Value == 43"); + return what + 1; + }, function(what) { + ok(false, "Then.reject has been called"); + }).catch(function() { + ok(false, "Catch has been called"); + }).then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 44, "Value == 44"); + runTest(); + }, function(what) { + ok(false, "Then.reject has been called"); + }); +} + +function promiseRejectThenCatchThen() { + var promise = new Promise(function(resolve, reject) { + reject(42); + }); + + var promise2 = promise.then(function(what) { + ok(false, "Then.resolve has been called"); + }, function(what) { + ok(true, "Then.reject has been called"); + is(what, 42, "Value == 42"); + return what + 1; + }); + + isnot(promise, promise2, "These 2 promise objs are different"); + + promise2.then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 43, "Value == 43"); + return what+1; + }).catch(function(what) { + ok(false, "Catch has been called"); + }).then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 44, "Value == 44"); + runTest(); + }); +} + +function promiseRejectThenCatchThen2() { + var promise = new Promise(function(resolve, reject) { + reject(42); + }); + + promise.then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 42, "Value == 42"); + return what+1; + }).catch(function(what) { + is(what, 42, "Value == 42"); + ok(true, "Catch has been called"); + return what+1; + }).then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 43, "Value == 43"); + runTest(); + }); +} + +function promiseRejectThenCatchExceptionThen() { + var promise = new Promise(function(resolve, reject) { + reject(42); + }); + + promise.then(function(what) { + ok(false, "Then.resolve has been called"); + }, function(what) { + ok(true, "Then.reject has been called"); + is(what, 42, "Value == 42"); + throw(what + 1); + }).catch(function(what) { + ok(true, "Catch has been called"); + is(what, 43, "Value == 43"); + return what + 1; + }).then(function(what) { + ok(true, "Then.resolve has been called"); + is(what, 44, "Value == 44"); + runTest(); + }); +} + +function promiseThenCatchOrderingResolve() { + var global = 0; + var f = new Promise(function(r1, r2) { + r1(42); + }); + + f.then(function() { + f.then(function() { + global++; + }); + f.catch(function() { + global++; + }); + f.then(function() { + global++; + }); + setTimeout(function() { + is(global, 2, "Many steps... should return 2"); + runTest(); + }, 0); + }); +} + +function promiseThenCatchOrderingReject() { + var global = 0; + var f = new Promise(function(r1, r2) { + r2(42); + }) + + f.then(function() {}, function() { + f.then(function() { + global++; + }); + f.catch(function() { + global++; + }); + f.then(function() {}, function() { + global++; + }); + setTimeout(function() { + is(global, 2, "Many steps... should return 2"); + runTest(); + }, 0); + }); +} + +function promiseThenNoArg() { + var promise = new Promise(function(resolve, reject) { + resolve(42); + }); + + var clone = promise.then(); + isnot(promise, clone, "These 2 promise objs are different"); + promise.then(function(v) { + clone.then(function(cv) { + is(v, cv, "Both resolve to the same value"); + runTest(); + }); + }); +} + +function promiseThenUndefinedResolveFunction() { + var promise = new Promise(function(resolve, reject) { + reject(42); + }); + + try { + promise.then(undefined, function(v) { + is(v, 42, "Promise rejected with 42"); + runTest(); + }); + } catch (e) { + ok(false, "then should not throw on undefined resolve function"); + } +} + +function promiseThenNullResolveFunction() { + var promise = new Promise(function(resolve, reject) { + reject(42); + }); + + try { + promise.then(null, function(v) { + is(v, 42, "Promise rejected with 42"); + runTest(); + }); + } catch (e) { + ok(false, "then should not throw on null resolve function"); + } +} + +function promiseCatchNoArg() { + var promise = new Promise(function(resolve, reject) { + reject(42); + }); + + var clone = promise.catch(); + isnot(promise, clone, "These 2 promise objs are different"); + promise.catch(function(v) { + clone.catch(function(cv) { + is(v, cv, "Both reject to the same value"); + runTest(); + }); + }); +} + +function promiseNestedPromise() { + new Promise(function(resolve, reject) { + resolve(new Promise(function(resolve, reject) { + ok(true, "Nested promise is executed"); + resolve(42); + })); + }).then(function(value) { + is(value, 42, "Nested promise is executed and then == 42"); + runTest(); + }); +} + +function promiseNestedNestedPromise() { + new Promise(function(resolve, reject) { + resolve(new Promise(function(resolve, reject) { + ok(true, "Nested promise is executed"); + resolve(42); + }).then(function(what) { return what+1; })); + }).then(function(value) { + is(value, 43, "Nested promise is executed and then == 43"); + runTest(); + }); +} + +function promiseWrongNestedPromise() { + new Promise(function(resolve, reject) { + resolve(new Promise(function(r, r2) { + ok(true, "Nested promise is executed"); + r(42); + })); + reject(42); + }).then(function(value) { + is(value, 42, "Nested promise is executed and then == 42"); + runTest(); + }, function(value) { + ok(false, "This is wrong"); + }); +} + +function promiseLoop() { + new Promise(function(resolve, reject) { + resolve(new Promise(function(r1, r2) { + ok(true, "Nested promise is executed"); + r1(new Promise(function(r1, r2) { + ok(true, "Nested nested promise is executed"); + r1(42); + })); + })); + }).then(function(value) { + is(value, 42, "Nested nested promise is executed and then == 42"); + runTest(); + }, function(value) { + ok(false, "This is wrong"); + }); +} + +function promiseStaticReject() { + var promise = Promise.reject(42).then(function(what) { + ok(false, "This should not be called"); + }, function(what) { + is(what, 42, "Value == 42"); + runTest(); + }); +} + +function promiseStaticResolve() { + var promise = Promise.resolve(42).then(function(what) { + is(what, 42, "Value == 42"); + runTest(); + }, function() { + ok(false, "This should not be called"); + }); +} + +function promiseResolveNestedPromise() { + var promise = Promise.resolve(new Promise(function(r, r2) { + ok(true, "Nested promise is executed"); + r(42); + }, function() { + ok(false, "This should not be called"); + })).then(function(what) { + is(what, 42, "Value == 42"); + runTest(); + }, function() { + ok(false, "This should not be called"); + }); +} + +function promiseRejectNoHandler() { + // This test only checks that the code that reports unhandled errors in the + // Promises implementation does not crash or leak. + var promise = new Promise(function(res, rej) { + noSuchMethod(); + }); + runTest(); +} + +function promiseUtilitiesDefined() { + ok(Promise.all, "Promise.all must be defined when Promise is enabled."); + ok(Promise.race, "Promise.race must be defined when Promise is enabled."); + runTest(); +} + +function promiseAllArray() { + var p = Promise.all([1, new Date(), Promise.resolve("firefox")]); + ok(p instanceof Promise, "Return value of Promise.all should be a Promise."); + p.then(function(values) { + ok(Array.isArray(values), "Resolved value should be an array."); + is(values.length, 3, "Resolved array length should match iterable's length."); + is(values[0], 1, "Array values should match."); + ok(values[1] instanceof Date, "Array values should match."); + is(values[2], "firefox", "Array values should match."); + runTest(); + }, function() { + ok(false, "Promise.all shouldn't fail when iterable has no rejected Promises."); + runTest(); + }); +} + +function promiseAllWaitsForAllPromises() { + var arr = [ + new Promise(function(resolve) { + setTimeout(resolve.bind(undefined, 1), 50); + }), + new Promise(function(resolve) { + setTimeout(resolve.bind(undefined, 2), 10); + }), + new Promise(function(resolve) { + setTimeout(resolve.bind(undefined, new Promise(function(resolve2) { + resolve2(3); + })), 10); + }), + new Promise(function(resolve) { + setTimeout(resolve.bind(undefined, 4), 20); + }) + ]; + + var p = Promise.all(arr); + p.then(function(values) { + ok(Array.isArray(values), "Resolved value should be an array."); + is(values.length, 4, "Resolved array length should match iterable's length."); + is(values[0], 1, "Array values should match."); + is(values[1], 2, "Array values should match."); + is(values[2], 3, "Array values should match."); + is(values[3], 4, "Array values should match."); + runTest(); + }, function() { + ok(false, "Promise.all shouldn't fail when iterable has no rejected Promises."); + runTest(); + }); +} + +function promiseAllRejectFails() { + var arr = [ + new Promise(function(resolve) { + setTimeout(resolve.bind(undefined, 1), 50); + }), + new Promise(function(resolve, reject) { + setTimeout(reject.bind(undefined, 2), 10); + }), + new Promise(function(resolve) { + setTimeout(resolve.bind(undefined, 3), 10); + }), + new Promise(function(resolve) { + setTimeout(resolve.bind(undefined, 4), 20); + }) + ]; + + var p = Promise.all(arr); + p.then(function(values) { + ok(false, "Promise.all shouldn't resolve when iterable has rejected Promises."); + runTest(); + }, function(e) { + ok(true, "Promise.all should reject when iterable has rejected Promises."); + is(e, 2, "Rejection value should match."); + runTest(); + }); +} + +function promiseRaceEmpty() { + var p = Promise.race([]); + ok(p instanceof Promise, "Should return a Promise."); + // An empty race never resolves! + runTest(); +} + +function promiseRaceValuesArray() { + var p = Promise.race([true, new Date(), 3]); + ok(p instanceof Promise, "Should return a Promise."); + p.then(function(winner) { + is(winner, true, "First value should win."); + runTest(); + }, function(err) { + ok(false, "Should not fail " + err + "."); + runTest(); + }); +} + +function promiseRacePromiseArray() { + var arr = [ + new Promise(function(resolve) { + resolve("first"); + }), + Promise.resolve("second"), + new Promise(function() {}), + new Promise(function(resolve) { + setTimeout(function() { + setTimeout(function() { + resolve("fourth"); + }, 0); + }, 0); + }), + ]; + + var p = Promise.race(arr); + p.then(function(winner) { + is(winner, "first", "First queued resolution should win the race."); + runTest(); + }); +} + +function promiseRaceReject() { + var p = Promise.race([ + Promise.reject(new Error("Fail bad!")), + new Promise(function(resolve) { + setTimeout(resolve, 0); + }) + ]); + + p.then(function() { + ok(false, "Should not resolve when winning Promise rejected."); + runTest(); + }, function(e) { + ok(true, "Should be rejected"); + ok(e instanceof Error, "Should reject with Error."); + ok(e.message == "Fail bad!", "Message should match."); + runTest(); + }); +} + +function promiseRaceThrow() { + var p = Promise.race([ + new Promise(function(resolve) { + nonExistent(); + }), + new Promise(function(resolve) { + setTimeout(resolve, 0); + }) + ]); + + p.then(function() { + ok(false, "Should not resolve when winning Promise had an error."); + runTest(); + }, function(e) { + ok(true, "Should be rejected"); + ok(e instanceof ReferenceError, "Should reject with ReferenceError for function nonExistent()."); + runTest(); + }); +} + +function promiseResolveArray() { + var p = Promise.resolve([1,2,3]); + ok(p instanceof Promise, "Should return a Promise."); + p.then(function(v) { + ok(Array.isArray(v), "Resolved value should be an Array"); + is(v.length, 3, "Length should match"); + is(v[0], 1, "Resolved value should match original"); + is(v[1], 2, "Resolved value should match original"); + is(v[2], 3, "Resolved value should match original"); + runTest(); + }); +} + +function promiseResolveThenable() { + var p = Promise.resolve({ then: function(onFulfill, onReject) { onFulfill(2); } }); + ok(p instanceof Promise, "Should cast to a Promise."); + p.then(function(v) { + is(v, 2, "Should resolve to 2."); + runTest(); + }, function(e) { + ok(false, "promiseResolveThenable should've resolved"); + runTest(); + }); +} + +function promiseResolvePromise() { + var original = Promise.resolve(true); + var cast = Promise.resolve(original); + + ok(cast instanceof Promise, "Should cast to a Promise."); + is(cast, original, "Should return original Promise."); + cast.then(function(v) { + is(v, true, "Should resolve to true."); + runTest(); + }); +} + +// Bug 1009569. +// Ensure that thenables are run on a clean stack asynchronously. +// Test case adopted from +// https://gist.github.com/getify/d64bb01751b50ed6b281#file-bug1-js. +function promiseResolveThenableCleanStack() { + function immed(s) { x++; s(); } + function incX(){ x++; } + + var x = 0; + var thenable = { then: immed }; + var results = []; + + var p = Promise.resolve(thenable).then(incX); + results.push(x); + + // check what happens after all "next cycle" steps + // have had a chance to complete + setTimeout(function(){ + // Result should be [0, 2] since `thenable` will be called async. + is(results[0], 0, "Expected thenable to be called asynchronously"); + // See Bug 1023547 comment 13 for why this check has to be gated on p. + p.then(function() { + results.push(x); + is(results[1], 2, "Expected thenable to be called asynchronously"); + runTest(); + }); + },1000); +} + +// Bug 1062323 +function promiseWrapperAsyncResolution() +{ + var p = new Promise(function(resolve, reject){ + resolve(); + }); + + var results = []; + var q = p.then(function () { + results.push("1-1"); + }).then(function () { + results.push("1-2"); + }).then(function () { + results.push("1-3"); + }); + + var r = p.then(function () { + results.push("2-1"); + }).then(function () { + results.push("2-2"); + }).then(function () { + results.push("2-3"); + }); + + Promise.all([q, r]).then(function() { + var match = results[0] == "1-1" && + results[1] == "2-1" && + results[2] == "1-2" && + results[3] == "2-2" && + results[4] == "1-3" && + results[5] == "2-3"; + ok(match, "Chained promises should resolve asynchronously."); + runTest(); + }, function() { + ok(false, "promiseWrapperAsyncResolution: One of the promises failed."); + runTest(); + }); +} + +var tests = [ + promiseResolve, + promiseReject, + promiseException, + promiseAsync_TimeoutResolveThen, + promiseAsync_ResolveTimeoutThen, + promiseAsync_ResolveThenTimeout, + promiseAsync_SyncXHRAndImportScripts, + promiseDoubleThen, + promiseThenException, + promiseThenCatchThen, + promiseRejectThenCatchThen, + promiseRejectThenCatchThen2, + promiseRejectThenCatchExceptionThen, + promiseThenCatchOrderingResolve, + promiseThenCatchOrderingReject, + promiseNestedPromise, + promiseNestedNestedPromise, + promiseWrongNestedPromise, + promiseLoop, + promiseStaticReject, + promiseStaticResolve, + promiseResolveNestedPromise, + promiseResolveNoArg, + promiseRejectNoArg, + + promiseThenNoArg, + promiseThenUndefinedResolveFunction, + promiseThenNullResolveFunction, + promiseCatchNoArg, + promiseRejectNoHandler, + + promiseUtilitiesDefined, + + promiseAllArray, + promiseAllWaitsForAllPromises, + promiseAllRejectFails, + + promiseRaceEmpty, + promiseRaceValuesArray, + promiseRacePromiseArray, + promiseRaceReject, + promiseRaceThrow, + + promiseResolveArray, + promiseResolveThenable, + promiseResolvePromise, + + promiseResolveThenableCleanStack, + + promiseWrapperAsyncResolution, +]; + +function runTest() { + if (!tests.length) { + postMessage({ type: 'finish' }); + return; + } + + var test = tests.shift(); + test(); +} + +onmessage = function() { + runTest(); +} diff --git a/dom/workers/test/recursion_worker.js b/dom/workers/test/recursion_worker.js new file mode 100644 index 000000000..1a61ec9d1 --- /dev/null +++ b/dom/workers/test/recursion_worker.js @@ -0,0 +1,46 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This function should never run on a too much recursion error. +onerror = function(event) { + postMessage(event.message); +}; + +// Pure JS recursion +function recurse() { + recurse(); +} + +// JS -> C++ -> JS -> C++ recursion +function recurse2() { + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + xhr.open("GET", "nonexistent.file"); + } + xhr.open("GET", "nonexistent.file"); +} + +var messageCount = 0; +onmessage = function(event) { + switch (++messageCount) { + case 2: + recurse2(); + + // An exception thrown from an event handler like xhr.onreadystatechange + // should not leave an exception pending in the code that generated the + // event. + postMessage("Done"); + return; + + case 1: + recurse(); + throw "Exception should have prevented us from getting here!"; + + default: + throw "Weird number of messages: " + messageCount; + } + + throw "Impossible to get here!"; +} diff --git a/dom/workers/test/recursiveOnerror_worker.js b/dom/workers/test/recursiveOnerror_worker.js new file mode 100644 index 000000000..e6656d68f --- /dev/null +++ b/dom/workers/test/recursiveOnerror_worker.js @@ -0,0 +1,11 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onerror = function(message, filename, lineno) { + throw new Error("2"); +}; + +onmessage = function(event) { + throw new Error("1"); +}; diff --git a/dom/workers/test/redirect_to_foreign.sjs b/dom/workers/test/redirect_to_foreign.sjs new file mode 100644 index 000000000..1a5e24a59 --- /dev/null +++ b/dom/workers/test/redirect_to_foreign.sjs @@ -0,0 +1,4 @@ +function handleRequest(request, response) { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", "http://example.org/tests/dom/workers/test/foreign.js"); +} diff --git a/dom/workers/test/referrer.sjs b/dom/workers/test/referrer.sjs new file mode 100644 index 000000000..dcfd1fe37 --- /dev/null +++ b/dom/workers/test/referrer.sjs @@ -0,0 +1,15 @@ +function handleRequest(request, response) +{ + if (request.queryString == "result") { + response.write(getState("referer")); + setState("referer", "INVALID"); + } else if (request.queryString == "worker") { + response.setHeader("Content-Type", "text/javascript", false); + response.write("onmessage = function() { postMessage(42); }"); + setState("referer", request.getHeader("referer")); + } else if (request.queryString == 'import') { + setState("referer", request.getHeader("referer")); + response.write("'hello world'"); + } +} + diff --git a/dom/workers/test/referrer_test_server.sjs b/dom/workers/test/referrer_test_server.sjs new file mode 100644 index 000000000..550fefb8a --- /dev/null +++ b/dom/workers/test/referrer_test_server.sjs @@ -0,0 +1,101 @@ +Components.utils.importGlobalProperties(["URLSearchParams"]); +const SJS = "referrer_test_server.sjs?"; +const SHARED_KEY = SJS; + +var SAME_ORIGIN = "https://example.com/tests/dom/workers/test/" + SJS; +var CROSS_ORIGIN = "https://test2.example.com/tests/dom/workers/test/" + SJS; +var DOWNGRADE = "http://example.com/tests/dom/workers/test/" + SJS; + +function createUrl(aRequestType, aPolicy) { + var searchParams = new URLSearchParams(); + searchParams.append("ACTION", "request-worker"); + searchParams.append("Referrer-Policy", aPolicy); + searchParams.append("TYPE", aRequestType); + + var url = SAME_ORIGIN; + + if (aRequestType === "cross-origin") { + url = CROSS_ORIGIN; + } else if (aRequestType === "downgrade") { + url = DOWNGRADE; + } + + return url + searchParams.toString(); +} +function createWorker (aRequestType, aPolicy) { + return ` + onmessage = function() { + fetch("${createUrl(aRequestType, aPolicy)}").then(function () { + postMessage(42); + close(); + }); + } + `; +} + +function handleRequest(request, response) { + var params = new URLSearchParams(request.queryString); + var policy = params.get("Referrer-Policy"); + var type = params.get("TYPE"); + var action = params.get("ACTION"); + response.setHeader("Content-Security-Policy", "default-src *", false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + + if (policy) { + response.setHeader("Referrer-Policy", policy, false); + } + + if (action === "test") { + response.setHeader("Content-Type", "text/javascript", false); + response.write(createWorker(type, policy)); + return; + } + + if (action === "resetState") { + setSharedState(SHARED_KEY, "{}"); + response.write(""); + return; + } + + if (action === "get-test-results") { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + response.write(getSharedState(SHARED_KEY)); + return; + } + + if (action === "request-worker") { + var result = getSharedState(SHARED_KEY); + result = result ? JSON.parse(result) : {}; + var referrerLevel = "none"; + var test = {}; + + if (request.hasHeader("Referer")) { + var referrer = request.getHeader("Referer"); + if (referrer.indexOf("referrer_test_server") > 0) { + referrerLevel = "full"; + } else if (referrer.indexOf("https://example.com") == 0) { + referrerLevel = "origin"; + } else { + // this is never supposed to happen + referrerLevel = "other-origin"; + } + test.referrer = referrer; + } else { + test.referrer = ""; + } + + test.policy = referrerLevel; + test.expected = policy; + + // test id equals type + "-" + policy + // Ex: same-origin-default + result[type + "-" + policy] = test; + setSharedState(SHARED_KEY, JSON.stringify(result)); + + response.write("'hello world'"); + return; + } +} + + diff --git a/dom/workers/test/referrer_worker.html b/dom/workers/test/referrer_worker.html new file mode 100644 index 000000000..5693fc340 --- /dev/null +++ b/dom/workers/test/referrer_worker.html @@ -0,0 +1,145 @@ +<!DOCTYPE html> +<html> +<head> +</head> +<body onload="tests.next();"> +<script type="text/javascript;version=1.7"> +const SJS = "referrer_test_server.sjs?"; +const BASE_URL = "https://example.com/tests/dom/workers/test/" + SJS; +const GET_RESULT = BASE_URL + 'ACTION=get-test-results'; +const RESET_STATE = BASE_URL + 'ACTION=resetState'; + +function ok(val, message) { + val = val ? "true" : "false"; + window.parent.postMessage("SimpleTest.ok(" + val + ", '" + message + "');", "*"); +} + +function info(val) { + window.parent.postMessage("SimpleTest.info(" + val + ");", "*"); +} + +function is(a, b, message) { + ok(a == b, message); +} + +function finish() { + // Let window.onerror have a chance to fire + setTimeout(function() { + setTimeout(function() { + tests.close(); + window.parent.postMessage("SimpleTest.finish();", "*"); + }, 0); + }, 0); +} + +var testCases = { + 'same-origin': { 'Referrer-Policy' : { 'default' : 'full', + 'origin' : 'origin', + 'origin-when-cross-origin' : 'full', + 'unsafe-url' : 'full', + 'same-origin' : 'full', + 'strict-origin' : 'origin', + 'strict-origin-when-cross-origin' : 'full', + 'no-referrer' : 'none', + 'unsafe-url, no-referrer' : 'none', + 'invalid' : 'full' }}, + + 'cross-origin': { 'Referrer-Policy' : { 'default' : 'full', + 'origin' : 'origin', + 'origin-when-cross-origin' : 'origin', + 'unsafe-url' : 'full', + 'same-origin' : 'none', + 'strict-origin' : 'origin', + 'strict-origin-when-cross-origin' : 'origin', + 'no-referrer' : 'none', + 'unsafe-url, no-referrer' : 'none', + 'invalid' : 'full' }}, + + // Downgrading in worker is blocked entirely without unblock option + // https://bugzilla.mozilla.org/show_bug.cgi?id=1198078#c17 + // Skip the downgrading test + /* 'downgrade': { 'Referrer-Policy' : { 'default' : 'full', + 'origin' : 'full', + 'origin-when-cross-origin"' : 'full', + 'unsafe-url' : 'full', + 'same-origin' : 'none', + 'strict-origin' : 'none', + 'strict-origin-when-cross-origin' : 'none', + 'no-referrer' : 'full', + 'unsafe-url, no-referrer' : 'none', + 'invalid' : 'full' }}, */ + + +}; + +var advance = function() { tests.next(); }; + +/** + * helper to perform an XHR + * to do checkIndividualResults and resetState + */ +function doXHR(aUrl, onSuccess, onFail) { + var xhr = new XMLHttpRequest({mozSystem: true}); + xhr.responseType = "json"; + xhr.onload = function () { + onSuccess(xhr); + }; + xhr.onerror = function () { + onFail(xhr); + }; + xhr.open('GET', aUrl, true); + xhr.send(null); +} + + +function resetState() { + doXHR(RESET_STATE, + advance, + function(xhr) { + ok(false, "error in reset state"); + finish(); + }); +} + +function checkIndividualResults(aType, aPolicy, aExpected) { + var onload = xhr => { + var results = xhr.response; + dump(JSON.stringify(xhr.response)); + // test id equals type + "-" + policy + // Ex: same-origin-default + var id = aType + "-" + aPolicy; + ok(id in results, id + " tests have to be performed."); + is(results[id].policy, aExpected, id + ' --- ' + results[id].policy + ' (' + results[id].referrer + ')'); + advance(); + }; + var onerror = xhr => { + ok(false, "Can't get results from the counter server."); + finish(); + }; + doXHR(GET_RESULT, onload, onerror); +} + +var tests = (function() { + + for (var type in testCases) { + for (var policy in testCases[type]['Referrer-Policy']) { + yield resetState(); + var searchParams = new URLSearchParams(); + searchParams.append("TYPE", type); + searchParams.append("ACTION", "test"); + searchParams.append("Referrer-Policy", policy); + var worker = new Worker(BASE_URL + searchParams.toString()); + worker.onmessage = function () { + advance(); + }; + yield worker.postMessage(42); + yield checkIndividualResults(type, policy, escape(testCases[type]['Referrer-Policy'][policy])); + } + } + + // complete. Be sure to yield so we don't call this twice. + yield finish(); +})(); +</script> +</body> +</html> diff --git a/dom/workers/test/rvals_worker.js b/dom/workers/test/rvals_worker.js new file mode 100644 index 000000000..51f9ae723 --- /dev/null +++ b/dom/workers/test/rvals_worker.js @@ -0,0 +1,13 @@ +onmessage = function(evt) { + postMessage(postMessage('ignore') == undefined); + + var id = setInterval(function() {}, 200); + postMessage(clearInterval(id) == undefined); + + id = setTimeout(function() {}, 200); + postMessage(clearTimeout(id) == undefined); + + postMessage(dump(42 + '\n') == undefined); + + postMessage('finished'); +} diff --git a/dom/workers/test/script_createFile.js b/dom/workers/test/script_createFile.js new file mode 100644 index 000000000..aee7df10e --- /dev/null +++ b/dom/workers/test/script_createFile.js @@ -0,0 +1,15 @@ +var { classes: Cc, interfaces: Ci, utils: Cu } = Components; +Cu.importGlobalProperties(["File", "Directory"]); + +addMessageListener("file.open", function (e) { + var tmpFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIDirectoryService) + .QueryInterface(Ci.nsIProperties) + .get('TmpD', Ci.nsIFile) + tmpFile.append('file.txt'); + tmpFile.createUnique(Components.interfaces.nsIFile.FILE_TYPE, 0o600); + + sendAsyncMessage("file.opened", { + data: File.createFromNsIFile(tmpFile) + }); +}); diff --git a/dom/workers/test/serviceworkers/activate_event_error_worker.js b/dom/workers/test/serviceworkers/activate_event_error_worker.js new file mode 100644 index 000000000..a5f159d35 --- /dev/null +++ b/dom/workers/test/serviceworkers/activate_event_error_worker.js @@ -0,0 +1,4 @@ +// Worker that errors on receiving an activate event. +onactivate = function(e) { + undefined.doSomething; +} diff --git a/dom/workers/test/serviceworkers/blocking_install_event_worker.js b/dom/workers/test/serviceworkers/blocking_install_event_worker.js new file mode 100644 index 000000000..0bc6f7b7f --- /dev/null +++ b/dom/workers/test/serviceworkers/blocking_install_event_worker.js @@ -0,0 +1,23 @@ +function postMessageToTest(msg) { + return clients.matchAll({ includeUncontrolled: true }) + .then(list => { + for (var client of list) { + if (client.url.endsWith('test_install_event_gc.html')) { + client.postMessage(msg); + break; + } + } + }); +} + +addEventListener('install', evt => { + // This must be a simple promise to trigger the CC failure. + evt.waitUntil(new Promise(function() { })); + postMessageToTest({ type: 'INSTALL_EVENT' }); +}); + +addEventListener('message', evt => { + if (evt.data.type === 'ping') { + postMessageToTest({ type: 'pong' }); + } +}); diff --git a/dom/workers/test/serviceworkers/browser.ini b/dom/workers/test/serviceworkers/browser.ini new file mode 100644 index 000000000..c0aae30d6 --- /dev/null +++ b/dom/workers/test/serviceworkers/browser.ini @@ -0,0 +1,10 @@ +[DEFAULT] +support-files = + browser_base_force_refresh.html + browser_cached_force_refresh.html + download/window.html + download/worker.js + force_refresh_browser_worker.js + +[browser_force_refresh.js] +[browser_download.js] diff --git a/dom/workers/test/serviceworkers/browser_base_force_refresh.html b/dom/workers/test/serviceworkers/browser_base_force_refresh.html new file mode 100644 index 000000000..1b0d2defe --- /dev/null +++ b/dom/workers/test/serviceworkers/browser_base_force_refresh.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> +</head> +<body> +<script type="text/javascript"> +addEventListener('load', function(event) { + navigator.serviceWorker.register('force_refresh_browser_worker.js').then(function(swr) { + if (!swr) { + return; + } + var custom = new Event('base-register', { bubbles: true }); + document.dispatchEvent(custom); + }); + + navigator.serviceWorker.ready.then(function() { + var custom = new Event('base-sw-ready', { bubbles: true }); + document.dispatchEvent(custom); + }); + + var custom = new Event('base-load', { bubbles: true }); + document.dispatchEvent(custom); +}); +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/browser_cached_force_refresh.html b/dom/workers/test/serviceworkers/browser_cached_force_refresh.html new file mode 100644 index 000000000..33bd8cdaa --- /dev/null +++ b/dom/workers/test/serviceworkers/browser_cached_force_refresh.html @@ -0,0 +1,64 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> +</head> +<body> +<script type="text/javascript"> +function ok(exp, msg) { + if (!exp) { + throw(msg); + } +} + +function is(actual, expected, msg) { + if (actual !== expected) { + throw('got "' + actual + '", but expected "' + expected + '" - ' + msg); + } +} + +function fail(err) { + var custom = new CustomEvent('cached-failure', { + bubbles: true, + detail: err + }); + document.dispatchEvent(custom); +} + +function getUncontrolledClients(sw) { + return new Promise(function(resolve, reject) { + navigator.serviceWorker.addEventListener('message', function onMsg(evt) { + if (evt.data.type === 'CLIENTS') { + navigator.serviceWorker.removeEventListener('message', onMsg); + resolve(evt.data.detail); + } + }); + sw.postMessage({ type: 'GET_UNCONTROLLED_CLIENTS' }) + }); +} + +addEventListener('load', function(event) { + if (!navigator.serviceWorker.controller) { + return fail(window.location.href + ' is not controlled!'); + } + + getUncontrolledClients(navigator.serviceWorker.controller) + .then(function(clientList) { + is(clientList.length, 1, 'should only have one client'); + is(clientList[0].url, window.location.href, + 'client url should match current window'); + is(clientList[0].frameType, 'top-level', + 'client should be a top-level window'); + var custom = new Event('cached-load', { bubbles: true }); + document.dispatchEvent(custom); + }) + .catch(function(err) { + fail(err); + }); +}); +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/browser_download.js b/dom/workers/test/serviceworkers/browser_download.js new file mode 100644 index 000000000..bd4da10c9 --- /dev/null +++ b/dom/workers/test/serviceworkers/browser_download.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import('resource://gre/modules/Services.jsm'); +var Downloads = Cu.import("resource://gre/modules/Downloads.jsm", {}).Downloads; +var DownloadsCommon = Cu.import("resource:///modules/DownloadsCommon.jsm", {}).DownloadsCommon; +Cu.import('resource://gre/modules/NetUtil.jsm'); + +var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", + "http://mochi.test:8888/") + +function getFile(aFilename) { + if (aFilename.startsWith('file:')) { + var url = NetUtil.newURI(aFilename).QueryInterface(Ci.nsIFileURL); + return url.file.clone(); + } + + var file = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile); + file.initWithPath(aFilename); + return file; +} + +function windowObserver(win, topic) { + if (topic !== 'domwindowopened') { + return; + } + + win.addEventListener('load', function onLoadWindow() { + win.removeEventListener('load', onLoadWindow, false); + if (win.document.documentURI === + 'chrome://mozapps/content/downloads/unknownContentType.xul') { + executeSoon(function() { + var button = win.document.documentElement.getButton('accept'); + button.disabled = false; + win.document.documentElement.acceptDialog(); + }); + } + }, false); +} + +function test() { + waitForExplicitFinish(); + + Services.ww.registerNotification(windowObserver); + + SpecialPowers.pushPrefEnv({'set': [['dom.serviceWorkers.enabled', true], + ['dom.serviceWorkers.exemptFromPerDomainMax', true], + ['dom.serviceWorkers.testing.enabled', true]]}, + function() { + var url = gTestRoot + 'download/window.html'; + var tab = gBrowser.addTab(); + gBrowser.selectedTab = tab; + + Downloads.getList(Downloads.ALL).then(function(downloadList) { + var downloadListener; + + function downloadVerifier(aDownload) { + if (aDownload.succeeded) { + var file = getFile(aDownload.target.path); + ok(file.exists(), 'download completed'); + is(file.fileSize, 33, 'downloaded file has correct size'); + file.remove(false); + DownloadsCommon.removeAndFinalizeDownload(aDownload); + + downloadList.removeView(downloadListener); + gBrowser.removeTab(tab); + Services.ww.unregisterNotification(windowObserver); + + executeSoon(finish); + } + } + + downloadListener = { + onDownloadAdded: downloadVerifier, + onDownloadChanged: downloadVerifier + }; + + return downloadList.addView(downloadListener); + }).then(function() { + gBrowser.loadURI(url); + }); + }); +} diff --git a/dom/workers/test/serviceworkers/browser_force_refresh.js b/dom/workers/test/serviceworkers/browser_force_refresh.js new file mode 100644 index 000000000..a2c9c871c --- /dev/null +++ b/dom/workers/test/serviceworkers/browser_force_refresh.js @@ -0,0 +1,91 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gTestRoot = getRootDirectory(gTestPath).replace("chrome://mochitests/content/", + "http://mochi.test:8888/") + +function refresh() { + EventUtils.synthesizeKey('R', { accelKey: true }); +} + +function forceRefresh() { + EventUtils.synthesizeKey('R', { accelKey: true, shiftKey: true }); +} + +function frameScript() { + function eventHandler(event) { + sendAsyncMessage("test:event", {type: event.type, detail: event.detail}); + } + + // These are tab-local, so no need to unregister them. + addEventListener('base-load', eventHandler, true, true); + addEventListener('base-register', eventHandler, true, true); + addEventListener('base-sw-ready', eventHandler, true, true); + addEventListener('cached-load', eventHandler, true, true); + addEventListener('cached-failure', eventHandler, true, true); +} + +function test() { + waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({'set': [['dom.serviceWorkers.enabled', true], + ['dom.serviceWorkers.exemptFromPerDomainMax', true], + ['dom.serviceWorkers.testing.enabled', true], + ['dom.caches.enabled', true], + ['browser.cache.disk.enable', false], + ['browser.cache.memory.enable', false]]}, + function() { + var url = gTestRoot + 'browser_base_force_refresh.html'; + var tab = gBrowser.addTab(); + gBrowser.selectedTab = tab; + + tab.linkedBrowser.messageManager.loadFrameScript("data:,(" + encodeURIComponent(frameScript) + ")()", true); + gBrowser.loadURI(url); + + function done() { + tab.linkedBrowser.messageManager.removeMessageListener("test:event", eventHandler); + + gBrowser.removeTab(tab); + executeSoon(finish); + } + + var maxCacheLoadCount = 3; + var cachedLoadCount = 0; + var baseLoadCount = 0; + + function eventHandler(msg) { + if (msg.data.type === 'base-load') { + baseLoadCount += 1; + if (cachedLoadCount === maxCacheLoadCount) { + is(baseLoadCount, 2, 'cached load should occur before second base load'); + return done(); + } + if (baseLoadCount !== 1) { + ok(false, 'base load without cached load should only occur once'); + return done(); + } + } else if (msg.data.type === 'base-register') { + ok(!cachedLoadCount, 'cached load should not occur before base register'); + is(baseLoadCount, 1, 'register should occur after first base load'); + } else if (msg.data.type === 'base-sw-ready') { + ok(!cachedLoadCount, 'cached load should not occur before base ready'); + is(baseLoadCount, 1, 'ready should occur after first base load'); + refresh(); + } else if (msg.data.type === 'cached-load') { + ok(cachedLoadCount < maxCacheLoadCount, 'cached load should not occur too many times'); + is(baseLoadCount, 1, 'cache load occur after first base load'); + cachedLoadCount += 1; + if (cachedLoadCount < maxCacheLoadCount) { + return refresh(); + } + forceRefresh(); + } else if (msg.data.type === 'cached-failure') { + ok(false, 'failure: ' + msg.data.detail); + done(); + } + + return; + } + + tab.linkedBrowser.messageManager.addMessageListener("test:event", eventHandler); + }); +} diff --git a/dom/workers/test/serviceworkers/bug1151916_driver.html b/dom/workers/test/serviceworkers/bug1151916_driver.html new file mode 100644 index 000000000..e540ad239 --- /dev/null +++ b/dom/workers/test/serviceworkers/bug1151916_driver.html @@ -0,0 +1,53 @@ +<html> + <body> + <script language="javascript"> + function fail(msg) { + window.parent.postMessage({ status: "failed", message: msg }, "*"); + } + + function success(msg) { + window.parent.postMessage({ status: "success", message: msg }, "*"); + } + + if (!window.parent) { + dump("This file must be embedded in an iframe!"); + } + + navigator.serviceWorker.getRegistration() + .then(function(reg) { + if (!reg) { + navigator.serviceWorker.ready.then(function(reg) { + if (reg.active.state == "activating") { + reg.active.onstatechange = function(e) { + reg.active.onstatechange = null; + if (reg.active.state == "activated") { + success("Registered and activated"); + } + } + } else { + success("Registered and activated"); + } + }); + navigator.serviceWorker.register("bug1151916_worker.js", + { scope: "." }); + } else { + // Simply force the sw to load a resource and touch self.caches. + if (!reg.active) { + fail("no-active-worker"); + return; + } + + fetch("madeup.txt").then(function(res) { + res.text().then(function(v) { + if (v == "Hi there") { + success("Loaded from cache"); + } else { + fail("Response text did not match"); + } + }, fail); + }, fail); + } + }, fail); + </script> + </body> +</html> diff --git a/dom/workers/test/serviceworkers/bug1151916_worker.js b/dom/workers/test/serviceworkers/bug1151916_worker.js new file mode 100644 index 000000000..06585e8e7 --- /dev/null +++ b/dom/workers/test/serviceworkers/bug1151916_worker.js @@ -0,0 +1,13 @@ +onactivate = function(e) { + e.waitUntil(self.caches.open("default-cache").then(function(cache) { + var response = new Response("Hi there"); + return cache.put("madeup.txt", response); + })); +} + +onfetch = function(e) { + if (e.request.url.match(/madeup.txt$/)) { + var p = self.caches.match("madeup.txt", { cacheName: "default-cache" }); + e.respondWith(p); + } +} diff --git a/dom/workers/test/serviceworkers/bug1240436_worker.js b/dom/workers/test/serviceworkers/bug1240436_worker.js new file mode 100644 index 000000000..5a588aedf --- /dev/null +++ b/dom/workers/test/serviceworkers/bug1240436_worker.js @@ -0,0 +1,2 @@ +// a contains a ZERO WIDTH JOINER (0x200D) +var a = "â€";
\ No newline at end of file diff --git a/dom/workers/test/serviceworkers/chrome.ini b/dom/workers/test/serviceworkers/chrome.ini new file mode 100644 index 000000000..e064e7fd0 --- /dev/null +++ b/dom/workers/test/serviceworkers/chrome.ini @@ -0,0 +1,16 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + chrome_helpers.js + empty.js + serviceworker.html + serviceworkerinfo_iframe.html + serviceworkermanager_iframe.html + serviceworkerregistrationinfo_iframe.html + worker.js + worker2.js + +[test_privateBrowsing.html] +[test_serviceworkerinfo.xul] +[test_serviceworkermanager.xul] +[test_serviceworkerregistrationinfo.xul] diff --git a/dom/workers/test/serviceworkers/chrome_helpers.js b/dom/workers/test/serviceworkers/chrome_helpers.js new file mode 100644 index 000000000..a438333e2 --- /dev/null +++ b/dom/workers/test/serviceworkers/chrome_helpers.js @@ -0,0 +1,74 @@ +let { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/Task.jsm"); + +let swm = Cc["@mozilla.org/serviceworkers/manager;1"]. + getService(Ci.nsIServiceWorkerManager); + +let EXAMPLE_URL = "https://example.com/chrome/dom/workers/test/serviceworkers/"; + +function waitForIframeLoad(iframe) { + return new Promise(function (resolve) { + iframe.onload = resolve; + }); +} + +function waitForRegister(scope, callback) { + return new Promise(function (resolve) { + let listener = { + onRegister: function (registration) { + if (registration.scope !== scope) { + return; + } + swm.removeListener(listener); + resolve(callback ? callback(registration) : registration); + } + }; + swm.addListener(listener); + }); +} + +function waitForUnregister(scope) { + return new Promise(function (resolve) { + let listener = { + onUnregister: function (registration) { + if (registration.scope !== scope) { + return; + } + swm.removeListener(listener); + resolve(registration); + } + }; + swm.addListener(listener); + }); +} + +function waitForServiceWorkerRegistrationChange(registration, callback) { + return new Promise(function (resolve) { + let listener = { + onChange: function () { + registration.removeListener(listener); + if (callback) { + callback(); + } + resolve(callback ? callback() : undefined); + } + }; + registration.addListener(listener); + }); +} + +function waitForServiceWorkerShutdown() { + return new Promise(function (resolve) { + let observer = { + observe: function (subject, topic, data) { + if (topic !== "service-worker-shutdown") { + return; + } + SpecialPowers.removeObserver(observer, "service-worker-shutdown"); + resolve(); + } + }; + SpecialPowers.addObserver(observer, "service-worker-shutdown", false); + }); +} diff --git a/dom/workers/test/serviceworkers/claim_clients/client.html b/dom/workers/test/serviceworkers/claim_clients/client.html new file mode 100644 index 000000000..eecfb294e --- /dev/null +++ b/dom/workers/test/serviceworkers/claim_clients/client.html @@ -0,0 +1,44 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1130684 - claim client </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + info("This page shouldn't be launched directly!"); + } + + window.onload = function() { + parent.postMessage("READY", "*"); + } + + navigator.serviceWorker.oncontrollerchange = function() { + parent.postMessage({ + event: "controllerchange", + controller: (navigator.serviceWorker.controller !== null) + }, "*"); + } + + navigator.serviceWorker.onmessage = function(e) { + parent.postMessage({ + event: "message", + data: e.data + }, "*"); + } + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/claim_fetch_worker.js b/dom/workers/test/serviceworkers/claim_fetch_worker.js new file mode 100644 index 000000000..ea62c37b8 --- /dev/null +++ b/dom/workers/test/serviceworkers/claim_fetch_worker.js @@ -0,0 +1,12 @@ +onfetch = function(e) { + if (e.request.url.indexOf("service_worker_controlled") >= 0) { + // pass through + e.respondWith(fetch(e.request)); + } else { + e.respondWith(new Response("Fetch was intercepted")); + } +} + +onmessage = function(e) { + clients.claim(); +} diff --git a/dom/workers/test/serviceworkers/claim_oninstall_worker.js b/dom/workers/test/serviceworkers/claim_oninstall_worker.js new file mode 100644 index 000000000..269afa721 --- /dev/null +++ b/dom/workers/test/serviceworkers/claim_oninstall_worker.js @@ -0,0 +1,7 @@ +oninstall = function(e) { + var claimFailedPromise = new Promise(function(resolve, reject) { + clients.claim().then(reject, () => resolve()); + }); + + e.waitUntil(claimFailedPromise); +} diff --git a/dom/workers/test/serviceworkers/claim_worker_1.js b/dom/workers/test/serviceworkers/claim_worker_1.js new file mode 100644 index 000000000..e5f6392d3 --- /dev/null +++ b/dom/workers/test/serviceworkers/claim_worker_1.js @@ -0,0 +1,28 @@ +onactivate = function(e) { + var result = { + resolve_value: false, + match_count_before: -1, + match_count_after: -1, + message: "claim_worker_1" + }; + + self.clients.matchAll().then(function(matched) { + // should be 0 + result.match_count_before = matched.length; + }).then(function() { + var claimPromise = self.clients.claim().then(function(ret) { + result.resolve_value = ret; + }); + + return claimPromise.then(self.clients.matchAll().then(function(matched) { + // should be 2 + result.match_count_after = matched.length; + for (i = 0; i < matched.length; i++) { + matched[i].postMessage(result); + } + if (result.match_count_after !== 2) { + dump("ERROR: claim_worker_1 failed to capture clients.\n"); + } + })); + }); +} diff --git a/dom/workers/test/serviceworkers/claim_worker_2.js b/dom/workers/test/serviceworkers/claim_worker_2.js new file mode 100644 index 000000000..be8281d34 --- /dev/null +++ b/dom/workers/test/serviceworkers/claim_worker_2.js @@ -0,0 +1,27 @@ +onactivate = function(e) { + var result = { + resolve_value: false, + match_count_before: -1, + match_count_after: -1, + message: "claim_worker_2" + }; + + self.clients.matchAll().then(function(matched) { + // should be 0 + result.match_count_before = matched.length; + }).then(function() { + var claimPromise = self.clients.claim().then(function(ret) { + result.resolve_value = ret; + }); + + return claimPromise.then(self.clients.matchAll().then(function(matched) { + // should be 1 + result.match_count_after = matched.length; + if (result.match_count_after === 1) { + matched[0].postMessage(result); + } else { + dump("ERROR: claim_worker_2 failed to capture clients.\n"); + } + })); + }); +} diff --git a/dom/workers/test/serviceworkers/close_test.js b/dom/workers/test/serviceworkers/close_test.js new file mode 100644 index 000000000..6138d6421 --- /dev/null +++ b/dom/workers/test/serviceworkers/close_test.js @@ -0,0 +1,19 @@ +function ok(v, msg) { + client.postMessage({status: "ok", result: !!v, message: msg}); +} + +var client; +onmessage = function(e) { + if (e.data.message == "start") { + self.clients.matchAll().then(function(clients) { + client = clients[0]; + try { + close(); + ok(false, "close() should throw"); + } catch (e) { + ok(e.name === "InvalidAccessError", "close() should throw InvalidAccessError"); + } + client.postMessage({status: "done"}); + }); + } +} diff --git a/dom/workers/test/serviceworkers/controller/index.html b/dom/workers/test/serviceworkers/controller/index.html new file mode 100644 index 000000000..740e24f15 --- /dev/null +++ b/dom/workers/test/serviceworkers/controller/index.html @@ -0,0 +1,74 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 94048 - test install event.</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + // Make sure to use good, unique messages, since the actual expression will not show up in test results. + function my_ok(result, msg) { + parent.postMessage({status: "ok", result: result, message: msg}, "*"); + } + + function finish() { + parent.postMessage({status: "done"}, "*"); + } + + navigator.serviceWorker.ready.then(function(swr) { + my_ok(swr.scope.match(/serviceworkers\/control$/), + "This page should be controlled by upper level registration"); + my_ok(swr.installing == undefined, + "Upper level registration should not have a installing worker."); + if (navigator.serviceWorker.controller) { + // We are controlled. + // Register a new worker for this sub-scope. After that, controller should still be for upper level, but active should change to be this scope's. + navigator.serviceWorker.register("../worker2.js", { scope: "./" }).then(function(e) { + my_ok("installing" in e, "ServiceWorkerRegistration.installing exists."); + my_ok(e.installing instanceof ServiceWorker, "ServiceWorkerRegistration.installing is a ServiceWorker."); + + my_ok("waiting" in e, "ServiceWorkerRegistration.waiting exists."); + my_ok("active" in e, "ServiceWorkerRegistration.active exists."); + + my_ok(e.installing && + e.installing.scriptURL.match(/worker2.js$/), + "Installing is serviceworker/controller"); + + my_ok("scope" in e, "ServiceWorkerRegistration.scope exists."); + my_ok(e.scope.match(/serviceworkers\/controller\/$/), "Scope is serviceworker/controller " + e.scope); + + my_ok("unregister" in e, "ServiceWorkerRegistration.unregister exists."); + + my_ok(navigator.serviceWorker.controller.scriptURL.match(/worker\.js$/), + "Controller is still worker.js"); + + e.unregister().then(function(result) { + my_ok(result, "Unregistering the SW should succeed"); + finish(); + }, function(e) { + dump("Error unregistering the SW: " + e + "\n"); + }); + }); + } else { + my_ok(false, "Should've been controlled!"); + finish(); + } + }).catch(function(e) { + my_ok(false, "Some test threw an error " + e); + finish(); + }); +</script> +</pre> +</body> +</html> + + diff --git a/dom/workers/test/serviceworkers/create_another_sharedWorker.html b/dom/workers/test/serviceworkers/create_another_sharedWorker.html new file mode 100644 index 000000000..f49194fa5 --- /dev/null +++ b/dom/workers/test/serviceworkers/create_another_sharedWorker.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<title>Shared workers: create antoehr sharedworekr client</title> +<pre id=log>Hello World</pre> +<script> + var worker = new SharedWorker('sharedWorker_fetch.js'); +</script> diff --git a/dom/workers/test/serviceworkers/download/window.html b/dom/workers/test/serviceworkers/download/window.html new file mode 100644 index 000000000..7d7893e0e --- /dev/null +++ b/dom/workers/test/serviceworkers/download/window.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> +</head> +<body> +<script type="text/javascript"> + +function wait_until_controlled() { + return new Promise(function(resolve) { + if (navigator.serviceWorker.controller) { + return resolve(); + } + navigator.serviceWorker.addEventListener('controllerchange', function onController() { + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.removeEventListener('controllerchange', onController); + return resolve(); + } + }); + }); +} +addEventListener('load', function(event) { + var registration; + navigator.serviceWorker.register('worker.js').then(function(swr) { + registration = swr; + + // While the iframe below is a navigation, we still wait until we are + // controlled here. We want an active client to hold the service worker + // alive since it calls unregister() on itself. + return wait_until_controlled(); + + }).then(function() { + var frame = document.createElement('iframe'); + document.body.appendChild(frame); + frame.src = 'fake_download'; + + // The service worker is unregistered in the fetch event. The window and + // frame are cleaned up from the browser chrome script. + }); +}); +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/download/worker.js b/dom/workers/test/serviceworkers/download/worker.js new file mode 100644 index 000000000..fe46d1a3b --- /dev/null +++ b/dom/workers/test/serviceworkers/download/worker.js @@ -0,0 +1,30 @@ +addEventListener('install', function(evt) { + evt.waitUntil(self.skipWaiting()); +}); + +addEventListener('activate', function(evt) { + // We claim the current clients in order to ensure that we have an + // active client when we call unregister in the fetch handler. Otherwise + // the unregister() can kill the current worker before returning a + // response. + evt.waitUntil(clients.claim()); +}); + +addEventListener('fetch', function(evt) { + // This worker may live long enough to receive a fetch event from the next + // test. Just pass such requests through to the network. + if (evt.request.url.indexOf('fake_download') === -1) { + return; + } + + // We should only get a single download fetch event. Automatically unregister. + evt.respondWith(registration.unregister().then(function() { + return new Response('service worker generated download', { + headers: { + 'Content-Disposition': 'attachment; filename="fake_download.bin"', + // fake encoding header that should have no effect + 'Content-Encoding': 'gzip', + } + }); + })); +}); diff --git a/dom/workers/test/serviceworkers/empty.js b/dom/workers/test/serviceworkers/empty.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/dom/workers/test/serviceworkers/empty.js diff --git a/dom/workers/test/serviceworkers/error_reporting_helpers.js b/dom/workers/test/serviceworkers/error_reporting_helpers.js new file mode 100644 index 000000000..fbc4ca6fc --- /dev/null +++ b/dom/workers/test/serviceworkers/error_reporting_helpers.js @@ -0,0 +1,68 @@ +"use strict"; + +/** + * Helpers for use in tests that want to verify that localized error messages + * are logged during the test. Because most of our errors (ex: + * ServiceWorkerManager) generate nsIScriptError instances with flattened + * strings (the interpolated arguments aren't kept around), we load the string + * bundle and use it to derive the exact string message we expect for the given + * payload. + **/ + +let stringBundleService = + SpecialPowers.Cc["@mozilla.org/intl/stringbundle;1"] + .getService(SpecialPowers.Ci.nsIStringBundleService); +let localizer = + stringBundleService.createBundle("chrome://global/locale/dom/dom.properties"); + +/** + * Start monitoring the console for the given localized error message string(s) + * with the given arguments to be logged. Call before running code that will + * generate the console message. Pair with a call to + * `wait_for_expected_message` invoked after the message should have been + * generated. + * + * Multiple error messages can be expected, just repeat the msgId and args + * argument pair as needed. + * + * @param {String} msgId + * The localization message identifier used in the properties file. + * @param {String[]} args + * The list of formatting arguments we expect the error to be generated with. + * @return {Object} Promise/handle to pass to wait_for_expected_message. + */ +function expect_console_message(/* msgId, args, ... */) { + let expectations = []; + // process repeated paired arguments of: msgId, args + for (let i = 0; i < arguments.length; i += 2) { + let msgId = arguments[i]; + let args = arguments[i + 1]; + expectations.push({ + errorMessage: localizer.formatStringFromName(msgId, args, args.length) + }); + } + return new Promise(resolve => { + SimpleTest.monitorConsole(resolve, expectations); + }); +} +let expect_console_messages = expect_console_message; + +/** + * Stop monitoring the console, returning a Promise that will be resolved when + * the sentinel console message sent through the async data path has been + * received. The Promise will not reject on failure; instead a mochitest + * failure will have been generated by ok(false)/equivalent by the time it is + * resolved. + */ +function wait_for_expected_message(expectedPromise) { + SimpleTest.endMonitorConsole(); + return expectedPromise; +} + +/** + * Derive an absolute URL string from a relative URL to simplify error message + * argument generation. + */ +function make_absolute_url(relUrl) { + return new URL(relUrl, window.location).href; +} diff --git a/dom/workers/test/serviceworkers/eval_worker.js b/dom/workers/test/serviceworkers/eval_worker.js new file mode 100644 index 000000000..c60f2f637 --- /dev/null +++ b/dom/workers/test/serviceworkers/eval_worker.js @@ -0,0 +1 @@ +eval('1+1'); diff --git a/dom/workers/test/serviceworkers/eventsource/eventsource.resource b/dom/workers/test/serviceworkers/eventsource/eventsource.resource new file mode 100644 index 000000000..eb62cbd4c --- /dev/null +++ b/dom/workers/test/serviceworkers/eventsource/eventsource.resource @@ -0,0 +1,22 @@ +:this file must be enconded in utf8 +:and its Content-Type must be equal to text/event-stream + +retry:500 +data: 2 +unknow: unknow + +event: other_event_name +retry:500 +data: 2 +unknow: unknow + +event: click +retry:500 + +event: blur +retry:500 + +event:keypress +retry:500 + + diff --git a/dom/workers/test/serviceworkers/eventsource/eventsource.resource^headers^ b/dom/workers/test/serviceworkers/eventsource/eventsource.resource^headers^ new file mode 100644 index 000000000..5b88be7c3 --- /dev/null +++ b/dom/workers/test/serviceworkers/eventsource/eventsource.resource^headers^ @@ -0,0 +1,3 @@ +Content-Type: text/event-stream +Cache-Control: no-cache, must-revalidate +Access-Control-Allow-Origin: * diff --git a/dom/workers/test/serviceworkers/eventsource/eventsource_cors_response.html b/dom/workers/test/serviceworkers/eventsource/eventsource_cors_response.html new file mode 100644 index 000000000..7c6f7302f --- /dev/null +++ b/dom/workers/test/serviceworkers/eventsource/eventsource_cors_response.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script type="text/javascript"> + + var prefix = "http://mochi.test:8888/tests/dom/workers/test/serviceworkers/eventsource/"; + + function ok(aCondition, aMessage) { + parent.postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}, "*"); + } + + function doUnregister() { + navigator.serviceWorker.getRegistration().then(swr => { + swr.unregister().then(function(result) { + ok(result, "Unregister should return true."); + parent.postMessage({status: "callback", data: "done"}, "*"); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e); + }); + }); + } + + function doEventSource() { + var source = new EventSource(prefix + "eventsource.resource"); + source.onmessage = function(e) { + source.onmessage = null; + source.close(); + ok(true, "EventSource should work with cors responses"); + doUnregister(); + }; + source.onerror = function(error) { + source.onerror = null; + source.close(); + ok(false, "Something went wrong"); + }; + } + + function onLoad() { + if (!parent) { + dump("eventsource/eventsource_cors_response.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function onMessage(e) { + if (e.data.status === "callback") { + switch(e.data.data) { + case "eventsource": + doEventSource(); + window.removeEventListener("message", onMessage); + break; + default: + ok(false, "Something went wrong") + break + } + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage({status: "callback", data: "ready"}, "*"); + }); + + navigator.serviceWorker.addEventListener("message", function(event) { + parent.postMessage(event.data, "*"); + }); + } + + </script> +</head> +<body onload="onLoad()"> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/eventsource/eventsource_cors_response_intercept_worker.js b/dom/workers/test/serviceworkers/eventsource/eventsource_cors_response_intercept_worker.js new file mode 100644 index 000000000..579e9f568 --- /dev/null +++ b/dom/workers/test/serviceworkers/eventsource/eventsource_cors_response_intercept_worker.js @@ -0,0 +1,20 @@ +// Cross origin request +var prefix = 'http://example.com/tests/dom/workers/test/serviceworkers/eventsource/'; + +self.importScripts('eventsource_worker_helper.js'); + +self.addEventListener('fetch', function (event) { + var request = event.request; + var url = new URL(request.url); + + if (url.pathname !== '/tests/dom/workers/test/serviceworkers/eventsource/eventsource.resource') { + return; + } + + ok(request.mode === 'cors', 'EventSource should make a CORS request'); + ok(request.cache === 'no-store', 'EventSource should make a no-store request'); + var fetchRequest = new Request(prefix + 'eventsource.resource', { mode: 'cors'}); + event.respondWith(fetch(fetchRequest).then((fetchResponse) => { + return fetchResponse; + })); +}); diff --git a/dom/workers/test/serviceworkers/eventsource/eventsource_mixed_content_cors_response.html b/dom/workers/test/serviceworkers/eventsource/eventsource_mixed_content_cors_response.html new file mode 100644 index 000000000..f6ae0d96f --- /dev/null +++ b/dom/workers/test/serviceworkers/eventsource/eventsource_mixed_content_cors_response.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script type="text/javascript"> + + var prefix = "https://example.com/tests/dom/workers/test/serviceworkers/eventsource/"; + + function ok(aCondition, aMessage) { + parent.postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}, "*"); + } + + function doUnregister() { + navigator.serviceWorker.getRegistration().then(swr => { + swr.unregister().then(function(result) { + ok(result, "Unregister should return true."); + parent.postMessage({status: "callback", data: "done"}, "*"); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e); + }); + }); + } + + function doEventSource() { + var source = new EventSource(prefix + "eventsource.resource"); + source.onmessage = function(e) { + source.onmessage = null; + source.close(); + ok(false, "Something went wrong"); + }; + source.onerror = function(error) { + source.onerror = null; + source.close(); + ok(true, "EventSource should not work with mixed content cors responses"); + doUnregister(); + }; + } + + function onLoad() { + if (!parent) { + dump("eventsource/eventsource_cors_response.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function onMessage(e) { + if (e.data.status === "callback") { + switch(e.data.data) { + case "eventsource": + doEventSource(); + window.removeEventListener("message", onMessage); + break; + default: + ok(false, "Something went wrong") + break + } + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage({status: "callback", data: "ready"}, "*"); + }); + + navigator.serviceWorker.addEventListener("message", function(event) { + parent.postMessage(event.data, "*"); + }); + } + + </script> +</head> +<body onload="onLoad()"> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/eventsource/eventsource_mixed_content_cors_response_intercept_worker.js b/dom/workers/test/serviceworkers/eventsource/eventsource_mixed_content_cors_response_intercept_worker.js new file mode 100644 index 000000000..187d0bc6f --- /dev/null +++ b/dom/workers/test/serviceworkers/eventsource/eventsource_mixed_content_cors_response_intercept_worker.js @@ -0,0 +1,19 @@ +var prefix = 'http://example.com/tests/dom/workers/test/serviceworkers/eventsource/'; + +self.importScripts('eventsource_worker_helper.js'); + +self.addEventListener('fetch', function (event) { + var request = event.request; + var url = new URL(request.url); + + if (url.pathname !== '/tests/dom/workers/test/serviceworkers/eventsource/eventsource.resource') { + return; + } + + ok(request.mode === 'cors', 'EventSource should make a CORS request'); + ok(request.cache === 'no-store', 'EventSource should make a no-store request'); + var fetchRequest = new Request(prefix + 'eventsource.resource', { mode: 'cors'}); + event.respondWith(fetch(fetchRequest).then((fetchResponse) => { + return fetchResponse; + })); +}); diff --git a/dom/workers/test/serviceworkers/eventsource/eventsource_opaque_response.html b/dom/workers/test/serviceworkers/eventsource/eventsource_opaque_response.html new file mode 100644 index 000000000..f92811e63 --- /dev/null +++ b/dom/workers/test/serviceworkers/eventsource/eventsource_opaque_response.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script type="text/javascript"> + + var prefix = "http://mochi.test:8888/tests/dom/workers/test/serviceworkers/eventsource/"; + + function ok(aCondition, aMessage) { + parent.postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}, "*"); + } + + function doUnregister() { + navigator.serviceWorker.getRegistration().then(swr => { + swr.unregister().then(function(result) { + ok(result, "Unregister should return true."); + parent.postMessage({status: "callback", data: "done"}, "*"); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e); + }); + }); + } + + function doEventSource() { + var source = new EventSource(prefix + "eventsource.resource"); + source.onmessage = function(e) { + source.onmessage = null; + source.close(); + ok(false, "Something went wrong"); + }; + source.onerror = function(error) { + source.onerror = null; + source.close(); + ok(true, "EventSource should not work with opaque responses"); + doUnregister(); + }; + } + + function onLoad() { + if (!parent) { + dump("eventsource/eventsource_opaque_response.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function onMessage(e) { + if (e.data.status === "callback") { + switch(e.data.data) { + case "eventsource": + doEventSource(); + window.removeEventListener("message", onMessage); + break; + default: + ok(false, "Something went wrong") + break + } + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage({status: "callback", data: "ready"}, "*"); + }); + + navigator.serviceWorker.addEventListener("message", function(event) { + parent.postMessage(event.data, "*"); + }); + } + + </script> +</head> +<body onload="onLoad()"> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/eventsource/eventsource_opaque_response_intercept_worker.js b/dom/workers/test/serviceworkers/eventsource/eventsource_opaque_response_intercept_worker.js new file mode 100644 index 000000000..45a80e324 --- /dev/null +++ b/dom/workers/test/serviceworkers/eventsource/eventsource_opaque_response_intercept_worker.js @@ -0,0 +1,20 @@ +// Cross origin request +var prefix = 'http://example.com/tests/dom/workers/test/serviceworkers/eventsource/'; + +self.importScripts('eventsource_worker_helper.js'); + +self.addEventListener('fetch', function (event) { + var request = event.request; + var url = new URL(request.url); + + if (url.pathname !== '/tests/dom/workers/test/serviceworkers/eventsource/eventsource.resource') { + return; + } + + ok(request.mode === 'cors', 'EventSource should make a CORS request'); + ok(request.cache === 'no-store', 'EventSource should make a no-store request'); + var fetchRequest = new Request(prefix + 'eventsource.resource', { mode: 'no-cors'}); + event.respondWith(fetch(fetchRequest).then((fetchResponse) => { + return fetchResponse; + })); +}); diff --git a/dom/workers/test/serviceworkers/eventsource/eventsource_register_worker.html b/dom/workers/test/serviceworkers/eventsource/eventsource_register_worker.html new file mode 100644 index 000000000..59e8e92ab --- /dev/null +++ b/dom/workers/test/serviceworkers/eventsource/eventsource_register_worker.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script type="text/javascript"> + + function getURLParam (aTarget, aValue) { + return decodeURI(aTarget.search.replace(new RegExp("^(?:.*[&\\?]" + encodeURI(aValue).replace(/[\.\+\*]/g, "\\$&") + "(?:\\=([^&]*))?)?.*$", "i"), "$1")); + } + + function onLoad() { + navigator.serviceWorker.ready.then(function() { + parent.postMessage({status: "callback", data: "done"}, "*"); + }); + + navigator.serviceWorker.register(getURLParam(document.location, "script"), {scope: "."}); + } + + </script> +</head> +<body onload="onLoad()"> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/eventsource/eventsource_synthetic_response.html b/dom/workers/test/serviceworkers/eventsource/eventsource_synthetic_response.html new file mode 100644 index 000000000..d9f380e2f --- /dev/null +++ b/dom/workers/test/serviceworkers/eventsource/eventsource_synthetic_response.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script type="text/javascript"> + + var prefix = "http://mochi.test:8888/tests/dom/workers/test/serviceworkers/eventsource/"; + + function ok(aCondition, aMessage) { + parent.postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}, "*"); + } + + function doUnregister() { + navigator.serviceWorker.getRegistration().then(swr => { + swr.unregister().then(function(result) { + ok(result, "Unregister should return true."); + parent.postMessage({status: "callback", data: "done"}, "*"); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e); + }); + }); + } + + function doEventSource() { + var source = new EventSource(prefix + "eventsource.resource"); + source.onmessage = function(e) { + source.onmessage = null; + source.close(); + ok(true, "EventSource should work with synthetic responses"); + doUnregister(); + }; + source.onerror = function(error) { + source.onmessage = null; + source.close(); + ok(false, "Something went wrong"); + }; + } + + function onLoad() { + if (!parent) { + dump("eventsource/eventsource_synthetic_response.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function onMessage(e) { + if (e.data.status === "callback") { + switch(e.data.data) { + case "eventsource": + doEventSource(); + window.removeEventListener("message", onMessage); + break; + default: + ok(false, "Something went wrong") + break + } + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage({status: "callback", data: "ready"}, "*"); + }); + + navigator.serviceWorker.addEventListener("message", function(event) { + parent.postMessage(event.data, "*"); + }); + } + + </script> +</head> +<body onload="onLoad()"> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/eventsource/eventsource_synthetic_response_intercept_worker.js b/dom/workers/test/serviceworkers/eventsource/eventsource_synthetic_response_intercept_worker.js new file mode 100644 index 000000000..8692f9186 --- /dev/null +++ b/dom/workers/test/serviceworkers/eventsource/eventsource_synthetic_response_intercept_worker.js @@ -0,0 +1,24 @@ +self.importScripts('eventsource_worker_helper.js'); + +self.addEventListener('fetch', function (event) { + var request = event.request; + var url = new URL(request.url); + + if (url.pathname !== '/tests/dom/workers/test/serviceworkers/eventsource/eventsource.resource') { + return; + } + + ok(request.mode === 'cors', 'EventSource should make a CORS request'); + var headerList = { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache, must-revalidate' + }; + var headers = new Headers(headerList); + var init = { + headers: headers, + mode: 'cors' + }; + var body = 'data: data0\r\r'; + var response = new Response(body, init); + event.respondWith(response); +}); diff --git a/dom/workers/test/serviceworkers/eventsource/eventsource_worker_helper.js b/dom/workers/test/serviceworkers/eventsource/eventsource_worker_helper.js new file mode 100644 index 000000000..6d5dbb024 --- /dev/null +++ b/dom/workers/test/serviceworkers/eventsource/eventsource_worker_helper.js @@ -0,0 +1,12 @@ +function ok(aCondition, aMessage) { + return new Promise(function(resolve, reject) { + self.clients.matchAll().then(function(res) { + if (!res.length) { + reject(); + return; + } + res[0].postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}); + resolve(); + }); + }); +} diff --git a/dom/workers/test/serviceworkers/fetch.js b/dom/workers/test/serviceworkers/fetch.js new file mode 100644 index 000000000..38d20a638 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch.js @@ -0,0 +1,11 @@ +addEventListener('fetch', function(event) { + if (event.request.url.indexOf("fail.html") !== -1) { + event.respondWith(fetch("hello.html", {"integrity": "abc"})); + } else if (event.request.url.indexOf("fake.html") !== -1) { + event.respondWith(fetch("hello.html")); + } +}); + +addEventListener("activate", function(event) { + event.waitUntil(clients.claim()); +}); diff --git a/dom/workers/test/serviceworkers/fetch/context/beacon.sjs b/dom/workers/test/serviceworkers/fetch/context/beacon.sjs new file mode 100644 index 000000000..8401bc29b --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/context/beacon.sjs @@ -0,0 +1,43 @@ +/* + * This is based on dom/tests/mochitest/beacon/beacon-originheader-handler.sjs. + */ + +function handleRequest(request, response) +{ + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader("Content-Type", "text/plain", false); + + // case XHR-REQUEST: the xhr-request tries to query the + // stored context from the beacon request. + if (request.queryString == "queryContext") { + var context = getState("interceptContext"); + // if the beacon already stored the context - return. + if (context) { + response.write(context); + setState("interceptContext", ""); + return; + } + // otherwise wait for the beacon request + response.processAsync(); + setObjectState("sw-xhr-response", response); + return; + } + + // case BEACON-REQUEST: get the beacon context and + // store the context on the server. + var context = request.queryString; + setState("interceptContext", context); + + // if there is an xhr-request waiting, return the context now. + try{ + getObjectState("sw-xhr-response", function(xhrResponse) { + if (!xhrResponse) { + return; + } + setState("interceptContext", ""); + xhrResponse.write(context); + xhrResponse.finish(); + }); + } catch(e) { + } +} diff --git a/dom/workers/test/serviceworkers/fetch/context/context_test.js b/dom/workers/test/serviceworkers/fetch/context/context_test.js new file mode 100644 index 000000000..b98d2ab3c --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/context/context_test.js @@ -0,0 +1,135 @@ +self.addEventListener("fetch", function(event) { + if (event.request.url.indexOf("index.html") >= 0 || + event.request.url.indexOf("register.html") >= 0 || + event.request.url.indexOf("unregister.html") >= 0 || + event.request.url.indexOf("ping.html") >= 0 || + event.request.url.indexOf("xml.xml") >= 0 || + event.request.url.indexOf("csp-violate.sjs") >= 0) { + // Handle pass-through requests + event.respondWith(fetch(event.request)); + } else if (event.request.url.indexOf("fetch.txt") >= 0) { + var body = event.request.context == "fetch" ? + "so fetch" : "so unfetch"; + event.respondWith(new Response(body)); + } else if (event.request.url.indexOf("img.jpg") >= 0) { + if (event.request.context == "image") { + event.respondWith(fetch("realimg.jpg")); + } + } else if (event.request.url.indexOf("responsive.jpg") >= 0) { + if (event.request.context == "imageset") { + event.respondWith(fetch("realimg.jpg")); + } + } else if (event.request.url.indexOf("audio.ogg") >= 0) { + if (event.request.context == "audio") { + event.respondWith(fetch("realaudio.ogg")); + } + } else if (event.request.url.indexOf("video.ogg") >= 0) { + if (event.request.context == "video") { + event.respondWith(fetch("realaudio.ogg")); + } + } else if (event.request.url.indexOf("beacon.sjs") >= 0) { + if (event.request.url.indexOf("queryContext") == -1) { + event.respondWith(fetch("beacon.sjs?" + event.request.context)); + } else { + event.respondWith(fetch(event.request)); + } + } else if (event.request.url.indexOf("csp-report.sjs") >= 0) { + respondToServiceWorker(event, "csp-report"); + } else if (event.request.url.indexOf("embed") >= 0) { + respondToServiceWorker(event, "embed"); + } else if (event.request.url.indexOf("object") >= 0) { + respondToServiceWorker(event, "object"); + } else if (event.request.url.indexOf("font") >= 0) { + respondToServiceWorker(event, "font"); + } else if (event.request.url.indexOf("iframe") >= 0) { + if (event.request.context == "iframe") { + event.respondWith(fetch("context_test.js")); + } + } else if (event.request.url.indexOf("frame") >= 0) { + if (event.request.context == "frame") { + event.respondWith(fetch("context_test.js")); + } + } else if (event.request.url.indexOf("newwindow") >= 0) { + respondToServiceWorker(event, "newwindow"); + } else if (event.request.url.indexOf("ping") >= 0) { + respondToServiceWorker(event, "ping"); + } else if (event.request.url.indexOf("plugin") >= 0) { + respondToServiceWorker(event, "plugin"); + } else if (event.request.url.indexOf("script.js") >= 0) { + if (event.request.context == "script") { + event.respondWith(new Response("")); + } + } else if (event.request.url.indexOf("style.css") >= 0) { + respondToServiceWorker(event, "style"); + } else if (event.request.url.indexOf("track") >= 0) { + respondToServiceWorker(event, "track"); + } else if (event.request.url.indexOf("xhr") >= 0) { + if (event.request.context == "xmlhttprequest") { + event.respondWith(new Response("")); + } + } else if (event.request.url.indexOf("xslt") >= 0) { + respondToServiceWorker(event, "xslt"); + } else if (event.request.url.indexOf("myworker") >= 0) { + if (event.request.context == "worker") { + event.respondWith(fetch("worker.js")); + } + } else if (event.request.url.indexOf("myparentworker") >= 0) { + if (event.request.context == "worker") { + event.respondWith(fetch("parentworker.js")); + } + } else if (event.request.url.indexOf("mysharedworker") >= 0) { + if (event.request.context == "sharedworker") { + event.respondWith(fetch("sharedworker.js")); + } + } else if (event.request.url.indexOf("myparentsharedworker") >= 0) { + if (event.request.context == "sharedworker") { + event.respondWith(fetch("parentsharedworker.js")); + } + } else if (event.request.url.indexOf("cache") >= 0) { + var cache; + var origContext = event.request.context; + event.respondWith(caches.open("cache") + .then(function(c) { + cache = c; + // Store the Request in the cache. + return cache.put(event.request, new Response("fake")); + }).then(function() { + // Read it back. + return cache.keys(event.request); + }).then(function(res) { + var req = res[0]; + // Check to see if the context remained the same. + var success = req.context === origContext; + return clients.matchAll() + .then(function(clients) { + // Report it back to the main page. + clients.forEach(function(c) { + c.postMessage({data: "cache", success: success}); + }); + })}).then(function() { + // Cleanup. + return caches.delete("cache"); + }).then(function() { + return new Response("ack"); + })); + } + // Fail any request that we don't know about. + try { + event.respondWith(Promise.reject(event.request.url)); + dump("Fetch event received invalid context value " + event.request.context + + " for " + event.request.url + "\n"); + } catch(e) { + // Eat up the possible InvalidStateError exception that we may get if some + // code above has called respondWith too. + } +}); + +function respondToServiceWorker(event, data) { + event.respondWith(clients.matchAll() + .then(function(clients) { + clients.forEach(function(c) { + c.postMessage({data: data, context: event.request.context}); + }); + return new Response("ack"); + })); +} diff --git a/dom/workers/test/serviceworkers/fetch/context/csp-violate.sjs b/dom/workers/test/serviceworkers/fetch/context/csp-violate.sjs new file mode 100644 index 000000000..4c3e76d15 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/context/csp-violate.sjs @@ -0,0 +1,6 @@ +function handleRequest(request, response) +{ + response.setHeader("Content-Security-Policy", "default-src 'none'; report-uri /tests/dom/workers/test/serviceworkers/fetch/context/csp-report.sjs", false); + response.setHeader("Content-Type", "text/html", false); + response.write("<link rel=stylesheet href=style.css>"); +} diff --git a/dom/workers/test/serviceworkers/fetch/context/index.html b/dom/workers/test/serviceworkers/fetch/context/index.html new file mode 100644 index 000000000..c6dfef99c --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/context/index.html @@ -0,0 +1,422 @@ +<!DOCTYPE html> +<script> + var isAndroid = navigator.userAgent.includes("Android"); + var isB2G = !isAndroid && /Mobile|Tablet/.test(navigator.userAgent); + + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function is(a, b, msg) { + ok(a === b, msg + ", expected '" + b + "', got '" + a + "'"); + } + + function todo(v, msg) { + window.parent.postMessage({status: "todo", result: !!v, message: msg}, "*"); + } + + function finish() { + window.parent.postMessage({status: "done"}, "*"); + } + + function testFetch() { + return fetch("fetch.txt").then(function(r) { + return r.text(); + }).then(function(body) { + is(body, "so fetch", "A fetch() Request should have the 'fetch' context"); + }); + } + + function testImage() { + return new Promise(function(resolve, reject) { + var img = document.createElement("img"); + img.src = "img.jpg"; + // The service worker will respond with an existing image only if the + // Request has the correct context, otherwise the Promise will get + // rejected and the test will fail. + img.onload = resolve; + img.onerror = reject; + }); + } + + function testImageSrcSet() { + return new Promise(function(resolve, reject) { + var img = document.createElement("img"); + img.srcset = "responsive.jpg 100w"; + // The service worker will respond with an existing image only if the + // Request has the correct context, otherwise the Promise will get + // rejected and the test will fail. + img.onload = resolve; + img.onerror = reject; + }); + } + + function testPicture() { + return new Promise(function(resolve, reject) { + var pic = document.createElement("picture"); + var img = document.createElement("img"); + pic.appendChild(img); + img.src = "responsive.jpg?picture"; + // The service worker will respond with an existing image only if the + // Request has the correct context, otherwise the Promise will get + // rejected and the test will fail. + img.onload = resolve; + img.onerror = reject; + }); + } + + function testAudio() { + return new Promise(function(resolve, reject) { + var audio = document.createElement("audio"); + audio.src = "audio.ogg"; + audio.preload = "metadata"; + // The service worker will respond with an existing audio only if the + // Request has the correct context, otherwise the Promise will get + // rejected and the test will fail. + audio.onloadedmetadata = resolve; + audio.onerror = reject; + }); + } + + function testVideo() { + return new Promise(function(resolve, reject) { + var video = document.createElement("video"); + video.src = "video.ogg"; + video.preload = "metadata"; + // The service worker will respond with an existing video only if the + // Request has the correct context, otherwise the Promise will get + // rejected and the test will fail. + video.onloadedmetadata = resolve; + video.onerror = reject; + }); + } + + function testBeacon() { + ok(navigator.sendBeacon("beacon.sjs"), "Sending the beacon should succeed"); + // query the context from beacon.sjs + return fetch("beacon.sjs?queryContext") + .then(function(r) { + return r.text(); + }).then(function(body) { + is(body, "beacon", "The context for the intercepted beacon should be correct"); + }); + } + + function testCSPReport() { + return new Promise(function(resolve, reject) { + var iframe = document.createElement("iframe"); + iframe.src = "csp-violate.sjs"; + document.documentElement.appendChild(iframe); + navigator.serviceWorker.addEventListener("message", function onMessage(e) { + if (e.data.data == "csp-report") { + is(e.data.context, "cspreport", "Expected the cspreport context on a CSP violation report"); + navigator.serviceWorker.removeEventListener("message", onMessage); + resolve(); + } + }, false); + }); + } + + function testEmbed() { + return Promise.resolve().then(function() { + todo(false, "<embed> tag is not currently intercepted. See Bug 1168676."); + }); + + return new Promise(function(resolve, reject) { + var embed = document.createElement("embed"); + embed.src = "embed"; + document.documentElement.appendChild(embed); + navigator.serviceWorker.addEventListener("message", function onMessage(e) { + if (e.data.data == "embed") { + is(e.data.context, "embed", "Expected the object context on an embed"); + navigator.serviceWorker.removeEventListener("message", onMessage); + resolve(); + } + }, false); + }); + } + + function testObject() { + return Promise.resolve().then(function() { + todo(false, "<object> tag is not currently intercepted. See Bug 1168676"); + }); + + return new Promise(function(resolve, reject) { + var object = document.createElement("object"); + object.data = "object"; + document.documentElement.appendChild(object); + navigator.serviceWorker.addEventListener("message", function onMessage(e) { + if (e.data.data == "object") { + is(e.data.context, "object", "Expected the object context on an object"); + navigator.serviceWorker.removeEventListener("message", onMessage); + resolve(); + } + }, false); + }); + } + + function testFont() { + return new Promise(function(resolve, reject) { + var css = '@font-face { font-family: "sw-font"; src: url("font"); }'; + css += '* { font-family: "sw-font"; }'; + var style = document.createElement("style"); + style.appendChild(document.createTextNode(css)); + document.documentElement.appendChild(style); + navigator.serviceWorker.addEventListener("message", function onMessage(e) { + if (e.data.data == "font") { + is(e.data.context, "font", "Expected the font context on an font"); + navigator.serviceWorker.removeEventListener("message", onMessage); + resolve(); + } + }, false); + }); + } + + function testIFrame() { + return new Promise(function(resolve, reject) { + var iframe = document.createElement("iframe"); + iframe.src = "iframe"; + document.documentElement.appendChild(iframe); + // The service worker will respond with an existing document only if the + // Request has the correct context, otherwise the Promise will get + // rejected and the test will fail. + iframe.onload = resolve; + iframe.onerror = reject; + }); + } + + function testFrame() { + return new Promise(function(resolve, reject) { + var frame = document.createElement("frame"); + frame.src = "frame"; + document.documentElement.appendChild(frame); + // The service worker will respond with an existing document only if the + // Request has the correct context, otherwise the Promise will get + // rejected and the test will fail. + frame.onload = resolve; + frame.onerror = reject; + }); + } + + function testInternal() { + if (isB2G) { + // We can't open new windows on b2g, so skip this part of the test there. + return Promise.resolve(); + } + return new Promise(function(resolve, reject) { + // Test this with a new window opened through script. There are of course + // other possible ways of testing this too. + var win = window.open("newwindow", "_blank", "width=100,height=100"); + navigator.serviceWorker.addEventListener("message", function onMessage(e) { + if (e.data.data == "newwindow") { + is(e.data.context, "internal", "Expected the internal context on a newly opened window"); + navigator.serviceWorker.removeEventListener("message", onMessage); + win.close(); + resolve(); + } + }, false); + }); + } + + function testPing() { + return new Promise(function(resolve, reject) { + var iframe = document.createElement("iframe"); + iframe.src = "ping.html"; + document.documentElement.appendChild(iframe); + navigator.serviceWorker.addEventListener("message", function onMessage(e) { + if (e.data.data == "ping") { + is(e.data.context, "ping", "Expected the ping context on an anchor ping"); + navigator.serviceWorker.removeEventListener("message", onMessage); + resolve(); + } + }, false); + }); + } + + function testPlugin() { + return Promise.resolve().then(function() { + todo(false, "plugins are not currently intercepted. See Bug 1168676."); + }); + + var isMobile = /Mobile|Tablet/.test(navigator.userAgent); + if (isMobile || parent.isMulet()) { + // We can't use plugins on mobile, so skip this part of the test there. + return Promise.resolve(); + } + + return new Promise(function(resolve, reject) { + var embed = document.createElement("embed"); + embed.type = "application/x-test"; + embed.setAttribute("posturl", "plugin"); + embed.setAttribute("postmode", "stream"); + embed.setAttribute("streammode", "normal"); + embed.setAttribute("src", "fetch.txt"); + document.documentElement.appendChild(embed); + navigator.serviceWorker.addEventListener("message", function onMessage(e) { + if (e.data.data == "plugin") { + is(e.data.context, "plugin", "Expected the plugin context on a request coming from a plugin"); + navigator.serviceWorker.removeEventListener("message", onMessage); + // Without this, the test leaks in e10s! + embed.parentNode.removeChild(embed); + resolve(); + } + }, false); + }); + } + + function testScript() { + return new Promise(function(resolve, reject) { + var script = document.createElement("script"); + script.src = "script.js"; + document.documentElement.appendChild(script); + // The service worker will respond with an existing script only if the + // Request has the correct context, otherwise the Promise will get + // rejected and the test will fail. + script.onload = resolve; + script.onerror = reject; + }); + } + + function testStyle() { + return new Promise(function(resolve, reject) { + var link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = "style.css"; + document.documentElement.appendChild(link); + navigator.serviceWorker.addEventListener("message", function onMessage(e) { + if (e.data.data == "style") { + is(e.data.context, "style", "Expected the style context on a request coming from a stylesheet"); + navigator.serviceWorker.removeEventListener("message", onMessage); + resolve(); + } + }, false); + }); + } + + function testTrack() { + return new Promise(function(resolve, reject) { + var video = document.createElement("video"); + var track = document.createElement("track"); + track.src = "track"; + video.appendChild(track); + document.documentElement.appendChild(video); + navigator.serviceWorker.addEventListener("message", function onMessage(e) { + if (e.data.data == "track") { + is(e.data.context, "track", "Expected the track context on a request coming from a track"); + navigator.serviceWorker.removeEventListener("message", onMessage); + resolve(); + } + }, false); + }); + } + + function testXHR() { + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.open("get", "xhr", true); + xhr.send(); + // The service worker will respond with an existing resource only if the + // Request has the correct context, otherwise the Promise will get + // rejected and the test will fail. + xhr.onload = resolve; + xhr.onerror = reject; + }); + } + + function testXSLT() { + return new Promise(function(resolve, reject) { + var iframe = document.createElement("iframe"); + iframe.src = "xml.xml"; + document.documentElement.appendChild(iframe); + navigator.serviceWorker.addEventListener("message", function onMessage(e) { + if (e.data.data == "xslt") { + is(e.data.context, "xslt", "Expected the xslt context on an XSLT stylesheet"); + navigator.serviceWorker.removeEventListener("message", onMessage); + // Without this, the test leaks in e10s! + iframe.parentNode.removeChild(iframe); + resolve(); + } + }, false); + }); + } + + function testWorker() { + return new Promise(function(resolve, reject) { + var worker = new Worker("myworker"); + worker.onmessage = function(e) { + if (e.data == "ack") { + worker.terminate(); + resolve(); + } + }; + worker.onerror = reject; + }); + } + + function testNestedWorker() { + return new Promise(function(resolve, reject) { + var worker = new Worker("myparentworker"); + worker.onmessage = function(e) { + if (e.data == "ack") { + worker.terminate(); + resolve(); + } + }; + worker.onerror = reject; + }); + } + + function testSharedWorker() { + return new Promise(function(resolve, reject) { + var worker = new SharedWorker("mysharedworker"); + worker.port.start(); + worker.port.onmessage = function(e) { + if (e.data == "ack") { + resolve(); + } + }; + worker.onerror = reject; + }); + } + + function testNestedWorkerInSharedWorker() { + return new Promise(function(resolve, reject) { + var worker = new SharedWorker("myparentsharedworker"); + worker.port.start(); + worker.port.onmessage = function(e) { + if (e.data == "ack") { + resolve(); + } + }; + worker.onerror = reject; + }); + } + + function testCache() { + return new Promise(function(resolve, reject) { + // Issue an XHR that will be intercepted by the SW in order to start off + // the test with a RequestContext value that is not the default ("fetch"). + // This needs to run inside a fetch event handler because synthesized + // RequestContext objects can only have the "fetch" context, and we'd + // prefer to test the more general case of some other RequestContext value. + var xhr = new XMLHttpRequest(); + xhr.open("get", "cache", true); + xhr.send(); + navigator.serviceWorker.addEventListener("message", function onMessage(e) { + if (e.data.data == "cache") { + ok(e.data.success, "The RequestContext can be persisted in the cache."); + navigator.serviceWorker.removeEventListener("message", onMessage); + resolve(); + } + }, false); + }); + } + + var testName = location.search.substr(1); + window[testName]().then(function() { + finish(); + }, function(e) { + ok(false, "A promise was rejected: " + e); + finish(); + }); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/context/parentsharedworker.js b/dom/workers/test/serviceworkers/fetch/context/parentsharedworker.js new file mode 100644 index 000000000..eac8d5e71 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/context/parentsharedworker.js @@ -0,0 +1,8 @@ +onconnect = function(e) { + e.ports[0].start(); + var worker = new Worker("myworker?shared"); + worker.onmessage = function(e2) { + e.ports[0].postMessage(e2.data); + self.close(); + }; +}; diff --git a/dom/workers/test/serviceworkers/fetch/context/parentworker.js b/dom/workers/test/serviceworkers/fetch/context/parentworker.js new file mode 100644 index 000000000..839fb6640 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/context/parentworker.js @@ -0,0 +1,4 @@ +var worker = new Worker("myworker"); +worker.onmessage = function(e) { + postMessage(e.data); +}; diff --git a/dom/workers/test/serviceworkers/fetch/context/ping.html b/dom/workers/test/serviceworkers/fetch/context/ping.html new file mode 100644 index 000000000..b1bebe41e --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/context/ping.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<script> + onload = function() { + document.querySelector("a").click(); + }; +</script> +<a ping="ping" href="fetch.txt">link</a> diff --git a/dom/workers/test/serviceworkers/fetch/context/realaudio.ogg b/dom/workers/test/serviceworkers/fetch/context/realaudio.ogg Binary files differnew file mode 100644 index 000000000..1a41623f8 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/context/realaudio.ogg diff --git a/dom/workers/test/serviceworkers/fetch/context/realimg.jpg b/dom/workers/test/serviceworkers/fetch/context/realimg.jpg Binary files differnew file mode 100644 index 000000000..5b920f7c0 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/context/realimg.jpg diff --git a/dom/workers/test/serviceworkers/fetch/context/register.html b/dom/workers/test/serviceworkers/fetch/context/register.html new file mode 100644 index 000000000..6528d0eae --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/context/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("context_test.js", {scope: "."}); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/context/sharedworker.js b/dom/workers/test/serviceworkers/fetch/context/sharedworker.js new file mode 100644 index 000000000..94dca5839 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/context/sharedworker.js @@ -0,0 +1,5 @@ +onconnect = function(e) { + e.ports[0].start(); + e.ports[0].postMessage("ack"); + self.close(); +}; diff --git a/dom/workers/test/serviceworkers/fetch/context/unregister.html b/dom/workers/test/serviceworkers/fetch/context/unregister.html new file mode 100644 index 000000000..1f13508fa --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/context/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/context/worker.js b/dom/workers/test/serviceworkers/fetch/context/worker.js new file mode 100644 index 000000000..e26e5bc69 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/context/worker.js @@ -0,0 +1 @@ +postMessage("ack"); diff --git a/dom/workers/test/serviceworkers/fetch/context/xml.xml b/dom/workers/test/serviceworkers/fetch/context/xml.xml new file mode 100644 index 000000000..69c64adf1 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/context/xml.xml @@ -0,0 +1,3 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/xsl" href="xslt"?> +<root/> diff --git a/dom/workers/test/serviceworkers/fetch/deliver-gzip.sjs b/dom/workers/test/serviceworkers/fetch/deliver-gzip.sjs new file mode 100644 index 000000000..abacdd2ad --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/deliver-gzip.sjs @@ -0,0 +1,17 @@ +function handleRequest(request, response) { + // The string "hello" repeated 10 times followed by newline. Compressed using gzip. + var bytes = [0x1f, 0x8b, 0x08, 0x08, 0x4d, 0xe2, 0xf9, 0x54, 0x00, 0x03, 0x68, + 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0xcb, 0x48, 0xcd, 0xc9, 0xc9, 0xcf, + 0x20, 0x85, 0xe0, 0x02, 0x00, 0xf5, 0x4b, 0x38, 0xcf, 0x33, 0x00, + 0x00, 0x00]; + + response.setHeader("Content-Encoding", "gzip", false); + response.setHeader("Content-Length", "" + bytes.length, false); + response.setHeader("Content-Type", "text/plain", false); + + var bos = Components.classes["@mozilla.org/binaryoutputstream;1"] + .createInstance(Components.interfaces.nsIBinaryOutputStream); + bos.setOutputStream(response.bodyOutputStream); + + bos.writeByteArray(bytes, bytes.length); +} diff --git a/dom/workers/test/serviceworkers/fetch/fetch_tests.js b/dom/workers/test/serviceworkers/fetch/fetch_tests.js new file mode 100644 index 000000000..54da1b66e --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/fetch_tests.js @@ -0,0 +1,416 @@ +var origin = 'http://mochi.test:8888'; + +function fetchXHRWithMethod(name, method, onload, onerror, headers) { + expectAsyncResult(); + + onload = onload || function() { + my_ok(false, "XHR load should not complete successfully"); + finish(); + }; + onerror = onerror || function() { + my_ok(false, "XHR load for " + name + " should be intercepted successfully"); + finish(); + }; + + var x = new XMLHttpRequest(); + x.open(method, name, true); + x.onload = function() { onload(x) }; + x.onerror = function() { onerror(x) }; + headers = headers || []; + headers.forEach(function(header) { + x.setRequestHeader(header[0], header[1]); + }); + x.send(); +} + +var corsServerPath = '/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs'; +var corsServerURL = 'http://example.com' + corsServerPath; + +function redirectURL(hops) { + return hops[0].server + corsServerPath + "?hop=1&hops=" + + encodeURIComponent(hops.toSource()); +} + +function fetchXHR(name, onload, onerror, headers) { + return fetchXHRWithMethod(name, 'GET', onload, onerror, headers); +} + +fetchXHR('bare-synthesized.txt', function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok(xhr.responseText == "synthesized response body", "load should have synthesized response"); + finish(); +}); + +fetchXHR('test-respondwith-response.txt', function(xhr) { + my_ok(xhr.status == 200, "test-respondwith-response load should be successful"); + my_ok(xhr.responseText == "test-respondwith-response response body", "load should have response"); + finish(); +}); + +fetchXHR('synthesized-404.txt', function(xhr) { + my_ok(xhr.status == 404, "load should 404"); + my_ok(xhr.responseText == "synthesized response body", "404 load should have synthesized response"); + finish(); +}); + +fetchXHR('synthesized-headers.txt', function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok(xhr.getResponseHeader("X-Custom-Greeting") === "Hello", "custom header should be set"); + my_ok(xhr.responseText == "synthesized response body", "custom header load should have synthesized response"); + finish(); +}); + +fetchXHR('synthesized-redirect-real-file.txt', function(xhr) { +dump("Got status AARRGH " + xhr.status + " " + xhr.responseText + "\n"); + my_ok(xhr.status == 200, "load should be successful"); + my_ok(xhr.responseText == "This is a real file.\n", "Redirect to real file should complete."); + finish(); +}); + +fetchXHR('synthesized-redirect-twice-real-file.txt', function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok(xhr.responseText == "This is a real file.\n", "Redirect to real file (twice) should complete."); + finish(); +}); + +fetchXHR('synthesized-redirect-synthesized.txt', function(xhr) { + my_ok(xhr.status == 200, "synth+redirect+synth load should be successful"); + my_ok(xhr.responseText == "synthesized response body", "load should have redirected+synthesized response"); + finish(); +}); + +fetchXHR('synthesized-redirect-twice-synthesized.txt', function(xhr) { + my_ok(xhr.status == 200, "synth+redirect+synth (twice) load should be successful"); + my_ok(xhr.responseText == "synthesized response body", "load should have redirected+synthesized (twice) response"); + finish(); +}); + +fetchXHR('redirect.sjs', function(xhr) { + my_ok(xhr.status == 404, "redirected load should be uninterrupted"); + finish(); +}); + +fetchXHR('ignored.txt', function(xhr) { + my_ok(xhr.status == 404, "load should be uninterrupted"); + finish(); +}); + +fetchXHR('rejected.txt', null, function(xhr) { + my_ok(xhr.status == 0, "load should not complete"); + finish(); +}); + +fetchXHR('nonresponse.txt', null, function(xhr) { + my_ok(xhr.status == 0, "load should not complete"); + finish(); +}); + +fetchXHR('nonresponse2.txt', null, function(xhr) { + my_ok(xhr.status == 0, "load should not complete"); + finish(); +}); + +fetchXHR('nonpromise.txt', null, function(xhr) { + my_ok(xhr.status == 0, "load should not complete"); + finish(); +}); + +fetchXHR('headers.txt', function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok(xhr.responseText == "1", "request header checks should have passed"); + finish(); +}, null, [["X-Test1", "header1"], ["X-Test2", "header2"]]); + +fetchXHR('http://user:pass@mochi.test:8888/user-pass', function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok(xhr.responseText == 'http://user:pass@mochi.test:8888/user-pass', 'The username and password should be preserved'); + finish(); +}); + +var expectedUncompressedResponse = ""; +for (var i = 0; i < 10; ++i) { + expectedUncompressedResponse += "hello"; +} +expectedUncompressedResponse += "\n"; + +// ServiceWorker does not intercept, at which point the network request should +// be correctly decoded. +fetchXHR('deliver-gzip.sjs', function(xhr) { + my_ok(xhr.status == 200, "network gzip load should be successful"); + my_ok(xhr.responseText == expectedUncompressedResponse, "network gzip load should have synthesized response."); + my_ok(xhr.getResponseHeader("Content-Encoding") == "gzip", "network Content-Encoding should be gzip."); + my_ok(xhr.getResponseHeader("Content-Length") == "35", "network Content-Length should be of original gzipped file."); + finish(); +}); + +fetchXHR('hello.gz', function(xhr) { + my_ok(xhr.status == 200, "gzip load should be successful"); + my_ok(xhr.responseText == expectedUncompressedResponse, "gzip load should have synthesized response."); + my_ok(xhr.getResponseHeader("Content-Encoding") == "gzip", "Content-Encoding should be gzip."); + my_ok(xhr.getResponseHeader("Content-Length") == "35", "Content-Length should be of original gzipped file."); + finish(); +}); + +fetchXHR('hello-after-extracting.gz', function(xhr) { + my_ok(xhr.status == 200, "gzip load after extracting should be successful"); + my_ok(xhr.responseText == expectedUncompressedResponse, "gzip load after extracting should have synthesized response."); + my_ok(xhr.getResponseHeader("Content-Encoding") == "gzip", "Content-Encoding after extracting should be gzip."); + my_ok(xhr.getResponseHeader("Content-Length") == "35", "Content-Length after extracting should be of original gzipped file."); + finish(); +}); + +fetchXHR(corsServerURL + '?status=200&allowOrigin=*', function(xhr) { + my_ok(xhr.status == 200, "cross origin load with correct headers should be successful"); + my_ok(xhr.getResponseHeader("access-control-allow-origin") == null, "cors headers should be filtered out"); + finish(); +}); + +// Verify origin header is sent properly even when we have a no-intercept SW. +var uriOrigin = encodeURIComponent(origin); +fetchXHR('http://example.org' + corsServerPath + '?ignore&status=200&origin=' + uriOrigin + + '&allowOrigin=' + uriOrigin, function(xhr) { + my_ok(xhr.status == 200, "cross origin load with correct headers should be successful"); + my_ok(xhr.getResponseHeader("access-control-allow-origin") == null, "cors headers should be filtered out"); + finish(); +}); + +// Verify that XHR is considered CORS tainted even when original URL is same-origin +// redirected to cross-origin. +fetchXHR(redirectURL([{ server: origin }, + { server: 'http://example.org', + allowOrigin: origin }]), function(xhr) { + my_ok(xhr.status == 200, "cross origin load with correct headers should be successful"); + my_ok(xhr.getResponseHeader("access-control-allow-origin") == null, "cors headers should be filtered out"); + finish(); +}); + +// Test that CORS preflight requests cannot be intercepted. Performs a +// cross-origin XHR that the SW chooses not to intercept. This requires a +// preflight request, which the SW must not be allowed to intercept. +fetchXHR(corsServerURL + '?status=200&allowOrigin=*', null, function(xhr) { + my_ok(xhr.status == 0, "cross origin load with incorrect headers should be a failure"); + finish(); +}, [["X-Unsafe", "unsafe"]]); + +// Test that CORS preflight requests cannot be intercepted. Performs a +// cross-origin XHR that the SW chooses to intercept and respond with a +// cross-origin fetch. This requires a preflight request, which the SW must not +// be allowed to intercept. +fetchXHR('http://example.org' + corsServerPath + '?status=200&allowOrigin=*', null, function(xhr) { + my_ok(xhr.status == 0, "cross origin load with incorrect headers should be a failure"); + finish(); +}, [["X-Unsafe", "unsafe"]]); + +// Test that when the page fetches a url the controlling SW forces a redirect to +// another location. This other location fetch should also be intercepted by +// the SW. +fetchXHR('something.txt', function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok(xhr.responseText == "something else response body", "load should have something else"); + finish(); +}); + +// Test fetch will internally get it's SkipServiceWorker flag set. The request is +// made from the SW through fetch(). fetch() fetches a server-side JavaScript +// file that force a redirect. The redirect location fetch does not go through +// the SW. +fetchXHR('redirect_serviceworker.sjs', function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok(xhr.responseText == "// empty worker, always succeed!\n", "load should have redirection content"); + finish(); +}); + +fetchXHR('empty-header', function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok(xhr.responseText == "emptyheader", "load should have the expected content"); + finish(); +}, null, [["emptyheader", ""]]); + +expectAsyncResult(); +fetch('http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200&allowOrigin=*') +.then(function(res) { + my_ok(res.ok, "Valid CORS request should receive valid response"); + my_ok(res.type == "cors", "Response type should be CORS"); + res.text().then(function(body) { + my_ok(body === "<res>hello pass</res>\n", "cors response body should match"); + finish(); + }); +}, function(e) { + my_ok(false, "CORS Fetch failed"); + finish(); +}); + +expectAsyncResult(); +fetch('http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200', { mode: 'no-cors' }) +.then(function(res) { + my_ok(res.type == "opaque", "Response type should be opaque"); + my_ok(res.status == 0, "Status should be 0"); + res.text().then(function(body) { + my_ok(body === "", "opaque response body should be empty"); + finish(); + }); +}, function(e) { + my_ok(false, "no-cors Fetch failed"); + finish(); +}); + +expectAsyncResult(); +fetch('opaque-on-same-origin') +.then(function(res) { + my_ok(false, "intercepted opaque response for non no-cors request should fail."); + finish(); +}, function(e) { + my_ok(true, "intercepted opaque response for non no-cors request should fail."); + finish(); +}); + +expectAsyncResult(); +fetch('http://example.com/opaque-no-cors', { mode: "no-cors" }) +.then(function(res) { + my_ok(res.type == "opaque", "intercepted opaque response for no-cors request should have type opaque."); + finish(); +}, function(e) { + my_ok(false, "intercepted opaque response for no-cors request should pass."); + finish(); +}); + +expectAsyncResult(); +fetch('http://example.com/cors-for-no-cors', { mode: "no-cors" }) +.then(function(res) { + my_ok(res.type == "opaque", "intercepted non-opaque response for no-cors request should resolve to opaque response."); + finish(); +}, function(e) { + my_ok(false, "intercepted non-opaque response for no-cors request should resolve to opaque response. It should not fail."); + finish(); +}); + +function arrayBufferFromString(str) { + var arr = new Uint8Array(str.length); + for (var i = 0; i < str.length; ++i) { + arr[i] = str.charCodeAt(i); + } + return arr; +} + +expectAsyncResult(); +fetch(new Request('body-simple', {method: 'POST', body: 'my body'})) +.then(function(res) { + return res.text(); +}).then(function(body) { + my_ok(body == 'my bodymy body', "the body of the intercepted fetch should be visible in the SW"); + finish(); +}); + +expectAsyncResult(); +fetch(new Request('body-arraybufferview', {method: 'POST', body: arrayBufferFromString('my body')})) +.then(function(res) { + return res.text(); +}).then(function(body) { + my_ok(body == 'my bodymy body', "the ArrayBufferView body of the intercepted fetch should be visible in the SW"); + finish(); +}); + +expectAsyncResult(); +fetch(new Request('body-arraybuffer', {method: 'POST', body: arrayBufferFromString('my body').buffer})) +.then(function(res) { + return res.text(); +}).then(function(body) { + my_ok(body == 'my bodymy body', "the ArrayBuffer body of the intercepted fetch should be visible in the SW"); + finish(); +}); + +expectAsyncResult(); +var usp = new URLSearchParams(); +usp.set("foo", "bar"); +usp.set("baz", "qux"); +fetch(new Request('body-urlsearchparams', {method: 'POST', body: usp})) +.then(function(res) { + return res.text(); +}).then(function(body) { + my_ok(body == 'foo=bar&baz=quxfoo=bar&baz=qux', "the URLSearchParams body of the intercepted fetch should be visible in the SW"); + finish(); +}); + +expectAsyncResult(); +var fd = new FormData(); +fd.set("foo", "bar"); +fd.set("baz", "qux"); +fetch(new Request('body-formdata', {method: 'POST', body: fd})) +.then(function(res) { + return res.text(); +}).then(function(body) { + my_ok(body.indexOf("Content-Disposition: form-data; name=\"foo\"\r\n\r\nbar") < + body.indexOf("Content-Disposition: form-data; name=\"baz\"\r\n\r\nqux"), + "the FormData body of the intercepted fetch should be visible in the SW"); + finish(); +}); + +expectAsyncResult(); +fetch(new Request('body-blob', {method: 'POST', body: new Blob(new String('my body'))})) +.then(function(res) { + return res.text(); +}).then(function(body) { + my_ok(body == 'my bodymy body', "the Blob body of the intercepted fetch should be visible in the SW"); + finish(); +}); + +expectAsyncResult(); +fetch('interrupt.sjs') +.then(function(res) { + my_ok(true, "interrupted fetch succeeded"); + res.text().then(function(body) { + my_ok(false, "interrupted fetch shouldn't have complete body"); + finish(); + }, + function() { + my_ok(true, "interrupted fetch shouldn't have complete body"); + finish(); + }) +}, function(e) { + my_ok(false, "interrupted fetch failed"); + finish(); +}); + +['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'].forEach(function(method) { + fetchXHRWithMethod('xhr-method-test.txt', method, function(xhr) { + my_ok(xhr.status == 200, method + " load should be successful"); + my_ok(xhr.responseText == ("intercepted " + method), method + " load should have synthesized response"); + finish(); + }); +}); + +expectAsyncResult(); +fetch(new Request('empty-header', {headers:{"emptyheader":""}})) +.then(function(res) { + return res.text(); +}).then(function(body) { + my_ok(body == "emptyheader", "The empty header was observed in the fetch event"); + finish(); +}, function(err) { + my_ok(false, "A promise was rejected with " + err); + finish(); +}); + +expectAsyncResult(); +fetch('fetchevent-extendable') +.then(function(res) { + return res.text(); +}).then(function(body) { + my_ok(body == "extendable", "FetchEvent inherits from ExtendableEvent"); + finish(); +}, function(err) { + my_ok(false, "A promise was rejected with " + err); + finish(); +}); + +expectAsyncResult(); +fetch('fetchevent-request') +.then(function(res) { + return res.text(); +}).then(function(body) { + my_ok(body == "non-nullable", "FetchEvent.request must be non-nullable"); + finish(); +}, function(err) { + my_ok(false, "A promise was rejected with " + err); + finish(); +}); diff --git a/dom/workers/test/serviceworkers/fetch/fetch_worker_script.js b/dom/workers/test/serviceworkers/fetch/fetch_worker_script.js new file mode 100644 index 000000000..61efb647c --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/fetch_worker_script.js @@ -0,0 +1,29 @@ +function my_ok(v, msg) { + postMessage({type: "ok", value: v, msg: msg}); +} + +function finish() { + postMessage('finish'); +} + +function expectAsyncResult() { + postMessage('expect'); +} + +expectAsyncResult(); +try { + var success = false; + importScripts("nonexistent_imported_script.js"); +} catch(x) { +} + +my_ok(success, "worker imported script should be intercepted"); +finish(); + +function check_intercepted_script() { + success = true; +} + +importScripts('fetch_tests.js') + +finish(); //corresponds to the gExpected increment before creating this worker diff --git a/dom/workers/test/serviceworkers/fetch/hsts/embedder.html b/dom/workers/test/serviceworkers/fetch/hsts/embedder.html new file mode 100644 index 000000000..c98555423 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/hsts/embedder.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<script> + window.onmessage = function(e) { + window.parent.postMessage(e.data, "*"); + }; +</script> +<iframe src="http://example.com/tests/dom/workers/test/serviceworkers/fetch/hsts/index.html"></iframe> diff --git a/dom/workers/test/serviceworkers/fetch/hsts/hsts_test.js b/dom/workers/test/serviceworkers/fetch/hsts/hsts_test.js new file mode 100644 index 000000000..ab54164ed --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/hsts/hsts_test.js @@ -0,0 +1,11 @@ +self.addEventListener("fetch", function(event) { + if (event.request.url.indexOf("index.html") >= 0) { + event.respondWith(fetch("realindex.html")); + } else if (event.request.url.indexOf("image-20px.png") >= 0) { + if (event.request.url.indexOf("https://") == 0) { + event.respondWith(fetch("image-40px.png")); + } else { + event.respondWith(Response.error()); + } + } +}); diff --git a/dom/workers/test/serviceworkers/fetch/hsts/image-20px.png b/dom/workers/test/serviceworkers/fetch/hsts/image-20px.png Binary files differnew file mode 100644 index 000000000..ae6a8a6b8 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/hsts/image-20px.png diff --git a/dom/workers/test/serviceworkers/fetch/hsts/image-40px.png b/dom/workers/test/serviceworkers/fetch/hsts/image-40px.png Binary files differnew file mode 100644 index 000000000..fe391dc8a --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/hsts/image-40px.png diff --git a/dom/workers/test/serviceworkers/fetch/hsts/image.html b/dom/workers/test/serviceworkers/fetch/hsts/image.html new file mode 100644 index 000000000..cadbdef5a --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/hsts/image.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<script> +onload=function(){ + var img = new Image(); + img.src = "http://example.com/tests/dom/workers/test/serviceworkers/fetch/hsts/image-20px.png"; + img.onload = function() { + window.parent.postMessage({status: "image", data: img.width}, "*"); + }; + img.onerror = function() { + window.parent.postMessage({status: "image", data: "error"}, "*"); + }; +}; +</script> diff --git a/dom/workers/test/serviceworkers/fetch/hsts/realindex.html b/dom/workers/test/serviceworkers/fetch/hsts/realindex.html new file mode 100644 index 000000000..b3d1d527e --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/hsts/realindex.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<script> + var securityInfoPresent = !!SpecialPowers.wrap(document).docShell.currentDocumentChannel.securityInfo; + window.parent.postMessage({status: "protocol", + data: location.protocol, + securityInfoPresent: securityInfoPresent}, + "*"); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/hsts/register.html b/dom/workers/test/serviceworkers/fetch/hsts/register.html new file mode 100644 index 000000000..bcdc146ae --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/hsts/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("hsts_test.js", {scope: "."}); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/hsts/register.html^headers^ b/dom/workers/test/serviceworkers/fetch/hsts/register.html^headers^ new file mode 100644 index 000000000..a46bf65bd --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/hsts/register.html^headers^ @@ -0,0 +1,2 @@ +Cache-Control: no-cache +Strict-Transport-Security: max-age=60 diff --git a/dom/workers/test/serviceworkers/fetch/hsts/unregister.html b/dom/workers/test/serviceworkers/fetch/hsts/unregister.html new file mode 100644 index 000000000..1f13508fa --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/hsts/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/https/clonedresponse/https_test.js b/dom/workers/test/serviceworkers/fetch/https/clonedresponse/https_test.js new file mode 100644 index 000000000..48f7b9307 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/https/clonedresponse/https_test.js @@ -0,0 +1,15 @@ +self.addEventListener("install", function(event) { + event.waitUntil(caches.open("cache").then(function(cache) { + return cache.add("index.html"); + })); +}); + +self.addEventListener("fetch", function(event) { + if (event.request.url.indexOf("index.html") >= 0) { + event.respondWith(new Promise(function(resolve, reject) { + caches.match(event.request).then(function(response) { + resolve(response.clone()); + }); + })); + } +}); diff --git a/dom/workers/test/serviceworkers/fetch/https/clonedresponse/index.html b/dom/workers/test/serviceworkers/fetch/https/clonedresponse/index.html new file mode 100644 index 000000000..a43554844 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/https/clonedresponse/index.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/https/clonedresponse/register.html b/dom/workers/test/serviceworkers/fetch/https/clonedresponse/register.html new file mode 100644 index 000000000..41774f70d --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/https/clonedresponse/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("https_test.js", {scope: "."}); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/https/clonedresponse/unregister.html b/dom/workers/test/serviceworkers/fetch/https/clonedresponse/unregister.html new file mode 100644 index 000000000..1f13508fa --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/https/clonedresponse/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/https/https_test.js b/dom/workers/test/serviceworkers/fetch/https/https_test.js new file mode 100644 index 000000000..6f87bb5ee --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/https/https_test.js @@ -0,0 +1,23 @@ +self.addEventListener("install", function(event) { + event.waitUntil(caches.open("cache").then(function(cache) { + var synth = new Response('<!DOCTYPE html><script>window.parent.postMessage({status: "done-synth-sw"}, "*");</script>', + {headers:{"Content-Type": "text/html"}}); + return Promise.all([ + cache.add("index.html"), + cache.put("synth-sw.html", synth), + ]); + })); +}); + +self.addEventListener("fetch", function(event) { + if (event.request.url.indexOf("index.html") >= 0) { + event.respondWith(caches.match(event.request)); + } else if (event.request.url.indexOf("synth-sw.html") >= 0) { + event.respondWith(caches.match(event.request)); + } else if (event.request.url.indexOf("synth-window.html") >= 0) { + event.respondWith(caches.match(event.request)); + } else if (event.request.url.indexOf("synth.html") >= 0) { + event.respondWith(new Response('<!DOCTYPE html><script>window.parent.postMessage({status: "done-synth"}, "*");</script>', + {headers:{"Content-Type": "text/html"}})); + } +}); diff --git a/dom/workers/test/serviceworkers/fetch/https/index.html b/dom/workers/test/serviceworkers/fetch/https/index.html new file mode 100644 index 000000000..a43554844 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/https/index.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/https/register.html b/dom/workers/test/serviceworkers/fetch/https/register.html new file mode 100644 index 000000000..fa666fe95 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/https/register.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(reg => { + return window.caches.open("cache").then(function(cache) { + var synth = new Response('<!DOCTYPE html><script>window.parent.postMessage({status: "done-synth-window"}, "*");</scri' + 'pt>', + {headers:{"Content-Type": "text/html"}}); + return cache.put('synth-window.html', synth).then(_ => done(reg)); + }); + }); + navigator.serviceWorker.register("https_test.js", {scope: "."}); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/https/unregister.html b/dom/workers/test/serviceworkers/fetch/https/unregister.html new file mode 100644 index 000000000..1f13508fa --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/https/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/imagecache-maxage/image-20px.png b/dom/workers/test/serviceworkers/fetch/imagecache-maxage/image-20px.png Binary files differnew file mode 100644 index 000000000..ae6a8a6b8 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/imagecache-maxage/image-20px.png diff --git a/dom/workers/test/serviceworkers/fetch/imagecache-maxage/image-40px.png b/dom/workers/test/serviceworkers/fetch/imagecache-maxage/image-40px.png Binary files differnew file mode 100644 index 000000000..fe391dc8a --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/imagecache-maxage/image-40px.png diff --git a/dom/workers/test/serviceworkers/fetch/imagecache-maxage/index.html b/dom/workers/test/serviceworkers/fetch/imagecache-maxage/index.html new file mode 100644 index 000000000..426c27a73 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/imagecache-maxage/index.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<script> +var width, url, width2, url2; +function maybeReport() { + if (width !== undefined && url !== undefined && + width2 !== undefined && url2 !== undefined) { + window.parent.postMessage({status: "result", + width: width, + width2: width2, + url: url, + url2: url2}, "*"); + } +} +onload = function() { + width = document.querySelector("img").width; + width2 = document.querySelector("img").width; + maybeReport(); +}; +navigator.serviceWorker.onmessage = function(event) { + if (event.data.suffix == "2") { + url2 = event.data.url; + } else { + url = event.data.url; + } + maybeReport(); +}; +</script> +<img src="image.png"> +<img src="image2.png"> diff --git a/dom/workers/test/serviceworkers/fetch/imagecache-maxage/maxage_test.js b/dom/workers/test/serviceworkers/fetch/imagecache-maxage/maxage_test.js new file mode 100644 index 000000000..1922111df --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/imagecache-maxage/maxage_test.js @@ -0,0 +1,41 @@ +function synthesizeImage(suffix) { + // Serve image-20px for the first page, and image-40px for the second page. + return clients.matchAll().then(clients => { + var url = "image-20px.png"; + clients.forEach(client => { + if (client.url.indexOf("?new") > 0) { + url = "image-40px.png"; + } + client.postMessage({suffix: suffix, url: url}); + }); + return fetch(url); + }).then(response => { + return response.arrayBuffer(); + }).then(ab => { + var headers; + if (suffix == "") { + headers = { + "Content-Type": "image/png", + "Date": "Tue, 1 Jan 1990 01:02:03 GMT", + "Cache-Control": "max-age=1", + }; + } else { + headers = { + "Content-Type": "image/png", + "Cache-Control": "no-cache", + }; + } + return new Response(ab, { + status: 200, + headers: headers, + }); + }); +} + +self.addEventListener("fetch", function(event) { + if (event.request.url.indexOf("image.png") >= 0) { + event.respondWith(synthesizeImage("")); + } else if (event.request.url.indexOf("image2.png") >= 0) { + event.respondWith(synthesizeImage("2")); + } +}); diff --git a/dom/workers/test/serviceworkers/fetch/imagecache-maxage/register.html b/dom/workers/test/serviceworkers/fetch/imagecache-maxage/register.html new file mode 100644 index 000000000..af4dde2e2 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/imagecache-maxage/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("maxage_test.js", {scope: "."}); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/imagecache-maxage/unregister.html b/dom/workers/test/serviceworkers/fetch/imagecache-maxage/unregister.html new file mode 100644 index 000000000..1f13508fa --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/imagecache-maxage/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/imagecache/image-20px.png b/dom/workers/test/serviceworkers/fetch/imagecache/image-20px.png Binary files differnew file mode 100644 index 000000000..ae6a8a6b8 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/imagecache/image-20px.png diff --git a/dom/workers/test/serviceworkers/fetch/imagecache/image-40px.png b/dom/workers/test/serviceworkers/fetch/imagecache/image-40px.png Binary files differnew file mode 100644 index 000000000..fe391dc8a --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/imagecache/image-40px.png diff --git a/dom/workers/test/serviceworkers/fetch/imagecache/imagecache_test.js b/dom/workers/test/serviceworkers/fetch/imagecache/imagecache_test.js new file mode 100644 index 000000000..598d8213f --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/imagecache/imagecache_test.js @@ -0,0 +1,15 @@ +function synthesizeImage() { + return clients.matchAll().then(clients => { + var url = "image-40px.png"; + clients.forEach(client => { + client.postMessage(url); + }); + return fetch(url); + }); +} + +self.addEventListener("fetch", function(event) { + if (event.request.url.indexOf("image-20px.png") >= 0) { + event.respondWith(synthesizeImage()); + } +}); diff --git a/dom/workers/test/serviceworkers/fetch/imagecache/index.html b/dom/workers/test/serviceworkers/fetch/imagecache/index.html new file mode 100644 index 000000000..93b30f184 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/imagecache/index.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<script> +var width, url; +function maybeReport() { + if (width !== undefined && url !== undefined) { + window.parent.postMessage({status: "result", + width: width, + url: url}, "*"); + } +} +onload = function() { + width = document.querySelector("img").width; + maybeReport(); +}; +navigator.serviceWorker.onmessage = function(event) { + url = event.data; + maybeReport(); +}; +</script> +<img src="image-20px.png"> diff --git a/dom/workers/test/serviceworkers/fetch/imagecache/postmortem.html b/dom/workers/test/serviceworkers/fetch/imagecache/postmortem.html new file mode 100644 index 000000000..72a650d26 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/imagecache/postmortem.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<script> +onload = function() { + var width = document.querySelector("img").width; + window.parent.postMessage({status: "postmortem", + width: width}, "*"); +}; +</script> +<img src="image-20px.png"> diff --git a/dom/workers/test/serviceworkers/fetch/imagecache/register.html b/dom/workers/test/serviceworkers/fetch/imagecache/register.html new file mode 100644 index 000000000..f6d1eb382 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/imagecache/register.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<!-- Load the image here to put it in the image cache --> +<img src="image-20px.png"> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("imagecache_test.js", {scope: "."}); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/imagecache/unregister.html b/dom/workers/test/serviceworkers/fetch/imagecache/unregister.html new file mode 100644 index 000000000..1f13508fa --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/imagecache/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/importscript-mixedcontent/https_test.js b/dom/workers/test/serviceworkers/fetch/importscript-mixedcontent/https_test.js new file mode 100644 index 000000000..0f08ba74e --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/importscript-mixedcontent/https_test.js @@ -0,0 +1,28 @@ +function sendResponseToParent(response) { + return ` + <!DOCTYPE html> + <script> + window.parent.postMessage({status: "done", data: "${response}"}, "*"); + </script> + `; +} + +self.addEventListener("fetch", function(event) { + if (event.request.url.indexOf("index.html") >= 0) { + var response = "good"; + try { + importScripts("http://example.org/tests/dom/workers/test/foreign.js"); + } catch(e) { + dump("Got error " + e + " when importing the script\n"); + } + if (response === "good") { + try { + importScripts("/tests/dom/workers/test/redirect_to_foreign.sjs"); + } catch(e) { + dump("Got error " + e + " when importing the script\n"); + } + } + event.respondWith(new Response(sendResponseToParent(response), + {headers: {'Content-Type': 'text/html'}})); + } +}); diff --git a/dom/workers/test/serviceworkers/fetch/importscript-mixedcontent/register.html b/dom/workers/test/serviceworkers/fetch/importscript-mixedcontent/register.html new file mode 100644 index 000000000..41774f70d --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/importscript-mixedcontent/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("https_test.js", {scope: "."}); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/importscript-mixedcontent/unregister.html b/dom/workers/test/serviceworkers/fetch/importscript-mixedcontent/unregister.html new file mode 100644 index 000000000..1f13508fa --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/importscript-mixedcontent/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/index.html b/dom/workers/test/serviceworkers/fetch/index.html new file mode 100644 index 000000000..4db0fb139 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/index.html @@ -0,0 +1,183 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 94048 - test install event.</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<div id="style-test" style="background-color: white"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + function my_ok(result, msg) { + window.opener.postMessage({status: "ok", result: result, message: msg}, "*"); + } + + function check_intercepted_script() { + document.getElementById('intercepted-script').test_result = + document.currentScript == document.getElementById('intercepted-script'); + } + + function fetchXHR(name, onload, onerror, headers) { + gExpected++; + + onload = onload || function() { + my_ok(false, "load should not complete successfully"); + finish(); + }; + onerror = onerror || function() { + my_ok(false, "load should be intercepted successfully"); + finish(); + }; + + var x = new XMLHttpRequest(); + x.open('GET', name, true); + x.onload = function() { onload(x) }; + x.onerror = function() { onerror(x) }; + headers = headers || []; + headers.forEach(function(header) { + x.setRequestHeader(header[0], header[1]); + }); + x.send(); + } + + var gExpected = 0; + var gEncountered = 0; + function finish() { + gEncountered++; + if (gEncountered == gExpected) { + window.opener.postMessage({status: "done"}, "*"); + } + } + + function test_onload(creator, complete) { + gExpected++; + var elem = creator(); + elem.onload = function() { + complete.call(elem); + finish(); + }; + elem.onerror = function() { + my_ok(false, elem.tagName + " load should complete successfully"); + finish(); + }; + document.body.appendChild(elem); + } + + function expectAsyncResult() { + gExpected++; + } + + my_ok(navigator.serviceWorker.controller != null, "should be controlled"); +</script> +<script src="fetch_tests.js"></script> +<script> + test_onload(function() { + var elem = document.createElement('img'); + elem.src = "nonexistent_image.gifs"; + elem.id = 'intercepted-img'; + return elem; + }, function() { + my_ok(this.complete, "image should be complete"); + my_ok(this.naturalWidth == 1 && this.naturalHeight == 1, "image should be 1x1 gif"); + }); + + test_onload(function() { + var elem = document.createElement('script'); + elem.id = 'intercepted-script'; + elem.src = "nonexistent_script.js"; + return elem; + }, function() { + my_ok(this.test_result, "script load should be intercepted"); + }); + + test_onload(function() { + var elem = document.createElement('link'); + elem.href = "nonexistent_stylesheet.css"; + elem.rel = "stylesheet"; + return elem; + }, function() { + var styled = document.getElementById('style-test'); + my_ok(window.getComputedStyle(styled).backgroundColor == 'rgb(0, 0, 0)', + "stylesheet load should be intercepted"); + }); + + test_onload(function() { + var elem = document.createElement('iframe'); + elem.id = 'intercepted-iframe'; + elem.src = "nonexistent_page.html"; + return elem; + }, function() { + my_ok(this.test_result, "iframe load should be intercepted"); + }); + + test_onload(function() { + var elem = document.createElement('iframe'); + elem.id = 'intercepted-iframe-2'; + elem.src = "navigate.html"; + return elem; + }, function() { + my_ok(this.test_result, "iframe should successfully load"); + }); + + gExpected++; + var xmlDoc = document.implementation.createDocument(null, null, null); + xmlDoc.load('load_cross_origin_xml_document_synthetic.xml'); + xmlDoc.onload = function(evt) { + var content = new XMLSerializer().serializeToString(evt.target); + my_ok(!content.includes('parsererror'), "Load synthetic cross origin XML Document should be allowed"); + finish(); + }; + + gExpected++; + var xmlDoc = document.implementation.createDocument(null, null, null); + xmlDoc.load('load_cross_origin_xml_document_cors.xml'); + xmlDoc.onload = function(evt) { + var content = new XMLSerializer().serializeToString(evt.target); + my_ok(!content.includes('parsererror'), "Load CORS cross origin XML Document should be allowed"); + finish(); + }; + + gExpected++; + var xmlDoc = document.implementation.createDocument(null, null, null); + xmlDoc.load('load_cross_origin_xml_document_opaque.xml'); + xmlDoc.onload = function(evt) { + var content = new XMLSerializer().serializeToString(evt.target); + my_ok(content.includes('parsererror'), "Load opaque cross origin XML Document should not be allowed"); + finish(); + }; + + gExpected++; + var worker = new Worker('nonexistent_worker_script.js'); + worker.onmessage = function(e) { + my_ok(e.data == "worker-intercept-success", "worker load intercepted"); + finish(); + }; + worker.onerror = function() { + my_ok(false, "worker load should be intercepted"); + }; + + gExpected++; + var worker = new Worker('fetch_worker_script.js'); + worker.onmessage = function(e) { + if (e.data == "finish") { + finish(); + } else if (e.data == "expect") { + gExpected++; + } else if (e.data.type == "ok") { + my_ok(e.data.value, "Fetch test on worker: " + e.data.msg); + } + }; + worker.onerror = function() { + my_ok(false, "worker should not cause any errors"); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/fetch/interrupt.sjs b/dom/workers/test/serviceworkers/fetch/interrupt.sjs new file mode 100644 index 000000000..f6fe870ef --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/interrupt.sjs @@ -0,0 +1,20 @@ +function handleRequest(request, response) { + var body = "a"; + for (var i = 0; i < 20; i++) { + body += body; + } + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n") + var count = 10; + response.write("Content-Length: " + body.length * count + "\r\n"); + response.write("Content-Type: text/plain; charset=utf-8\r\n"); + response.write("Cache-Control: no-cache, must-revalidate\r\n"); + response.write("\r\n"); + + for (var i = 0; i < count; i++) { + response.write(body); + } + + throw Components.results.NS_BINDING_ABORTED; +} diff --git a/dom/workers/test/serviceworkers/fetch/origin/https/index-https.sjs b/dom/workers/test/serviceworkers/fetch/origin/https/index-https.sjs new file mode 100644 index 000000000..7266925ea --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/origin/https/index-https.sjs @@ -0,0 +1,4 @@ +function handleRequest(request, response) { + response.setStatusLine(null, 308, "Permanent Redirect"); + response.setHeader("Location", "https://example.org/tests/dom/workers/test/serviceworkers/fetch/origin/https/realindex.html", false); +} diff --git a/dom/workers/test/serviceworkers/fetch/origin/https/origin_test.js b/dom/workers/test/serviceworkers/fetch/origin/https/origin_test.js new file mode 100644 index 000000000..9839fc5f0 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/origin/https/origin_test.js @@ -0,0 +1,29 @@ +var prefix = "/tests/dom/workers/test/serviceworkers/fetch/origin/https/"; + +function addOpaqueRedirect(cache, file) { + return fetch(new Request(prefix + file, { redirect: "manual" })).then(function(response) { + return cache.put(prefix + file, response); + }); +} + +self.addEventListener("install", function(event) { + event.waitUntil( + self.caches.open("origin-cache") + .then(c => { + return addOpaqueRedirect(c, 'index-https.sjs'); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + if (event.request.url.indexOf("index-cached-https.sjs") >= 0) { + event.respondWith( + self.caches.open("origin-cache") + .then(c => { + return c.match(prefix + 'index-https.sjs'); + }) + ); + } else { + event.respondWith(fetch(event.request)); + } +}); diff --git a/dom/workers/test/serviceworkers/fetch/origin/https/realindex.html b/dom/workers/test/serviceworkers/fetch/origin/https/realindex.html new file mode 100644 index 000000000..87f348945 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/origin/https/realindex.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<script> + window.opener.postMessage({status: "domain", data: document.domain}, "*"); + window.opener.postMessage({status: "origin", data: location.origin}, "*"); + window.opener.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/origin/https/realindex.html^headers^ b/dom/workers/test/serviceworkers/fetch/origin/https/realindex.html^headers^ new file mode 100644 index 000000000..5ed82fd06 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/origin/https/realindex.html^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: https://example.com diff --git a/dom/workers/test/serviceworkers/fetch/origin/https/register.html b/dom/workers/test/serviceworkers/fetch/origin/https/register.html new file mode 100644 index 000000000..2e99adba5 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/origin/https/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("origin_test.js", {scope: "."}); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/origin/https/unregister.html b/dom/workers/test/serviceworkers/fetch/origin/https/unregister.html new file mode 100644 index 000000000..1f13508fa --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/origin/https/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/origin/index-to-https.sjs b/dom/workers/test/serviceworkers/fetch/origin/index-to-https.sjs new file mode 100644 index 000000000..1cc916ff3 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/origin/index-to-https.sjs @@ -0,0 +1,4 @@ +function handleRequest(request, response) { + response.setStatusLine(null, 308, "Permanent Redirect"); + response.setHeader("Location", "https://example.org/tests/dom/workers/test/serviceworkers/fetch/origin/realindex.html", false); +} diff --git a/dom/workers/test/serviceworkers/fetch/origin/index.sjs b/dom/workers/test/serviceworkers/fetch/origin/index.sjs new file mode 100644 index 000000000..a79588e76 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/origin/index.sjs @@ -0,0 +1,4 @@ +function handleRequest(request, response) { + response.setStatusLine(null, 308, "Permanent Redirect"); + response.setHeader("Location", "http://example.org/tests/dom/workers/test/serviceworkers/fetch/origin/realindex.html", false); +} diff --git a/dom/workers/test/serviceworkers/fetch/origin/origin_test.js b/dom/workers/test/serviceworkers/fetch/origin/origin_test.js new file mode 100644 index 000000000..d2be9573b --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/origin/origin_test.js @@ -0,0 +1,41 @@ +var prefix = "/tests/dom/workers/test/serviceworkers/fetch/origin/"; + +function addOpaqueRedirect(cache, file) { + return fetch(new Request(prefix + file, { redirect: "manual" })).then(function(response) { + return cache.put(prefix + file, response); + }); +} + +self.addEventListener("install", function(event) { + event.waitUntil( + self.caches.open("origin-cache") + .then(c => { + return Promise.all( + [ + addOpaqueRedirect(c, 'index.sjs'), + addOpaqueRedirect(c, 'index-to-https.sjs') + ] + ); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + if (event.request.url.indexOf("index-cached.sjs") >= 0) { + event.respondWith( + self.caches.open("origin-cache") + .then(c => { + return c.match(prefix + 'index.sjs'); + }) + ); + } else if (event.request.url.indexOf("index-to-https-cached.sjs") >= 0) { + event.respondWith( + self.caches.open("origin-cache") + .then(c => { + return c.match(prefix + 'index-to-https.sjs'); + }) + ); + } else { + event.respondWith(fetch(event.request)); + } +}); diff --git a/dom/workers/test/serviceworkers/fetch/origin/realindex.html b/dom/workers/test/serviceworkers/fetch/origin/realindex.html new file mode 100644 index 000000000..87f348945 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/origin/realindex.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<script> + window.opener.postMessage({status: "domain", data: document.domain}, "*"); + window.opener.postMessage({status: "origin", data: location.origin}, "*"); + window.opener.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/origin/realindex.html^headers^ b/dom/workers/test/serviceworkers/fetch/origin/realindex.html^headers^ new file mode 100644 index 000000000..3a6a85d89 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/origin/realindex.html^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: http://mochi.test:8888 diff --git a/dom/workers/test/serviceworkers/fetch/origin/register.html b/dom/workers/test/serviceworkers/fetch/origin/register.html new file mode 100644 index 000000000..2e99adba5 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/origin/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("origin_test.js", {scope: "."}); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/origin/unregister.html b/dom/workers/test/serviceworkers/fetch/origin/unregister.html new file mode 100644 index 000000000..1f13508fa --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/origin/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/plugin/plugins.html b/dom/workers/test/serviceworkers/fetch/plugin/plugins.html new file mode 100644 index 000000000..78e31b3c2 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/plugin/plugins.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<script> + var obj, embed; + + function ok(v, msg) { + window.opener.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function finish() { + document.documentElement.removeChild(obj); + document.documentElement.removeChild(embed); + window.opener.postMessage({status: "done"}, "*"); + } + + function test_object() { + obj = document.createElement("object"); + obj.setAttribute('data', "object"); + document.documentElement.appendChild(obj); + } + + function test_embed() { + embed = document.createElement("embed"); + embed.setAttribute('src', "embed"); + document.documentElement.appendChild(embed); + } + + navigator.serviceWorker.addEventListener("message", function onMessage(e) { + if (e.data.context === "object") { + ok(false, "<object> should not be intercepted"); + } else if (e.data.context === "embed") { + ok(false, "<embed> should not be intercepted"); + } else if (e.data.context === "fetch" && e.data.resource === "foo.txt") { + navigator.serviceWorker.removeEventListener("message", onMessage); + finish(); + } + }, false); + + test_object(); + test_embed(); + // SW will definitely intercept fetch API, use this to see if plugins are + // intercepted before fetch(). + fetch("foo.txt"); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/plugin/worker.js b/dom/workers/test/serviceworkers/fetch/plugin/worker.js new file mode 100644 index 000000000..e97d06205 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/plugin/worker.js @@ -0,0 +1,14 @@ +self.addEventListener("fetch", function(event) { + var resource = event.request.url.split('/').pop(); + event.waitUntil( + clients.matchAll() + .then(clients => { + clients.forEach(client => { + if (client.url.includes("plugins.html")) { + client.postMessage({context: event.request.context, + resource: resource}); + } + }); + }) + ); +}); diff --git a/dom/workers/test/serviceworkers/fetch/real-file.txt b/dom/workers/test/serviceworkers/fetch/real-file.txt new file mode 100644 index 000000000..3ca2088ec --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/real-file.txt @@ -0,0 +1 @@ +This is a real file. diff --git a/dom/workers/test/serviceworkers/fetch/redirect.sjs b/dom/workers/test/serviceworkers/fetch/redirect.sjs new file mode 100644 index 000000000..dab558f4a --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/redirect.sjs @@ -0,0 +1,4 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", "synthesized-redirect-twice-real-file.txt"); +} diff --git a/dom/workers/test/serviceworkers/fetch/requesturl/index.html b/dom/workers/test/serviceworkers/fetch/requesturl/index.html new file mode 100644 index 000000000..bc3e400a9 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/requesturl/index.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.onmessage = window.onmessage = e => { + window.parent.postMessage(e.data, "*"); + }; +</script> +<iframe src="redirector.html"></iframe> diff --git a/dom/workers/test/serviceworkers/fetch/requesturl/redirect.sjs b/dom/workers/test/serviceworkers/fetch/requesturl/redirect.sjs new file mode 100644 index 000000000..7b92fec20 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/requesturl/redirect.sjs @@ -0,0 +1,4 @@ +function handleRequest(request, response) { + response.setStatusLine(null, 308, "Permanent Redirect"); + response.setHeader("Location", "http://example.org/tests/dom/workers/test/serviceworkers/fetch/requesturl/secret.html", false); +} diff --git a/dom/workers/test/serviceworkers/fetch/requesturl/redirector.html b/dom/workers/test/serviceworkers/fetch/requesturl/redirector.html new file mode 100644 index 000000000..73bf4af49 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/requesturl/redirector.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<meta http-equiv="refresh" content="3;URL=/tests/dom/workers/test/serviceworkers/fetch/requesturl/redirect.sjs"> diff --git a/dom/workers/test/serviceworkers/fetch/requesturl/register.html b/dom/workers/test/serviceworkers/fetch/requesturl/register.html new file mode 100644 index 000000000..19a2e022c --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/requesturl/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("requesturl_test.js", {scope: "."}); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/requesturl/requesturl_test.js b/dom/workers/test/serviceworkers/fetch/requesturl/requesturl_test.js new file mode 100644 index 000000000..c8be3daf4 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/requesturl/requesturl_test.js @@ -0,0 +1,17 @@ +addEventListener("fetch", event => { + var url = event.request.url; + var badURL = url.indexOf("secret.html") > -1; + event.respondWith( + new Promise(resolve => { + clients.matchAll().then(clients => { + for (var client of clients) { + if (client.url.indexOf("index.html") > -1) { + client.postMessage({status: "ok", result: !badURL, message: "Should not find a bad URL (" + url + ")"}); + break; + } + } + resolve(fetch(event.request)); + }); + }) + ); +}); diff --git a/dom/workers/test/serviceworkers/fetch/requesturl/secret.html b/dom/workers/test/serviceworkers/fetch/requesturl/secret.html new file mode 100644 index 000000000..694c33635 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/requesturl/secret.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +secret stuff +<script> + window.parent.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/requesturl/unregister.html b/dom/workers/test/serviceworkers/fetch/requesturl/unregister.html new file mode 100644 index 000000000..1f13508fa --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/requesturl/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/sandbox/index.html b/dom/workers/test/serviceworkers/fetch/sandbox/index.html new file mode 100644 index 000000000..1094a3995 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/sandbox/index.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({status: "ok", result: true, message: "The iframe is not being intercepted"}, "*"); + window.parent.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/sandbox/intercepted_index.html b/dom/workers/test/serviceworkers/fetch/sandbox/intercepted_index.html new file mode 100644 index 000000000..87261a495 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/sandbox/intercepted_index.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({status: "ok", result: false, message: "The iframe is being intercepted"}, "*"); + window.parent.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/sandbox/register.html b/dom/workers/test/serviceworkers/fetch/sandbox/register.html new file mode 100644 index 000000000..427b1a8da --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/sandbox/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("sandbox_test.js", {scope: "."}); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/sandbox/sandbox_test.js b/dom/workers/test/serviceworkers/fetch/sandbox/sandbox_test.js new file mode 100644 index 000000000..1ed351794 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/sandbox/sandbox_test.js @@ -0,0 +1,5 @@ +self.addEventListener("fetch", function(event) { + if (event.request.url.indexOf("index.html") >= 0) { + event.respondWith(fetch("intercepted_index.html")); + } +}); diff --git a/dom/workers/test/serviceworkers/fetch/sandbox/unregister.html b/dom/workers/test/serviceworkers/fetch/sandbox/unregister.html new file mode 100644 index 000000000..1f13508fa --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/sandbox/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/upgrade-insecure/embedder.html b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/embedder.html new file mode 100644 index 000000000..6098a45dd --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/embedder.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<script> + window.onmessage = function(e) { + window.parent.postMessage(e.data, "*"); + if (e.data.status == "protocol") { + document.querySelector("iframe").src = "image.html"; + } + }; +</script> +<iframe src="http://example.com/tests/dom/workers/test/serviceworkers/fetch/upgrade-insecure/index.html"></iframe> diff --git a/dom/workers/test/serviceworkers/fetch/upgrade-insecure/embedder.html^headers^ b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/embedder.html^headers^ new file mode 100644 index 000000000..602d9dc38 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/embedder.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: upgrade-insecure-requests diff --git a/dom/workers/test/serviceworkers/fetch/upgrade-insecure/image-20px.png b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/image-20px.png Binary files differnew file mode 100644 index 000000000..ae6a8a6b8 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/image-20px.png diff --git a/dom/workers/test/serviceworkers/fetch/upgrade-insecure/image-40px.png b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/image-40px.png Binary files differnew file mode 100644 index 000000000..fe391dc8a --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/image-40px.png diff --git a/dom/workers/test/serviceworkers/fetch/upgrade-insecure/image.html b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/image.html new file mode 100644 index 000000000..34e24e35a --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/image.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<script> +onload=function(){ + var img = new Image(); + img.src = "http://example.com/tests/dom/workers/test/serviceworkers/fetch/upgrade-insecure/image-20px.png"; + img.onload = function() { + window.parent.postMessage({status: "image", data: img.width}, "*"); + }; + img.onerror = function() { + window.parent.postMessage({status: "image", data: "error"}, "*"); + }; +}; +</script> diff --git a/dom/workers/test/serviceworkers/fetch/upgrade-insecure/realindex.html b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/realindex.html new file mode 100644 index 000000000..aaa255aad --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/realindex.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({status: "protocol", data: location.protocol}, "*"); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/upgrade-insecure/register.html b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/register.html new file mode 100644 index 000000000..6309b9b21 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("upgrade-insecure_test.js", {scope: "."}); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/upgrade-insecure/unregister.html b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/unregister.html new file mode 100644 index 000000000..1f13508fa --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/workers/test/serviceworkers/fetch/upgrade-insecure/upgrade-insecure_test.js b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/upgrade-insecure_test.js new file mode 100644 index 000000000..ab54164ed --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/upgrade-insecure/upgrade-insecure_test.js @@ -0,0 +1,11 @@ +self.addEventListener("fetch", function(event) { + if (event.request.url.indexOf("index.html") >= 0) { + event.respondWith(fetch("realindex.html")); + } else if (event.request.url.indexOf("image-20px.png") >= 0) { + if (event.request.url.indexOf("https://") == 0) { + event.respondWith(fetch("image-40px.png")); + } else { + event.respondWith(Response.error()); + } + } +}); diff --git a/dom/workers/test/serviceworkers/fetch_event_worker.js b/dom/workers/test/serviceworkers/fetch_event_worker.js new file mode 100644 index 000000000..1caef71e8 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch_event_worker.js @@ -0,0 +1,337 @@ +var seenIndex = false; + +onfetch = function(ev) { + if (ev.request.url.includes("ignore")) { + return; + } + + if (ev.request.url.includes("bare-synthesized.txt")) { + ev.respondWith(Promise.resolve( + new Response("synthesized response body", {}) + )); + } + + else if (ev.request.url.includes('file_CrossSiteXHR_server.sjs')) { + // N.B. this response would break the rules of CORS if it were allowed, but + // this test relies upon the preflight request not being intercepted and + // thus this response should not be used. + if (ev.request.method == 'OPTIONS') { + ev.respondWith(new Response('', {headers: {'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'X-Unsafe'}})) + } else if (ev.request.url.includes('example.org')) { + ev.respondWith(fetch(ev.request)); + } + } + + else if (ev.request.url.includes("synthesized-404.txt")) { + ev.respondWith(Promise.resolve( + new Response("synthesized response body", { status: 404 }) + )); + } + + else if (ev.request.url.includes("synthesized-headers.txt")) { + ev.respondWith(Promise.resolve( + new Response("synthesized response body", { + headers: { + "X-Custom-Greeting": "Hello" + } + }) + )); + } + + else if (ev.request.url.includes("test-respondwith-response.txt")) { + ev.respondWith(new Response("test-respondwith-response response body", {})); + } + + else if (ev.request.url.includes("synthesized-redirect-real-file.txt")) { + ev.respondWith(Promise.resolve( + Response.redirect("fetch/real-file.txt") + )); + } + + else if (ev.request.url.includes("synthesized-redirect-twice-real-file.txt")) { + ev.respondWith(Promise.resolve( + Response.redirect("synthesized-redirect-real-file.txt") + )); + } + + else if (ev.request.url.includes("synthesized-redirect-synthesized.txt")) { + ev.respondWith(Promise.resolve( + Response.redirect("bare-synthesized.txt") + )); + } + + else if (ev.request.url.includes("synthesized-redirect-twice-synthesized.txt")) { + ev.respondWith(Promise.resolve( + Response.redirect("synthesized-redirect-synthesized.txt") + )); + } + + else if (ev.request.url.includes("rejected.txt")) { + ev.respondWith(Promise.reject()); + } + + else if (ev.request.url.includes("nonresponse.txt")) { + ev.respondWith(Promise.resolve(5)); + } + + else if (ev.request.url.includes("nonresponse2.txt")) { + ev.respondWith(Promise.resolve({})); + } + + else if (ev.request.url.includes("nonpromise.txt")) { + try { + // This should coerce to Promise(5) instead of throwing + ev.respondWith(5); + } catch (e) { + // test is expecting failure, so return a success if we get a thrown + // exception + ev.respondWith(new Response('respondWith(5) threw ' + e)); + } + } + + else if (ev.request.url.includes("headers.txt")) { + var ok = true; + ok &= ev.request.headers.get("X-Test1") == "header1"; + ok &= ev.request.headers.get("X-Test2") == "header2"; + ev.respondWith(Promise.resolve( + new Response(ok.toString(), {}) + )); + } + + else if (ev.request.url.includes('user-pass')) { + ev.respondWith(new Response(ev.request.url)); + } + + else if (ev.request.url.includes("nonexistent_image.gif")) { + var imageAsBinaryString = atob("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs"); + var imageLength = imageAsBinaryString.length; + + // If we just pass |imageAsBinaryString| to the Response constructor, an + // encoding conversion occurs that corrupts the image. Instead, we need to + // convert it to a typed array. + // typed array. + var imageAsArray = new Uint8Array(imageLength); + for (var i = 0; i < imageLength; ++i) { + imageAsArray[i] = imageAsBinaryString.charCodeAt(i); + } + + ev.respondWith(Promise.resolve( + new Response(imageAsArray, { headers: { "Content-Type": "image/gif" } }) + )); + } + + else if (ev.request.url.includes("nonexistent_script.js")) { + ev.respondWith(Promise.resolve( + new Response("check_intercepted_script();", {}) + )); + } + + else if (ev.request.url.includes("nonexistent_stylesheet.css")) { + ev.respondWith(Promise.resolve( + new Response("#style-test { background-color: black !important; }", { + headers : { + "Content-Type": "text/css" + } + }) + )); + } + + else if (ev.request.url.includes("nonexistent_page.html")) { + ev.respondWith(Promise.resolve( + new Response("<script>window.frameElement.test_result = true;</script>", { + headers : { + "Content-Type": "text/html" + } + }) + )); + } + + else if (ev.request.url.includes("navigate.html")) { + var navigateModeCorrectlyChecked = false; + var requests = [ // should not throw + new Request(ev.request), + new Request(ev.request, undefined), + new Request(ev.request, null), + new Request(ev.request, {}), + new Request(ev.request, {someUnrelatedProperty: 42}), + ]; + try { + var request3 = new Request(ev.request, {method: "GET"}); // should throw + } catch(e) { + navigateModeCorrectlyChecked = requests[0].mode == "navigate"; + } + if (navigateModeCorrectlyChecked) { + ev.respondWith(Promise.resolve( + new Response("<script>window.frameElement.test_result = true;</script>", { + headers : { + "Content-Type": "text/html" + } + }) + )); + } + } + + else if (ev.request.url.includes("nonexistent_worker_script.js")) { + ev.respondWith(Promise.resolve( + new Response("postMessage('worker-intercept-success')", {}) + )); + } + + else if (ev.request.url.includes("nonexistent_imported_script.js")) { + ev.respondWith(Promise.resolve( + new Response("check_intercepted_script();", {}) + )); + } + + else if (ev.request.url.includes("deliver-gzip")) { + // Don't handle the request, this will make Necko perform a network request, at + // which point SetApplyConversion must be re-enabled, otherwise the request + // will fail. + return; + } + + else if (ev.request.url.includes("hello.gz")) { + ev.respondWith(fetch("fetch/deliver-gzip.sjs")); + } + + else if (ev.request.url.includes("hello-after-extracting.gz")) { + ev.respondWith(fetch("fetch/deliver-gzip.sjs").then(function(res) { + return res.text().then(function(body) { + return new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers }); + }); + })); + } + + else if (ev.request.url.includes('opaque-on-same-origin')) { + var url = 'http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200'; + ev.respondWith(fetch(url, { mode: 'no-cors' })); + } + + else if (ev.request.url.includes('opaque-no-cors')) { + if (ev.request.mode != "no-cors") { + ev.respondWith(Promise.reject()); + return; + } + + var url = 'http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200'; + ev.respondWith(fetch(url, { mode: ev.request.mode })); + } + + else if (ev.request.url.includes('cors-for-no-cors')) { + if (ev.request.mode != "no-cors") { + ev.respondWith(Promise.reject()); + return; + } + + var url = 'http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200&allowOrigin=*'; + ev.respondWith(fetch(url)); + } + + else if (ev.request.url.includes('example.com')) { + ev.respondWith(fetch(ev.request)); + } + + else if (ev.request.url.includes("index.html")) { + if (seenIndex) { + var body = "<script>" + + "opener.postMessage({status: 'ok', result: " + ev.isReload + "," + + "message: 'reload status should be indicated'}, '*');" + + "opener.postMessage({status: 'done'}, '*');" + + "</script>"; + ev.respondWith(new Response(body, {headers: {'Content-Type': 'text/html'}})); + } else { + seenIndex = true; + ev.respondWith(fetch(ev.request.url)); + } + } + + else if (ev.request.url.includes("body-")) { + ev.respondWith(ev.request.text().then(function (body) { + return new Response(body + body); + })); + } + + else if (ev.request.url.includes('something.txt')) { + ev.respondWith(Response.redirect('fetch/somethingelse.txt')); + } + + else if (ev.request.url.includes('somethingelse.txt')) { + ev.respondWith(new Response('something else response body', {})); + } + + else if (ev.request.url.includes('redirect_serviceworker.sjs')) { + // The redirect_serviceworker.sjs server-side JavaScript file redirects to + // 'http://mochi.test:8888/tests/dom/workers/test/serviceworkers/worker.js' + // The redirected fetch should not go through the SW since the original + // fetch was initiated from a SW. + ev.respondWith(fetch('redirect_serviceworker.sjs')); + } + + else if (ev.request.url.includes('load_cross_origin_xml_document_synthetic.xml')) { + if (ev.request.mode != 'same-origin') { + ev.respondWith(Promise.reject()); + return; + } + + ev.respondWith(Promise.resolve( + new Response("<response>body</response>", { headers: {'Content-Type': 'text/xtml'}}) + )); + } + + else if (ev.request.url.includes('load_cross_origin_xml_document_cors.xml')) { + if (ev.request.mode != 'same-origin') { + ev.respondWith(Promise.reject()); + return; + } + + var url = 'http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200&allowOrigin=*'; + ev.respondWith(fetch(url, { mode: 'cors' })); + } + + else if (ev.request.url.includes('load_cross_origin_xml_document_opaque.xml')) { + if (ev.request.mode != 'same-origin') { + Promise.resolve( + new Response("<error>Invalid Request mode</error>", { headers: {'Content-Type': 'text/xtml'}}) + ); + return; + } + + var url = 'http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200'; + ev.respondWith(fetch(url, { mode: 'no-cors' })); + } + + else if (ev.request.url.includes('xhr-method-test.txt')) { + ev.respondWith(new Response('intercepted ' + ev.request.method)); + } + + else if (ev.request.url.includes('empty-header')) { + if (!ev.request.headers.has("emptyheader") || + ev.request.headers.get("emptyheader") !== "") { + ev.respondWith(Promise.reject()); + return; + } + ev.respondWith(new Response("emptyheader")); + } + + else if (ev.request.url.includes('fetchevent-extendable')) { + if (ev instanceof ExtendableEvent) { + ev.respondWith(new Response("extendable")); + } else { + ev.respondWith(Promise.reject()); + } + } + + else if (ev.request.url.includes('fetchevent-request')) { + var threw = false; + try { + new FetchEvent("foo"); + } catch(e) { + if (e.name == "TypeError") { + threw = true; + } + } finally { + ev.respondWith(new Response(threw ? "non-nullable" : "nullable")); + } + } +}; diff --git a/dom/workers/test/serviceworkers/file_blob_response_worker.js b/dom/workers/test/serviceworkers/file_blob_response_worker.js new file mode 100644 index 000000000..4b4379d0b --- /dev/null +++ b/dom/workers/test/serviceworkers/file_blob_response_worker.js @@ -0,0 +1,38 @@ +function makeFileBlob(obj) { + return new Promise(function(resolve, reject) { + var request = indexedDB.open('file_blob_response_worker', 1); + request.onerror = reject; + request.onupgradeneeded = function(evt) { + var db = evt.target.result; + db.onerror = reject; + + var objectStore = db.createObjectStore('test', { autoIncrement: true }); + var index = objectStore.createIndex('test', 'index'); + }; + + request.onsuccess = function(evt) { + var db = evt.target.result; + db.onerror = reject; + + var blob = new Blob([JSON.stringify(obj)], + { type: 'application/json' }); + var data = { blob: blob, index: 5 }; + + objectStore = db.transaction('test', 'readwrite').objectStore('test'); + objectStore.add(data).onsuccess = function(evt) { + var key = evt.target.result; + objectStore = db.transaction('test').objectStore('test'); + objectStore.get(key).onsuccess = function(evt) { + resolve(evt.target.result.blob); + }; + }; + }; + }); +} + +self.addEventListener('fetch', function(evt) { + var result = { value: 'success' }; + evt.respondWith(makeFileBlob(result).then(function(blob) { + return new Response(blob) + })); +}); diff --git a/dom/workers/test/serviceworkers/force_refresh_browser_worker.js b/dom/workers/test/serviceworkers/force_refresh_browser_worker.js new file mode 100644 index 000000000..96d9d0f17 --- /dev/null +++ b/dom/workers/test/serviceworkers/force_refresh_browser_worker.js @@ -0,0 +1,34 @@ +var name = 'browserRefresherCache'; + +self.addEventListener('install', function(event) { + event.waitUntil( + Promise.all([caches.open(name), + fetch('./browser_cached_force_refresh.html')]).then(function(results) { + var cache = results[0]; + var response = results[1]; + return cache.put('./browser_base_force_refresh.html', response); + }) + ); +}); + +self.addEventListener('fetch', function (event) { + event.respondWith( + caches.open(name).then(function(cache) { + return cache.match(event.request); + }).then(function(response) { + return response || fetch(event.request); + }) + ); +}); + +self.addEventListener('message', function (event) { + if (event.data.type === 'GET_UNCONTROLLED_CLIENTS') { + event.waitUntil(clients.matchAll({ includeUncontrolled: true }) + .then(function(clientList) { + var resultList = clientList.map(function(c) { + return { url: c.url, frameType: c.frameType }; + }); + event.source.postMessage({ type: 'CLIENTS', detail: resultList }); + })); + } +}); diff --git a/dom/workers/test/serviceworkers/force_refresh_worker.js b/dom/workers/test/serviceworkers/force_refresh_worker.js new file mode 100644 index 000000000..f0752d0cb --- /dev/null +++ b/dom/workers/test/serviceworkers/force_refresh_worker.js @@ -0,0 +1,33 @@ +var name = 'refresherCache'; + +self.addEventListener('install', function(event) { + event.waitUntil( + Promise.all([caches.open(name), + fetch('./sw_clients/refresher_cached.html'), + fetch('./sw_clients/refresher_cached_compressed.html')]).then(function(results) { + var cache = results[0]; + var response = results[1]; + var compressed = results[2]; + return Promise.all([cache.put('./sw_clients/refresher.html', response), + cache.put('./sw_clients/refresher_compressed.html', compressed)]); + }) + ); +}); + +self.addEventListener('fetch', function (event) { + event.respondWith( + caches.open(name).then(function(cache) { + return cache.match(event.request); + }).then(function(response) { + // If this is one of our primary cached responses, then the window + // must have generated the request via a normal window reload. That + // should be detectable in the event.request.cache attribute. + if (response && event.request.cache !== 'no-cache') { + dump('### ### FetchEvent.request.cache is "' + event.request.cache + + '" instead of expected "no-cache"\n'); + return Response.error(); + } + return response || fetch(event.request); + }) + ); +}); diff --git a/dom/workers/test/serviceworkers/gzip_redirect_worker.js b/dom/workers/test/serviceworkers/gzip_redirect_worker.js new file mode 100644 index 000000000..72aeba222 --- /dev/null +++ b/dom/workers/test/serviceworkers/gzip_redirect_worker.js @@ -0,0 +1,13 @@ +self.addEventListener('fetch', function (event) { + if (!event.request.url.endsWith('sw_clients/does_not_exist.html')) { + return; + } + + event.respondWith(new Response('', { + status: 301, + statusText: 'Moved Permanently', + headers: { + 'Location': 'refresher_compressed.html' + } + })); +}); diff --git a/dom/workers/test/serviceworkers/header_checker.sjs b/dom/workers/test/serviceworkers/header_checker.sjs new file mode 100644 index 000000000..706104103 --- /dev/null +++ b/dom/workers/test/serviceworkers/header_checker.sjs @@ -0,0 +1,9 @@ +function handleRequest(request, response) { + if (request.getHeader("Service-Worker") === "script") { + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Content-Type", "text/javascript"); + response.write("// empty"); + } else { + response.setStatusLine("1.1", 404, "Not Found"); + } +} diff --git a/dom/workers/test/serviceworkers/hello.html b/dom/workers/test/serviceworkers/hello.html new file mode 100644 index 000000000..97eb03c90 --- /dev/null +++ b/dom/workers/test/serviceworkers/hello.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + </head> + <body> + Hello. + </body> +<html> diff --git a/dom/workers/test/serviceworkers/importscript.sjs b/dom/workers/test/serviceworkers/importscript.sjs new file mode 100644 index 000000000..6d177a734 --- /dev/null +++ b/dom/workers/test/serviceworkers/importscript.sjs @@ -0,0 +1,11 @@ +function handleRequest(request, response) { + if (request.queryString == 'clearcounter') { + setState('counter', ''); + } else if (!getState('counter')) { + response.setHeader("Content-Type", "application/javascript", false); + response.write("callByScript();"); + setState('counter', '1'); + } else { + response.write("no cache no party!"); + } +} diff --git a/dom/workers/test/serviceworkers/importscript_worker.js b/dom/workers/test/serviceworkers/importscript_worker.js new file mode 100644 index 000000000..3cddec194 --- /dev/null +++ b/dom/workers/test/serviceworkers/importscript_worker.js @@ -0,0 +1,37 @@ +var counter = 0; +function callByScript() { + ++counter; +} + +// Use multiple scripts in this load to verify we support that case correctly. +// See bug 1249351 for a case where we broke this. +importScripts('lorem_script.js', 'importscript.sjs'); + +importScripts('importscript.sjs'); + +var missingScriptFailed = false; +try { + importScripts(['there-is-nothing-here.js']); +} catch(e) { + missingScriptFailed = true; +} + +onmessage = function(e) { + self.clients.matchAll().then(function(res) { + if (!res.length) { + dump("ERROR: no clients are currently controlled.\n"); + } + + if (!missingScriptFailed) { + res[0].postMessage("KO"); + } + + try { + importScripts(['importscript.sjs']); + res[0].postMessage("KO"); + return; + } catch(e) {} + + res[0].postMessage(counter == 2 ? "OK" : "KO"); + }); +}; diff --git a/dom/workers/test/serviceworkers/install_event_error_worker.js b/dom/workers/test/serviceworkers/install_event_error_worker.js new file mode 100644 index 000000000..c06d648b8 --- /dev/null +++ b/dom/workers/test/serviceworkers/install_event_error_worker.js @@ -0,0 +1,4 @@ +// Worker that errors on receiving an install event. +oninstall = function(e) { + undefined.doSomething; +}; diff --git a/dom/workers/test/serviceworkers/install_event_worker.js b/dom/workers/test/serviceworkers/install_event_worker.js new file mode 100644 index 000000000..f965d28aa --- /dev/null +++ b/dom/workers/test/serviceworkers/install_event_worker.js @@ -0,0 +1,3 @@ +oninstall = function(e) { + dump("Got install event\n"); +} diff --git a/dom/workers/test/serviceworkers/lorem_script.js b/dom/workers/test/serviceworkers/lorem_script.js new file mode 100644 index 000000000..5502a44da --- /dev/null +++ b/dom/workers/test/serviceworkers/lorem_script.js @@ -0,0 +1,8 @@ +var lorem_str = ` +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo +consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum +dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, +sunt in culpa qui officia deserunt mollit anim id est laborum. +` diff --git a/dom/workers/test/serviceworkers/match_all_advanced_worker.js b/dom/workers/test/serviceworkers/match_all_advanced_worker.js new file mode 100644 index 000000000..3721aedfe --- /dev/null +++ b/dom/workers/test/serviceworkers/match_all_advanced_worker.js @@ -0,0 +1,5 @@ +onmessage = function(e) { + self.clients.matchAll().then(function(clients) { + e.source.postMessage(clients.length); + }); +} diff --git a/dom/workers/test/serviceworkers/match_all_client/match_all_client_id.html b/dom/workers/test/serviceworkers/match_all_client/match_all_client_id.html new file mode 100644 index 000000000..7ac6fc9d0 --- /dev/null +++ b/dom/workers/test/serviceworkers/match_all_client/match_all_client_id.html @@ -0,0 +1,31 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1139425 - controlled page</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + window.onload = function() { + navigator.serviceWorker.ready.then(function(swr) { + swr.active.postMessage("Start"); + }); + } + + navigator.serviceWorker.onmessage = function(msg) { + // worker message; + testWindow.postMessage(msg.data, "*"); + window.close(); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/match_all_client_id_worker.js b/dom/workers/test/serviceworkers/match_all_client_id_worker.js new file mode 100644 index 000000000..a7d9ff594 --- /dev/null +++ b/dom/workers/test/serviceworkers/match_all_client_id_worker.js @@ -0,0 +1,28 @@ +onmessage = function(e) { + dump("MatchAllClientIdWorker:" + e.data + "\n"); + var id = []; + var iterations = 5; + var counter = 0; + + for (var i = 0; i < iterations; i++) { + self.clients.matchAll().then(function(res) { + if (!res.length) { + dump("ERROR: no clients are currently controlled.\n"); + } + + client = res[0]; + id[counter] = client.id; + counter++; + if (counter >= iterations) { + var response = true; + for (var index = 1; index < iterations; index++) { + if (id[0] != id[index]) { + response = false; + break; + } + } + client.postMessage(response); + } + }); + } +} diff --git a/dom/workers/test/serviceworkers/match_all_clients/match_all_controlled.html b/dom/workers/test/serviceworkers/match_all_clients/match_all_controlled.html new file mode 100644 index 000000000..25317b9fc --- /dev/null +++ b/dom/workers/test/serviceworkers/match_all_clients/match_all_controlled.html @@ -0,0 +1,65 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1058311 - controlled page</title> +<script class="testbody" type="text/javascript"> + var re = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + var frameType = "none"; + var testWindow = parent; + + if (parent != window) { + frameType = "nested"; + } else if (opener) { + frameType = "auxiliary"; + testWindow = opener; + } else if (parent != window) { + frameType = "top-level"; + } else { + postResult(false, "Unexpected frameType"); + } + + window.onload = function() { + navigator.serviceWorker.ready.then(function(swr) { + swr.active.postMessage("Start"); + }); + } + + function postResult(result, msg) { + response = { + result: result, + message: msg + }; + + testWindow.postMessage(response, "*"); + } + + navigator.serviceWorker.onmessage = function(msg) { + // worker message; + result = re.test(msg.data.id); + postResult(result, "Client id test"); + + result = msg.data.url == window.location; + postResult(result, "Client url test"); + + result = msg.data.visibilityState === document.visibilityState; + postResult(result, "Client visibility test. expected=" +document.visibilityState); + + result = msg.data.focused === document.hasFocus(); + postResult(result, "Client focus test. expected=" + document.hasFocus()); + + result = msg.data.frameType === frameType; + postResult(result, "Client frameType test. expected=" + frameType); + + postResult(true, "DONE"); + window.close(); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/match_all_properties_worker.js b/dom/workers/test/serviceworkers/match_all_properties_worker.js new file mode 100644 index 000000000..f007a5ce8 --- /dev/null +++ b/dom/workers/test/serviceworkers/match_all_properties_worker.js @@ -0,0 +1,20 @@ +onmessage = function(e) { + dump("MatchAllPropertiesWorker:" + e.data + "\n"); + self.clients.matchAll().then(function(res) { + if (!res.length) { + dump("ERROR: no clients are currently controlled.\n"); + } + + for (i = 0; i < res.length; i++) { + client = res[i]; + response = { + id: client.id, + url: client.url, + visibilityState: client.visibilityState, + focused: client.focused, + frameType: client.frameType + }; + client.postMessage(response); + } + }); +} diff --git a/dom/workers/test/serviceworkers/match_all_worker.js b/dom/workers/test/serviceworkers/match_all_worker.js new file mode 100644 index 000000000..9d1c8c363 --- /dev/null +++ b/dom/workers/test/serviceworkers/match_all_worker.js @@ -0,0 +1,10 @@ +function loop() { + self.clients.matchAll().then(function(result) { + setTimeout(loop, 0); + }); +} + +onactivate = function(e) { + // spam matchAll until the worker is closed. + loop(); +} diff --git a/dom/workers/test/serviceworkers/message_posting_worker.js b/dom/workers/test/serviceworkers/message_posting_worker.js new file mode 100644 index 000000000..26db99775 --- /dev/null +++ b/dom/workers/test/serviceworkers/message_posting_worker.js @@ -0,0 +1,8 @@ +onmessage = function(e) { + self.clients.matchAll().then(function(res) { + if (!res.length) { + dump("ERROR: no clients are currently controlled.\n"); + } + res[0].postMessage(e.data); + }); +}; diff --git a/dom/workers/test/serviceworkers/message_receiver.html b/dom/workers/test/serviceworkers/message_receiver.html new file mode 100644 index 000000000..82cb587c7 --- /dev/null +++ b/dom/workers/test/serviceworkers/message_receiver.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.onmessage = function(e) { + window.parent.postMessage(e.data, "*"); + }; +</script> diff --git a/dom/workers/test/serviceworkers/mochitest.ini b/dom/workers/test/serviceworkers/mochitest.ini new file mode 100644 index 000000000..29ac9e036 --- /dev/null +++ b/dom/workers/test/serviceworkers/mochitest.ini @@ -0,0 +1,317 @@ +[DEFAULT] + +support-files = + worker.js + worker2.js + worker3.js + fetch_event_worker.js + parse_error_worker.js + activate_event_error_worker.js + install_event_worker.js + install_event_error_worker.js + simpleregister/index.html + simpleregister/ready.html + controller/index.html + unregister/index.html + unregister/unregister.html + workerUpdate/update.html + sw_clients/simple.html + sw_clients/service_worker_controlled.html + match_all_worker.js + match_all_advanced_worker.js + worker_unregister.js + worker_update.js + message_posting_worker.js + fetch/index.html + fetch/fetch_worker_script.js + fetch/fetch_tests.js + fetch/deliver-gzip.sjs + fetch/redirect.sjs + fetch/real-file.txt + fetch/context/index.html + fetch/context/register.html + fetch/context/unregister.html + fetch/context/context_test.js + fetch/context/realimg.jpg + fetch/context/realaudio.ogg + fetch/context/beacon.sjs + fetch/context/csp-violate.sjs + fetch/context/ping.html + fetch/context/worker.js + fetch/context/parentworker.js + fetch/context/sharedworker.js + fetch/context/parentsharedworker.js + fetch/context/xml.xml + fetch/hsts/hsts_test.js + fetch/hsts/embedder.html + fetch/hsts/image.html + fetch/hsts/image-20px.png + fetch/hsts/image-40px.png + fetch/hsts/realindex.html + fetch/hsts/register.html + fetch/hsts/register.html^headers^ + fetch/hsts/unregister.html + fetch/https/index.html + fetch/https/register.html + fetch/https/unregister.html + fetch/https/https_test.js + fetch/https/clonedresponse/index.html + fetch/https/clonedresponse/register.html + fetch/https/clonedresponse/unregister.html + fetch/https/clonedresponse/https_test.js + fetch/imagecache/image-20px.png + fetch/imagecache/image-40px.png + fetch/imagecache/imagecache_test.js + fetch/imagecache/index.html + fetch/imagecache/postmortem.html + fetch/imagecache/register.html + fetch/imagecache/unregister.html + fetch/imagecache-maxage/index.html + fetch/imagecache-maxage/image-20px.png + fetch/imagecache-maxage/image-40px.png + fetch/imagecache-maxage/maxage_test.js + fetch/imagecache-maxage/register.html + fetch/imagecache-maxage/unregister.html + fetch/importscript-mixedcontent/register.html + fetch/importscript-mixedcontent/unregister.html + fetch/importscript-mixedcontent/https_test.js + fetch/interrupt.sjs + fetch/origin/index.sjs + fetch/origin/index-to-https.sjs + fetch/origin/realindex.html + fetch/origin/realindex.html^headers^ + fetch/origin/register.html + fetch/origin/unregister.html + fetch/origin/origin_test.js + fetch/origin/https/index-https.sjs + fetch/origin/https/realindex.html + fetch/origin/https/realindex.html^headers^ + fetch/origin/https/register.html + fetch/origin/https/unregister.html + fetch/origin/https/origin_test.js + fetch/requesturl/index.html + fetch/requesturl/redirect.sjs + fetch/requesturl/redirector.html + fetch/requesturl/register.html + fetch/requesturl/requesturl_test.js + fetch/requesturl/secret.html + fetch/requesturl/unregister.html + fetch/sandbox/index.html + fetch/sandbox/intercepted_index.html + fetch/sandbox/register.html + fetch/sandbox/unregister.html + fetch/sandbox/sandbox_test.js + fetch/upgrade-insecure/upgrade-insecure_test.js + fetch/upgrade-insecure/embedder.html + fetch/upgrade-insecure/embedder.html^headers^ + fetch/upgrade-insecure/image.html + fetch/upgrade-insecure/image-20px.png + fetch/upgrade-insecure/image-40px.png + fetch/upgrade-insecure/realindex.html + fetch/upgrade-insecure/register.html + fetch/upgrade-insecure/unregister.html + match_all_properties_worker.js + match_all_clients/match_all_controlled.html + test_serviceworker_interfaces.js + serviceworker_wrapper.js + message_receiver.html + close_test.js + serviceworker_not_sharedworker.js + match_all_client/match_all_client_id.html + match_all_client_id_worker.js + source_message_posting_worker.js + scope/scope_worker.js + redirect_serviceworker.sjs + importscript.sjs + importscript_worker.js + bug1151916_worker.js + bug1151916_driver.html + bug1240436_worker.js + notificationclick.html + notificationclick-otherwindow.html + notificationclick.js + notificationclick_focus.html + notificationclick_focus.js + notificationclose.html + notificationclose.js + worker_updatefoundevent.js + worker_updatefoundevent2.js + updatefoundevent.html + empty.js + notification_constructor_error.js + notification_get_sw.js + notification/register.html + notification/unregister.html + notification/listener.html + notification_alt/register.html + notification_alt/unregister.html + sanitize/frame.html + sanitize/register.html + sanitize/example_check_and_unregister.html + sanitize_worker.js + swa/worker_scope_different.js + swa/worker_scope_different.js^headers^ + swa/worker_scope_different2.js + swa/worker_scope_different2.js^headers^ + swa/worker_scope_precise.js + swa/worker_scope_precise.js^headers^ + swa/worker_scope_too_deep.js + swa/worker_scope_too_deep.js^headers^ + swa/worker_scope_too_narrow.js + swa/worker_scope_too_narrow.js^headers^ + claim_oninstall_worker.js + claim_worker_1.js + claim_worker_2.js + claim_clients/client.html + claim_fetch_worker.js + force_refresh_worker.js + sw_clients/refresher.html + sw_clients/refresher_compressed.html + sw_clients/refresher_compressed.html^headers^ + sw_clients/refresher_cached.html + sw_clients/refresher_cached_compressed.html + sw_clients/refresher_cached_compressed.html^headers^ + strict_mode_warning.js + skip_waiting_installed_worker.js + skip_waiting_scope/index.html + thirdparty/iframe1.html + thirdparty/iframe2.html + thirdparty/register.html + thirdparty/unregister.html + thirdparty/sw.js + register_https.html + gzip_redirect_worker.js + sw_clients/navigator.html + eval_worker.js + test_eval_allowed.html^headers^ + opaque_intercept_worker.js + notify_loaded.js + test_request_context.js + fetch/plugin/worker.js + fetch/plugin/plugins.html + eventsource/* + sw_clients/file_blob_upload_frame.html + redirect_post.sjs + xslt_worker.js + xslt/* + unresolved_fetch_worker.js + header_checker.sjs + openWindow_worker.js + redirect.sjs + open_window/client.html + lorem_script.js + file_blob_response_worker.js + !/dom/security/test/cors/file_CrossSiteXHR_server.sjs + !/dom/tests/mochitest/notification/MockServices.js + !/dom/tests/mochitest/notification/NotificationTest.js + blocking_install_event_worker.js + sw_bad_mime_type.js + sw_bad_mime_type.js^headers^ + error_reporting_helpers.js + fetch.js + hello.html + create_another_sharedWorker.html + sharedWorker_fetch.js + +[test_bug1151916.html] +[test_bug1240436.html] +[test_claim.html] +[test_claim_fetch.html] +[test_claim_oninstall.html] +[test_close.html] +[test_controller.html] +[test_cross_origin_url_after_redirect.html] +[test_csp_upgrade-insecure_intercept.html] +[test_empty_serviceworker.html] +[test_error_reporting.html] +[test_escapedSlashes.html] +[test_eval_allowed.html] +[test_eventsource_intercept.html] +[test_fetch_event.html] +skip-if = (debug && e10s) # Bug 1262224 +[test_fetch_integrity.html] +[test_file_blob_response.html] +[test_file_blob_upload.html] +[test_force_refresh.html] +[test_gzip_redirect.html] +[test_hsts_upgrade_intercept.html] +[test_https_fetch.html] +[test_https_fetch_cloned_response.html] +[test_https_origin_after_redirect.html] +[test_https_origin_after_redirect_cached.html] +[test_https_synth_fetch_from_cached_sw.html] +[test_imagecache.html] +[test_imagecache_max_age.html] +[test_importscript.html] +[test_importscript_mixedcontent.html] +tags = mcb +[test_install_event.html] +[test_install_event_gc.html] +[test_installation_simple.html] +[test_match_all.html] +[test_match_all_advanced.html] +[test_match_all_client_id.html] +[test_match_all_client_properties.html] +[test_navigator.html] +[test_not_intercept_plugin.html] +[test_notification_constructor_error.html] +[test_notification_get.html] +[test_notificationclick.html] +[test_notificationclick_focus.html] +[test_notificationclick-otherwindow.html] +[test_notificationclose.html] +[test_opaque_intercept.html] +[test_openWindow.html] +tags = openwindow +[test_origin_after_redirect.html] +[test_origin_after_redirect_cached.html] +[test_origin_after_redirect_to_https.html] +[test_origin_after_redirect_to_https_cached.html] +[test_post_message.html] +[test_post_message_advanced.html] +[test_post_message_source.html] +[test_register_base.html] +[test_register_https_in_http.html] +[test_request_context_audio.html] +[test_request_context_beacon.html] +[test_request_context_cache.html] +[test_request_context_cspreport.html] +[test_request_context_embed.html] +[test_request_context_fetch.html] +[test_request_context_font.html] +[test_request_context_frame.html] +[test_request_context_iframe.html] +[test_request_context_image.html] +[test_request_context_imagesrcset.html] +[test_request_context_internal.html] +[test_request_context_nestedworker.html] +[test_request_context_nestedworkerinsharedworker.html] +[test_request_context_object.html] +[test_request_context_picture.html] +[test_request_context_ping.html] +[test_request_context_plugin.html] +[test_request_context_script.html] +[test_request_context_sharedworker.html] +[test_request_context_style.html] +[test_request_context_track.html] +[test_request_context_video.html] +[test_request_context_worker.html] +[test_request_context_xhr.html] +[test_request_context_xslt.html] +[test_sandbox_intercept.html] +[test_scopes.html] +[test_sanitize.html] +[test_sanitize_domain.html] +[test_service_worker_allowed.html] +[test_serviceworker_header.html] +[test_serviceworker_interfaces.html] +[test_serviceworker_not_sharedworker.html] +[test_skip_waiting.html] +[test_strict_mode_warning.html] +[test_third_party_iframes.html] +[test_unregister.html] +[test_unresolved_fetch_interception.html] +[test_workerUnregister.html] +[test_workerUpdate.html] +[test_workerupdatefoundevent.html] +[test_xslt.html] diff --git a/dom/workers/test/serviceworkers/notification/listener.html b/dom/workers/test/serviceworkers/notification/listener.html new file mode 100644 index 000000000..1c6e282ec --- /dev/null +++ b/dom/workers/test/serviceworkers/notification/listener.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1114554 - proxy to forward messages from SW to test</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + navigator.serviceWorker.onmessage = function(msg) { + // worker message; + testWindow.postMessage(msg.data, "*"); + if (msg.data.type == 'finish') { + window.close(); + } + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/notification/register.html b/dom/workers/test/serviceworkers/notification/register.html new file mode 100644 index 000000000..b7df73bed --- /dev/null +++ b/dom/workers/test/serviceworkers/notification/register.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script> + function done() { + parent.callback(); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("../notification_get_sw.js", {scope: "."}).catch(function(e) { + dump("Registration failure " + e.message + "\n"); + }); +</script> diff --git a/dom/workers/test/serviceworkers/notification/unregister.html b/dom/workers/test/serviceworkers/notification/unregister.html new file mode 100644 index 000000000..d5a141f83 --- /dev/null +++ b/dom/workers/test/serviceworkers/notification/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.callback(); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/workers/test/serviceworkers/notification_alt/register.html b/dom/workers/test/serviceworkers/notification_alt/register.html new file mode 100644 index 000000000..b7df73bed --- /dev/null +++ b/dom/workers/test/serviceworkers/notification_alt/register.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script> + function done() { + parent.callback(); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("../notification_get_sw.js", {scope: "."}).catch(function(e) { + dump("Registration failure " + e.message + "\n"); + }); +</script> diff --git a/dom/workers/test/serviceworkers/notification_alt/unregister.html b/dom/workers/test/serviceworkers/notification_alt/unregister.html new file mode 100644 index 000000000..d5a141f83 --- /dev/null +++ b/dom/workers/test/serviceworkers/notification_alt/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.callback(); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/workers/test/serviceworkers/notification_constructor_error.js b/dom/workers/test/serviceworkers/notification_constructor_error.js new file mode 100644 index 000000000..644dba480 --- /dev/null +++ b/dom/workers/test/serviceworkers/notification_constructor_error.js @@ -0,0 +1 @@ +new Notification("Hi there"); diff --git a/dom/workers/test/serviceworkers/notification_get_sw.js b/dom/workers/test/serviceworkers/notification_get_sw.js new file mode 100644 index 000000000..540c9d93c --- /dev/null +++ b/dom/workers/test/serviceworkers/notification_get_sw.js @@ -0,0 +1,49 @@ +function postAll(data) { + self.clients.matchAll().then(function(clients) { + if (clients.length == 0) { + dump("***************** NO CLIENTS FOUND! Test messages are being lost *******************\n"); + } + clients.forEach(function(client) { + client.postMessage(data); + }); + }); +} + +function ok(a, msg) { + postAll({type: 'status', status: !!a, msg: a + ": " + msg }); +} + +function is(a, b, msg) { + postAll({type: 'status', status: a === b, msg: a + " === " + b + ": " + msg }); +} + +function done() { + postAll({type: 'finish'}); +} + +onmessage = function(e) { +dump("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% MESSAGE " + e.data + "\n"); + var start; + if (e.data == 'create') { + start = registration.showNotification("This is a title"); + } else { + start = Promise.resolve(); + } + + start.then(function() { + dump("CALLING getNotification\n"); + registration.getNotifications().then(function(notifications) { + dump("RECD getNotification\n"); + is(notifications.length, 1, "There should be one stored notification"); + var notification = notifications[0]; + if (!notification) { + done(); + return; + } + ok(notification instanceof Notification, "Should be a Notification"); + is(notification.title, "This is a title", "Title should match"); + notification.close(); + done(); + }); + }); +} diff --git a/dom/workers/test/serviceworkers/notificationclick-otherwindow.html b/dom/workers/test/serviceworkers/notificationclick-otherwindow.html new file mode 100644 index 000000000..f64e82aab --- /dev/null +++ b/dom/workers/test/serviceworkers/notificationclick-otherwindow.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1114554 - controlled page</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + navigator.serviceWorker.ready.then(function(swr) { + var ifr = document.createElement("iframe"); + document.documentElement.appendChild(ifr); + ifr.contentWindow.ServiceWorkerRegistration.prototype.showNotification + .call(swr, "Hi there. The ServiceWorker should receive a click event for this.", { data: { complex: ["jsval", 5] }}); + }); + + navigator.serviceWorker.onmessage = function(msg) { + testWindow.callback(msg.data.result); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/notificationclick.html b/dom/workers/test/serviceworkers/notificationclick.html new file mode 100644 index 000000000..448764a1c --- /dev/null +++ b/dom/workers/test/serviceworkers/notificationclick.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1114554 - controlled page</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + navigator.serviceWorker.ready.then(function(swr) { + swr.showNotification("Hi there. The ServiceWorker should receive a click event for this.", { data: { complex: ["jsval", 5] }}); + }); + + navigator.serviceWorker.onmessage = function(msg) { + testWindow.callback(msg.data.result); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/notificationclick.js b/dom/workers/test/serviceworkers/notificationclick.js new file mode 100644 index 000000000..39e7adb81 --- /dev/null +++ b/dom/workers/test/serviceworkers/notificationclick.js @@ -0,0 +1,19 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +// +onnotificationclick = function(e) { + self.clients.matchAll().then(function(clients) { + if (clients.length === 0) { + dump("********************* CLIENTS LIST EMPTY! Test will timeout! ***********************\n"); + return; + } + + clients.forEach(function(client) { + client.postMessage({ result: e.notification.data && + e.notification.data['complex'] && + e.notification.data['complex'][0] == "jsval" && + e.notification.data['complex'][1] == 5 }); + + }); + }); +} diff --git a/dom/workers/test/serviceworkers/notificationclick_focus.html b/dom/workers/test/serviceworkers/notificationclick_focus.html new file mode 100644 index 000000000..0152d397f --- /dev/null +++ b/dom/workers/test/serviceworkers/notificationclick_focus.html @@ -0,0 +1,28 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1144660 - controlled page</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + navigator.serviceWorker.ready.then(function(swr) { + swr.showNotification("Hi there. The ServiceWorker should receive a click event for this."); + }); + + navigator.serviceWorker.onmessage = function(msg) { + dump("GOT Message " + JSON.stringify(msg.data) + "\n"); + testWindow.callback(msg.data.ok); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/notificationclick_focus.js b/dom/workers/test/serviceworkers/notificationclick_focus.js new file mode 100644 index 000000000..5fb73651e --- /dev/null +++ b/dom/workers/test/serviceworkers/notificationclick_focus.js @@ -0,0 +1,40 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +// + +function promisifyTimerFocus(client, delay) { + return new Promise(function(resolve, reject) { + setTimeout(function() { + client.focus().then(resolve, reject); + }, delay); + }); +} + +onnotificationclick = function(e) { + e.waitUntil(self.clients.matchAll().then(function(clients) { + if (clients.length === 0) { + dump("********************* CLIENTS LIST EMPTY! Test will timeout! ***********************\n"); + return Promise.resolve(); + } + + var immediatePromise = clients[0].focus(); + var withinTimeout = promisifyTimerFocus(clients[0], 100); + + var afterTimeout = promisifyTimerFocus(clients[0], 2000).then(function() { + throw "Should have failed!"; + }, function() { + return Promise.resolve(); + }); + + return Promise.all([immediatePromise, withinTimeout, afterTimeout]).then(function() { + clients.forEach(function(client) { + client.postMessage({ok: true}); + }); + }).catch(function(e) { + dump("Error " + e + "\n"); + clients.forEach(function(client) { + client.postMessage({ok: false}); + }); + }); + })); +} diff --git a/dom/workers/test/serviceworkers/notificationclose.html b/dom/workers/test/serviceworkers/notificationclose.html new file mode 100644 index 000000000..10c8da453 --- /dev/null +++ b/dom/workers/test/serviceworkers/notificationclose.html @@ -0,0 +1,37 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1265841 - controlled page</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + navigator.serviceWorker.ready.then(function(swr) { + return swr.showNotification( + "Hi there. The ServiceWorker should receive a close event for this.", + { data: { complex: ["jsval", 5] }}).then(function() { + return swr; + }); + }).then(function(swr) { + return swr.getNotifications(); + }).then(function(notifications) { + notifications.forEach(function(notification) { + notification.close(); + }); + }); + + navigator.serviceWorker.onmessage = function(msg) { + testWindow.callback(msg.data.result); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/notificationclose.js b/dom/workers/test/serviceworkers/notificationclose.js new file mode 100644 index 000000000..d48218075 --- /dev/null +++ b/dom/workers/test/serviceworkers/notificationclose.js @@ -0,0 +1,19 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +// +onnotificationclose = function(e) { + self.clients.matchAll().then(function(clients) { + if (clients.length === 0) { + dump("********************* CLIENTS LIST EMPTY! Test will timeout! ***********************\n"); + return; + } + + clients.forEach(function(client) { + client.postMessage({ result: e.notification.data && + e.notification.data['complex'] && + e.notification.data['complex'][0] == "jsval" && + e.notification.data['complex'][1] == 5 }); + + }); + }); +} diff --git a/dom/workers/test/serviceworkers/notify_loaded.js b/dom/workers/test/serviceworkers/notify_loaded.js new file mode 100644 index 000000000..d07573b2c --- /dev/null +++ b/dom/workers/test/serviceworkers/notify_loaded.js @@ -0,0 +1 @@ +parent.postMessage('SCRIPT_LOADED', '*'); diff --git a/dom/workers/test/serviceworkers/opaque_intercept_worker.js b/dom/workers/test/serviceworkers/opaque_intercept_worker.js new file mode 100644 index 000000000..d593be783 --- /dev/null +++ b/dom/workers/test/serviceworkers/opaque_intercept_worker.js @@ -0,0 +1,25 @@ +var name = 'opaqueInterceptCache'; + +// Cross origin request to ensure that an opaque response is used +var prefix = 'http://example.com/tests/dom/workers/test/serviceworkers/' + +self.addEventListener('install', function(event) { + var request = new Request(prefix + 'notify_loaded.js', { mode: 'no-cors' }); + event.waitUntil( + Promise.all([caches.open(name), fetch(request)]).then(function(results) { + var cache = results[0]; + var response = results[1]; + return cache.put('./sw_clients/does_not_exist.js', response); + }) + ); +}); + +self.addEventListener('fetch', function (event) { + event.respondWith( + caches.open(name).then(function(cache) { + return cache.match(event.request); + }).then(function(response) { + return response || fetch(event.request); + }) + ); +}); diff --git a/dom/workers/test/serviceworkers/openWindow_worker.js b/dom/workers/test/serviceworkers/openWindow_worker.js new file mode 100644 index 000000000..46c0f998e --- /dev/null +++ b/dom/workers/test/serviceworkers/openWindow_worker.js @@ -0,0 +1,116 @@ +// the worker won't shut down between events because we increased +// the timeout values. +var client; +var window_count = 0; +var expected_window_count = 7; +var resolve_got_all_windows = null; +var got_all_windows = new Promise(function(res, rej) { + resolve_got_all_windows = res; +}); + +// |expected_window_count| needs to be updated for every new call that's +// expected to actually open a new window regardless of what |clients.openWindow| +// returns. +function testForUrl(url, throwType, clientProperties, resultsArray) { + return clients.openWindow(url) + .then(function(e) { + if (throwType != null) { + resultsArray.push({ + result: false, + message: "openWindow should throw " + throwType + }); + } else if (clientProperties) { + resultsArray.push({ + result: (e instanceof WindowClient), + message: "openWindow should resolve to a WindowClient" + }); + resultsArray.push({ + result: e.url == clientProperties.url, + message: "Client url should be " + clientProperties.url + }); + // Add more properties + } else { + resultsArray.push({ + result: e == null, + message: "Open window should resolve to null. Got: " + e + }); + } + }) + .catch(function(err) { + if (throwType == null) { + resultsArray.push({ + result: false, + message: "Unexpected throw: " + err + }); + } else { + resultsArray.push({ + result: err.toString().indexOf(throwType) >= 0, + message: "openWindow should throw: " + err + }); + } + }) +} + +onmessage = function(event) { + if (event.data == "testNoPopup") { + client = event.source; + + var results = []; + var promises = []; + promises.push(testForUrl("about:blank", "TypeError", null, results)); + promises.push(testForUrl("http://example.com", "InvalidAccessError", null, results)); + promises.push(testForUrl("_._*`InvalidURL", "InvalidAccessError", null, results)); + Promise.all(promises).then(function(e) { + client.postMessage(results); + }); + } + if (event.data == "NEW_WINDOW") { + window_count += 1; + if (window_count == expected_window_count) { + resolve_got_all_windows(); + } + } + + if (event.data == "CHECK_NUMBER_OF_WINDOWS") { + got_all_windows.then(function() { + return clients.matchAll(); + }).then(function(cl) { + event.source.postMessage({result: cl.length == expected_window_count, + message: "The number of windows is correct."}); + for (i = 0; i < cl.length; i++) { + cl[i].postMessage("CLOSE"); + } + }); + } +} + +onnotificationclick = function(e) { + var results = []; + var promises = []; + + var redirect = "http://mochi.test:8888/tests/dom/workers/test/serviceworkers/redirect.sjs?" + var redirect_xorigin = "http://example.com/tests/dom/workers/test/serviceworkers/redirect.sjs?" + var same_origin = "http://mochi.test:8888/tests/dom/workers/test/serviceworkers/open_window/client.html" + var different_origin = "http://example.com/tests/dom/workers/test/serviceworkers/open_window/client.html" + + + promises.push(testForUrl("about:blank", "TypeError", null, results)); + promises.push(testForUrl(different_origin, null, null, results)); + promises.push(testForUrl(same_origin, null, {url: same_origin}, results)); + promises.push(testForUrl("open_window/client.html", null, {url: same_origin}, results)); + + // redirect tests + promises.push(testForUrl(redirect + "open_window/client.html", null, + {url: same_origin}, results)); + promises.push(testForUrl(redirect + different_origin, null, null, results)); + + promises.push(testForUrl(redirect_xorigin + "open_window/client.html", null, + null, results)); + promises.push(testForUrl(redirect_xorigin + same_origin, null, + {url: same_origin}, results)); + + Promise.all(promises).then(function(e) { + client.postMessage(results); + }); +} + diff --git a/dom/workers/test/serviceworkers/open_window/client.html b/dom/workers/test/serviceworkers/open_window/client.html new file mode 100644 index 000000000..82b93ff7e --- /dev/null +++ b/dom/workers/test/serviceworkers/open_window/client.html @@ -0,0 +1,48 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1172870 - page opened by ServiceWorkerClients.OpenWindow</title> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + window.onload = function() { + if (window.location == "http://mochi.test:8888/tests/dom/workers/test/serviceworkers/open_window/client.html") { + navigator.serviceWorker.ready.then(function(result) { + navigator.serviceWorker.onmessage = function(event) { + if (event.data !== "CLOSE") { + dump("ERROR: unexepected reply from the service worker.\n"); + } + if (parent) { + parent.postMessage("CLOSE", "*"); + } + window.close(); + } + navigator.serviceWorker.controller.postMessage("NEW_WINDOW"); + }) + } else { + window.onmessage = function(event) { + if (event.data !== "CLOSE") { + dump("ERROR: unexepected reply from the iframe.\n"); + } + window.close(); + } + + var iframe = document.createElement('iframe'); + iframe.src = "http://mochi.test:8888/tests/dom/workers/test/serviceworkers/open_window/client.html"; + document.body.appendChild(iframe); + } + } + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/parse_error_worker.js b/dom/workers/test/serviceworkers/parse_error_worker.js new file mode 100644 index 000000000..b6a8ef0a1 --- /dev/null +++ b/dom/workers/test/serviceworkers/parse_error_worker.js @@ -0,0 +1,2 @@ +// intentional parse error. +var foo = {; diff --git a/dom/workers/test/serviceworkers/redirect.sjs b/dom/workers/test/serviceworkers/redirect.sjs new file mode 100644 index 000000000..b6249cadf --- /dev/null +++ b/dom/workers/test/serviceworkers/redirect.sjs @@ -0,0 +1,5 @@ +function handleRequest(request, response) +{ + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", request.queryString, false); +} diff --git a/dom/workers/test/serviceworkers/redirect_post.sjs b/dom/workers/test/serviceworkers/redirect_post.sjs new file mode 100644 index 000000000..8b805be63 --- /dev/null +++ b/dom/workers/test/serviceworkers/redirect_post.sjs @@ -0,0 +1,35 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream"); + +function handleRequest(request, response) +{ + var query = {}; + request.queryString.split('&').forEach(function (val) { + var [name, value] = val.split('='); + query[name] = unescape(value); + }); + + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var bodyBytes = []; + while ((bodyAvail = bodyStream.available()) > 0) + Array.prototype.push.apply(bodyBytes, bodyStream.readByteArray(bodyAvail)); + + var body = decodeURIComponent( + escape(String.fromCharCode.apply(null, bodyBytes))); + + var currentHop = query.hop ? parseInt(query.hop) : 0; + + var obj = JSON.parse(body); + if (currentHop < obj.hops) { + var newURL = '/tests/dom/workers/test/serviceworkers/redirect_post.sjs?hop=' + + (1 + currentHop); + response.setStatusLine(null, 307, 'redirect'); + response.setHeader('Location', newURL); + return; + } + + response.setHeader('Content-Type', 'application/json'); + response.write(body); +} diff --git a/dom/workers/test/serviceworkers/redirect_serviceworker.sjs b/dom/workers/test/serviceworkers/redirect_serviceworker.sjs new file mode 100644 index 000000000..9d3a0a2cd --- /dev/null +++ b/dom/workers/test/serviceworkers/redirect_serviceworker.sjs @@ -0,0 +1,4 @@ +function handleRequest(request, response) { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader("Location", "http://mochi.test:8888/tests/dom/workers/test/serviceworkers/worker.js"); +} diff --git a/dom/workers/test/serviceworkers/register_https.html b/dom/workers/test/serviceworkers/register_https.html new file mode 100644 index 000000000..d14d8c380 --- /dev/null +++ b/dom/workers/test/serviceworkers/register_https.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<script> +function ok(condition, message) { + parent.postMessage({type: "ok", status: condition, msg: message}, "*"); +} + +function done() { + parent.postMessage({type: "done"}, "*"); +} + +ok(location.protocol == "https:", "We should be loaded from HTTPS"); + +navigator.serviceWorker.register("empty.js", {scope: "register-https"}) + .then(reg => { + ok(false, "Registration should fail"); + done(); + }).catch(err => { + ok(err.name === "SecurityError", "Registration should fail with SecurityError"); + done(); + }); +</script> diff --git a/dom/workers/test/serviceworkers/sanitize/example_check_and_unregister.html b/dom/workers/test/serviceworkers/sanitize/example_check_and_unregister.html new file mode 100644 index 000000000..4de8f317b --- /dev/null +++ b/dom/workers/test/serviceworkers/sanitize/example_check_and_unregister.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<script> + function done(exists) { + parent.postMessage(exists, '*'); + } + + function fail() { + parent.postMessage("FAIL", '*'); + } + + navigator.serviceWorker.getRegistration(".").then(function(reg) { + if (reg) { + reg.unregister().then(done.bind(undefined, true), fail); + } else { + dump("getRegistration() returned undefined registration\n"); + done(false); + } + }, function(e) { + dump("getRegistration() failed\n"); + fail(); + }); +</script> + diff --git a/dom/workers/test/serviceworkers/sanitize/frame.html b/dom/workers/test/serviceworkers/sanitize/frame.html new file mode 100644 index 000000000..b4bf7a1ff --- /dev/null +++ b/dom/workers/test/serviceworkers/sanitize/frame.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script> + fetch("intercept-this").then(function(r) { + if (!r.ok) { + return "FAIL"; + } + return r.text(); + }).then(function(body) { + parent.postMessage(body, '*'); + }); +</script> diff --git a/dom/workers/test/serviceworkers/sanitize/register.html b/dom/workers/test/serviceworkers/sanitize/register.html new file mode 100644 index 000000000..f1edd27be --- /dev/null +++ b/dom/workers/test/serviceworkers/sanitize/register.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<script> + function done() { + parent.postMessage('', '*'); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("../sanitize_worker.js", {scope: "."}); +</script> + diff --git a/dom/workers/test/serviceworkers/sanitize_worker.js b/dom/workers/test/serviceworkers/sanitize_worker.js new file mode 100644 index 000000000..66495e186 --- /dev/null +++ b/dom/workers/test/serviceworkers/sanitize_worker.js @@ -0,0 +1,5 @@ +onfetch = function(e) { + if (e.request.url.indexOf("intercept-this") != -1) { + e.respondWith(new Response("intercepted")); + } +} diff --git a/dom/workers/test/serviceworkers/scope/scope_worker.js b/dom/workers/test/serviceworkers/scope/scope_worker.js new file mode 100644 index 000000000..4164e7a24 --- /dev/null +++ b/dom/workers/test/serviceworkers/scope/scope_worker.js @@ -0,0 +1,2 @@ +// This worker is used to test if calling register() without a scope argument +// leads to scope being relative to service worker script. diff --git a/dom/workers/test/serviceworkers/serviceworker.html b/dom/workers/test/serviceworkers/serviceworker.html new file mode 100644 index 000000000..11edd001a --- /dev/null +++ b/dom/workers/test/serviceworkers/serviceworker.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + navigator.serviceWorker.register("worker.js"); + </script> + </head> + <body> + This is a test page. + </body> +<html> diff --git a/dom/workers/test/serviceworkers/serviceworker_not_sharedworker.js b/dom/workers/test/serviceworkers/serviceworker_not_sharedworker.js new file mode 100644 index 000000000..077da2366 --- /dev/null +++ b/dom/workers/test/serviceworkers/serviceworker_not_sharedworker.js @@ -0,0 +1,21 @@ +function OnMessage(e) +{ + if (e.data.msg == "whoareyou") { + if ("ServiceWorker" in self) { + self.clients.matchAll().then(function(clients) { + clients[0].postMessage({result: "serviceworker"}); + }); + } else { + port.postMessage({result: "sharedworker"}); + } + } +}; + +var port; +onconnect = function(e) { + port = e.ports[0]; + port.onmessage = OnMessage; + port.start(); +}; + +onmessage = OnMessage; diff --git a/dom/workers/test/serviceworkers/serviceworker_wrapper.js b/dom/workers/test/serviceworkers/serviceworker_wrapper.js new file mode 100644 index 000000000..6a7ec0c0f --- /dev/null +++ b/dom/workers/test/serviceworkers/serviceworker_wrapper.js @@ -0,0 +1,101 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +// +// ServiceWorker equivalent of worker_wrapper.js. + +var client; + +function ok(a, msg) { + dump("OK: " + !!a + " => " + a + ": " + msg + "\n"); + client.postMessage({type: 'status', status: !!a, msg: a + ": " + msg }); +} + +function is(a, b, msg) { + dump("IS: " + (a===b) + " => " + a + " | " + b + ": " + msg + "\n"); + client.postMessage({type: 'status', status: a === b, msg: a + " === " + b + ": " + msg }); +} + +function workerTestArrayEquals(a, b) { + if (!Array.isArray(a) || !Array.isArray(b) || a.length != b.length) { + return false; + } + for (var i = 0, n = a.length; i < n; ++i) { + if (a[i] !== b[i]) { + return false; + } + } + return true; +} + +function workerTestDone() { + client.postMessage({ type: 'finish' }); +} + +function workerTestGetVersion(cb) { + addEventListener('message', function workerTestGetVersionCB(e) { + if (e.data.type !== 'returnVersion') { + return; + } + removeEventListener('message', workerTestGetVersionCB); + cb(e.data.result); + }); + client.postMessage({ + type: 'getVersion' + }); +} + +function workerTestGetUserAgent(cb) { + addEventListener('message', function workerTestGetUserAgentCB(e) { + if (e.data.type !== 'returnUserAgent') { + return; + } + removeEventListener('message', workerTestGetUserAgentCB); + cb(e.data.result); + }); + client.postMessage({ + type: 'getUserAgent' + }); +} + +function workerTestGetOSCPU(cb) { + addEventListener('message', function workerTestGetOSCPUCB(e) { + if (e.data.type !== 'returnOSCPU') { + return; + } + removeEventListener('message', workerTestGetOSCPUCB); + cb(e.data.result); + }); + client.postMessage({ + type: 'getOSCPU' + }); +} + +function workerTestGetStorageManager(cb) { + addEventListener('message', function workerTestGetStorageManagerCB(e) { + if (e.data.type !== 'returnStorageManager') { + return; + } + removeEventListener('message', workerTestGetStorageManagerCB); + cb(e.data.result); + }); + client.postMessage({ + type: 'getStorageManager' + }); +} + +addEventListener('message', function workerWrapperOnMessage(e) { + removeEventListener('message', workerWrapperOnMessage); + var data = e.data; + self.clients.matchAll().then(function(clients) { + client = clients[0]; + try { + importScripts(data.script); + } catch(e) { + client.postMessage({ + type: 'status', + status: false, + msg: 'worker failed to import ' + data.script + "; error: " + e.message + }); + } + }); +}); diff --git a/dom/workers/test/serviceworkers/serviceworkerinfo_iframe.html b/dom/workers/test/serviceworkers/serviceworkerinfo_iframe.html new file mode 100644 index 000000000..a0a2de760 --- /dev/null +++ b/dom/workers/test/serviceworkers/serviceworkerinfo_iframe.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + window.onmessage = function (event) { + if (event.data !== "register") { + return; + } + var promise = navigator.serviceWorker.register("worker.js"); + window.onmessage = function (event) { + if (event.data !== "unregister") { + return; + } + promise.then(function (registration) { + registration.unregister(); + }); + window.onmessage = null; + }; + }; + </script> + </head> + <body> + This is a test page. + </body> +<html> diff --git a/dom/workers/test/serviceworkers/serviceworkermanager_iframe.html b/dom/workers/test/serviceworkers/serviceworkermanager_iframe.html new file mode 100644 index 000000000..0df93da96 --- /dev/null +++ b/dom/workers/test/serviceworkers/serviceworkermanager_iframe.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + window.onmessage = function (event) { + if (event.data !== "register") { + return; + } + var promise = navigator.serviceWorker.register("worker.js"); + window.onmessage = function (event) { + if (event.data !== "register") { + return; + } + promise = promise.then(function (registration) { + return navigator.serviceWorker.register("worker2.js"); + }); + window.onmessage = function (event) { + if (event.data !== "unregister") { + return; + } + promise.then(function (registration) { + registration.unregister(); + }); + window.onmessage = null; + }; + }; + }; + </script> + </head> + <body> + This is a test page. + </body> +<html> diff --git a/dom/workers/test/serviceworkers/serviceworkerregistrationinfo_iframe.html b/dom/workers/test/serviceworkers/serviceworkerregistrationinfo_iframe.html new file mode 100644 index 000000000..f093d38db --- /dev/null +++ b/dom/workers/test/serviceworkers/serviceworkerregistrationinfo_iframe.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + var reg; + window.onmessage = function (event) { + if (event.data !== "register") { + return; + } + var promise = navigator.serviceWorker.register("worker.js"); + window.onmessage = function (event) { + if (event.data === "register") { + promise.then(function (registration) { + return navigator.serviceWorker.register("worker2.js") + .then(function(registration) { + reg = registration; + }); + }); + } else if (event.data === "unregister") { + reg.unregister(); + } + }; + }; + </script> + </head> + <body> + This is a test page. + </body> +<html> diff --git a/dom/workers/test/serviceworkers/sharedWorker_fetch.js b/dom/workers/test/serviceworkers/sharedWorker_fetch.js new file mode 100644 index 000000000..4970a1fd2 --- /dev/null +++ b/dom/workers/test/serviceworkers/sharedWorker_fetch.js @@ -0,0 +1,29 @@ +var clients = new Array(); +clients.length = 0; + +var broadcast = function(message) { + var length = clients.length; + for (var i = 0; i < length; i++) { + port = clients[i]; + port.postMessage(message); + } +} + +onconnect = function(e) { + clients.push(e.ports[0]); + if (clients.length == 1) { + clients[0].postMessage("Connected"); + } else if (clients.length == 2) { + broadcast("BothConnected"); + clients[0].onmessage = function(e) { + if (e.data == "StartFetchWithWrongIntegrity") { + // The fetch will succeed because the integrity value is invalid and we + // are looking for the console message regarding the bad integrity value. + fetch("SharedWorker_SRIFailed.html", {"integrity": "abc"}).then( + function () { + clients[0].postMessage('SRI_failed'); + }); + } + } + } +} diff --git a/dom/workers/test/serviceworkers/simpleregister/index.html b/dom/workers/test/serviceworkers/simpleregister/index.html new file mode 100644 index 000000000..2c0eb5345 --- /dev/null +++ b/dom/workers/test/serviceworkers/simpleregister/index.html @@ -0,0 +1,51 @@ +<html> + <head></head> + <body> + <script type="text/javascript"> + var expectedEvents = 2; + function eventReceived() { + window.parent.postMessage({ type: "check", status: expectedEvents > 0, msg: "updatefound received" }, "*"); + + if (--expectedEvents) { + window.parent.postMessage({ type: "finish" }, "*"); + } + } + + navigator.serviceWorker.getRegistrations().then(function(a) { + window.parent.postMessage({ type: "check", status: Array.isArray(a), + msg: "getRegistrations returns an array" }, "*"); + window.parent.postMessage({ type: "check", status: a.length > 0, + msg: "getRegistrations returns an array with 1 item" }, "*"); + for (var i = 0; i < a.length; ++i) { + window.parent.postMessage({ type: "check", status: a[i] instanceof ServiceWorkerRegistration, + msg: "getRegistrations returns an array of ServiceWorkerRegistration objects" }, "*"); + if (a[i].scope.match(/simpleregister\//)) { + a[i].onupdatefound = function(e) { + eventReceived(); + } + } + } + }); + + navigator.serviceWorker.getRegistration('http://mochi.test:8888/tests/dom/workers/test/serviceworkers/simpleregister/') + .then(function(a) { + window.parent.postMessage({ type: "check", status: a instanceof ServiceWorkerRegistration, + msg: "getRegistration returns a ServiceWorkerRegistration" }, "*"); + a.onupdatefound = function(e) { + eventReceived(); + } + }); + + navigator.serviceWorker.getRegistration('http://www.something_else.net/') + .then(function(a) { + window.parent.postMessage({ type: "check", status: false, + msg: "getRegistration should throw for security error!" }, "*"); + }, function(a) { + window.parent.postMessage({ type: "check", status: true, + msg: "getRegistration should throw for security error!" }, "*"); + }); + + window.parent.postMessage({ type: "ready" }, "*"); + </script> + </body> +</html> diff --git a/dom/workers/test/serviceworkers/simpleregister/ready.html b/dom/workers/test/serviceworkers/simpleregister/ready.html new file mode 100644 index 000000000..3afc4bfdb --- /dev/null +++ b/dom/workers/test/serviceworkers/simpleregister/ready.html @@ -0,0 +1,15 @@ +<html> + <head></head> + <body> + <script type="text/javascript"> + + window.addEventListener('message', function(evt) { + navigator.serviceWorker.ready.then(function() { + evt.ports[0].postMessage("WOW!"); + }); + }, false); + + </script> + </body> +</html> + diff --git a/dom/workers/test/serviceworkers/skip_waiting_installed_worker.js b/dom/workers/test/serviceworkers/skip_waiting_installed_worker.js new file mode 100644 index 000000000..68573f100 --- /dev/null +++ b/dom/workers/test/serviceworkers/skip_waiting_installed_worker.js @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +self.addEventListener('install', evt => { + evt.waitUntil(self.skipWaiting()); +}); diff --git a/dom/workers/test/serviceworkers/skip_waiting_scope/index.html b/dom/workers/test/serviceworkers/skip_waiting_scope/index.html new file mode 100644 index 000000000..b8a64d512 --- /dev/null +++ b/dom/workers/test/serviceworkers/skip_waiting_scope/index.html @@ -0,0 +1,37 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1131352 - Add ServiceWorkerGlobalScope skipWaiting()</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + info("skip_waiting_scope/index.html shouldn't be launched directly!"); + } + + navigator.serviceWorker.ready.then(function() { + parent.postMessage("READY", "*"); + }); + + navigator.serviceWorker.oncontrollerchange = function() { + parent.postMessage({ + event: "controllerchange", + controllerScriptURL: navigator.serviceWorker.controller && + navigator.serviceWorker.controller.scriptURL + }, "*"); + } + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/source_message_posting_worker.js b/dom/workers/test/serviceworkers/source_message_posting_worker.js new file mode 100644 index 000000000..36ce951fe --- /dev/null +++ b/dom/workers/test/serviceworkers/source_message_posting_worker.js @@ -0,0 +1,16 @@ +onmessage = function(e) { + if (!e.source) { + dump("ERROR: message doesn't have a source."); + } + + if (!(e instanceof ExtendableMessageEvent)) { + e.source.postMessage("ERROR. event is not an extendable message event."); + } + + // The client should be a window client + if (e.source instanceof WindowClient) { + e.source.postMessage(e.data); + } else { + e.source.postMessage("ERROR. source is not a window client."); + } +}; diff --git a/dom/workers/test/serviceworkers/strict_mode_warning.js b/dom/workers/test/serviceworkers/strict_mode_warning.js new file mode 100644 index 000000000..38418de3d --- /dev/null +++ b/dom/workers/test/serviceworkers/strict_mode_warning.js @@ -0,0 +1,4 @@ +function f() { + return 1; + return 2; +} diff --git a/dom/workers/test/serviceworkers/sw_bad_mime_type.js b/dom/workers/test/serviceworkers/sw_bad_mime_type.js new file mode 100644 index 000000000..f371807db --- /dev/null +++ b/dom/workers/test/serviceworkers/sw_bad_mime_type.js @@ -0,0 +1 @@ +// I need some contents. diff --git a/dom/workers/test/serviceworkers/sw_bad_mime_type.js^headers^ b/dom/workers/test/serviceworkers/sw_bad_mime_type.js^headers^ new file mode 100644 index 000000000..a1f9e38d9 --- /dev/null +++ b/dom/workers/test/serviceworkers/sw_bad_mime_type.js^headers^ @@ -0,0 +1 @@ +Content-Type: text/plain diff --git a/dom/workers/test/serviceworkers/sw_clients/file_blob_upload_frame.html b/dom/workers/test/serviceworkers/sw_clients/file_blob_upload_frame.html new file mode 100644 index 000000000..e594c514d --- /dev/null +++ b/dom/workers/test/serviceworkers/sw_clients/file_blob_upload_frame.html @@ -0,0 +1,77 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>test file blob upload with SW interception</title> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + +if (!parent) { + dump("sw_clients/file_blob_upload_frame.html shouldn't be launched directly!"); +} + +function makeFileBlob(obj) { + return new Promise(function(resolve, reject) { + + var request = indexedDB.open(window.location.pathname, 1); + request.onerror = reject; + request.onupgradeneeded = function(evt) { + var db = evt.target.result; + db.onerror = reject; + + var objectStore = db.createObjectStore('test', { autoIncrement: true }); + var index = objectStore.createIndex('test', 'index'); + }; + + request.onsuccess = function(evt) { + var db = evt.target.result; + db.onerror = reject; + + var blob = new Blob([JSON.stringify(obj)], + { type: 'application/json' }); + var data = { blob: blob, index: 5 }; + + objectStore = db.transaction('test', 'readwrite').objectStore('test'); + objectStore.add(data).onsuccess = function(evt) { + var key = evt.target.result; + objectStore = db.transaction('test').objectStore('test'); + objectStore.get(key).onsuccess = function(evt) { + resolve(evt.target.result.blob); + }; + }; + }; + }); +} + +navigator.serviceWorker.ready.then(function() { + parent.postMessage({ status: 'READY' }, '*'); +}); + +var URL = '/tests/dom/workers/test/serviceworkers/redirect_post.sjs'; + +addEventListener('message', function(evt) { + if (evt.data.type = 'TEST') { + makeFileBlob(evt.data.body).then(function(blob) { + return fetch(URL, { method: 'POST', body: blob }); + }).then(function(response) { + return response.json(); + }).then(function(result) { + parent.postMessage({ status: 'OK', result: result }, '*'); + }).catch(function(e) { + parent.postMessage({ status: 'ERROR', result: e.toString() }, '*'); + }); + } +}); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/sw_clients/navigator.html b/dom/workers/test/serviceworkers/sw_clients/navigator.html new file mode 100644 index 000000000..f6019bf28 --- /dev/null +++ b/dom/workers/test/serviceworkers/sw_clients/navigator.html @@ -0,0 +1,35 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - test match_all not crashing</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + dump("sw_clients/navigator.html shouldn't be launched directly!\n"); + } + + window.addEventListener("message", function(event) { + if (event.data.type === "NAVIGATE") { + window.location = event.data.url; + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage("NAVIGATOR_READY", "*"); + }); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/sw_clients/refresher.html b/dom/workers/test/serviceworkers/sw_clients/refresher.html new file mode 100644 index 000000000..054f6bfc8 --- /dev/null +++ b/dom/workers/test/serviceworkers/sw_clients/refresher.html @@ -0,0 +1,39 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - test match_all not crashing</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <!-- some tests will intercept this bogus script request --> + <script type="text/javascript" src="does_not_exist.js"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + dump("sw_clients/simple.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function(event) { + if (event.data === "REFRESH") { + window.location.reload(); + } else if (event.data === "FORCE_REFRESH") { + window.location.reload(true); + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage("READY", "*"); + }); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/sw_clients/refresher_cached.html b/dom/workers/test/serviceworkers/sw_clients/refresher_cached.html new file mode 100644 index 000000000..3ec0cc427 --- /dev/null +++ b/dom/workers/test/serviceworkers/sw_clients/refresher_cached.html @@ -0,0 +1,38 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - test match_all not crashing</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + info("sw_clients/simple.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function(event) { + if (event.data === "REFRESH") { + window.location.reload(); + } else if (event.data === "FORCE_REFRESH") { + window.location.reload(true); + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage("READY_CACHED", "*"); + }); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/sw_clients/refresher_cached_compressed.html b/dom/workers/test/serviceworkers/sw_clients/refresher_cached_compressed.html Binary files differnew file mode 100644 index 000000000..55e97ac24 --- /dev/null +++ b/dom/workers/test/serviceworkers/sw_clients/refresher_cached_compressed.html diff --git a/dom/workers/test/serviceworkers/sw_clients/refresher_cached_compressed.html^headers^ b/dom/workers/test/serviceworkers/sw_clients/refresher_cached_compressed.html^headers^ new file mode 100644 index 000000000..4204d8601 --- /dev/null +++ b/dom/workers/test/serviceworkers/sw_clients/refresher_cached_compressed.html^headers^ @@ -0,0 +1,2 @@ +Content-Type: text/html +Content-Encoding: gzip diff --git a/dom/workers/test/serviceworkers/sw_clients/refresher_compressed.html b/dom/workers/test/serviceworkers/sw_clients/refresher_compressed.html Binary files differnew file mode 100644 index 000000000..7a45bcafa --- /dev/null +++ b/dom/workers/test/serviceworkers/sw_clients/refresher_compressed.html diff --git a/dom/workers/test/serviceworkers/sw_clients/refresher_compressed.html^headers^ b/dom/workers/test/serviceworkers/sw_clients/refresher_compressed.html^headers^ new file mode 100644 index 000000000..4204d8601 --- /dev/null +++ b/dom/workers/test/serviceworkers/sw_clients/refresher_compressed.html^headers^ @@ -0,0 +1,2 @@ +Content-Type: text/html +Content-Encoding: gzip diff --git a/dom/workers/test/serviceworkers/sw_clients/service_worker_controlled.html b/dom/workers/test/serviceworkers/sw_clients/service_worker_controlled.html new file mode 100644 index 000000000..e0d7bce57 --- /dev/null +++ b/dom/workers/test/serviceworkers/sw_clients/service_worker_controlled.html @@ -0,0 +1,38 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>controlled page</title> + <!-- + Paged controlled by a service worker for testing matchAll(). + See bug 982726, 1058311. + --> +<script class="testbody" type="text/javascript"> + function fail(msg) { + info("service_worker_controlled.html: " + msg); + opener.postMessage("FAIL", "*"); + } + + if (!parent) { + info("service_worker_controlled.html should not be launched directly!"); + } + + window.onload = function() { + navigator.serviceWorker.ready.then(function(swr) { + parent.postMessage("READY", "*"); + }); + } + + navigator.serviceWorker.onmessage = function(msg) { + // forward message to the test page. + parent.postMessage(msg.data, "*"); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/sw_clients/simple.html b/dom/workers/test/serviceworkers/sw_clients/simple.html new file mode 100644 index 000000000..3e4d7deca --- /dev/null +++ b/dom/workers/test/serviceworkers/sw_clients/simple.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - test match_all not crashing</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + info("sw_clients/simple.html shouldn't be launched directly!"); + } + + navigator.serviceWorker.ready.then(function() { + parent.postMessage("READY", "*"); + }); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/swa/worker_scope_different.js b/dom/workers/test/serviceworkers/swa/worker_scope_different.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/dom/workers/test/serviceworkers/swa/worker_scope_different.js diff --git a/dom/workers/test/serviceworkers/swa/worker_scope_different.js^headers^ b/dom/workers/test/serviceworkers/swa/worker_scope_different.js^headers^ new file mode 100644 index 000000000..e85a7f09d --- /dev/null +++ b/dom/workers/test/serviceworkers/swa/worker_scope_different.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: different/path diff --git a/dom/workers/test/serviceworkers/swa/worker_scope_different2.js b/dom/workers/test/serviceworkers/swa/worker_scope_different2.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/dom/workers/test/serviceworkers/swa/worker_scope_different2.js diff --git a/dom/workers/test/serviceworkers/swa/worker_scope_different2.js^headers^ b/dom/workers/test/serviceworkers/swa/worker_scope_different2.js^headers^ new file mode 100644 index 000000000..e37307d66 --- /dev/null +++ b/dom/workers/test/serviceworkers/swa/worker_scope_different2.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: /different/path diff --git a/dom/workers/test/serviceworkers/swa/worker_scope_precise.js b/dom/workers/test/serviceworkers/swa/worker_scope_precise.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/dom/workers/test/serviceworkers/swa/worker_scope_precise.js diff --git a/dom/workers/test/serviceworkers/swa/worker_scope_precise.js^headers^ b/dom/workers/test/serviceworkers/swa/worker_scope_precise.js^headers^ new file mode 100644 index 000000000..30b053055 --- /dev/null +++ b/dom/workers/test/serviceworkers/swa/worker_scope_precise.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: /tests/dom/workers/test/serviceworkers/swa diff --git a/dom/workers/test/serviceworkers/swa/worker_scope_too_deep.js b/dom/workers/test/serviceworkers/swa/worker_scope_too_deep.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/dom/workers/test/serviceworkers/swa/worker_scope_too_deep.js diff --git a/dom/workers/test/serviceworkers/swa/worker_scope_too_deep.js^headers^ b/dom/workers/test/serviceworkers/swa/worker_scope_too_deep.js^headers^ new file mode 100644 index 000000000..b2056fc4a --- /dev/null +++ b/dom/workers/test/serviceworkers/swa/worker_scope_too_deep.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: /tests/dom/workers/test/serviceworkers/swa/deep/way/too/specific diff --git a/dom/workers/test/serviceworkers/swa/worker_scope_too_narrow.js b/dom/workers/test/serviceworkers/swa/worker_scope_too_narrow.js new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/dom/workers/test/serviceworkers/swa/worker_scope_too_narrow.js diff --git a/dom/workers/test/serviceworkers/swa/worker_scope_too_narrow.js^headers^ b/dom/workers/test/serviceworkers/swa/worker_scope_too_narrow.js^headers^ new file mode 100644 index 000000000..22add13bf --- /dev/null +++ b/dom/workers/test/serviceworkers/swa/worker_scope_too_narrow.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: /tests/dom/workers diff --git a/dom/workers/test/serviceworkers/test_bug1151916.html b/dom/workers/test/serviceworkers/test_bug1151916.html new file mode 100644 index 000000000..92811775b --- /dev/null +++ b/dom/workers/test/serviceworkers/test_bug1151916.html @@ -0,0 +1,105 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1151916 - Test principal is set on cached serviceworkers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +<!-- + If the principal is not set, accessing self.caches in the worker will crash. +--> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var frame; + + function listenForMessage() { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data.status == "failed") { + ok(false, "iframe had error " + e.data.message); + reject(e.data.message); + } else if (e.data.status == "success") { + ok(true, "iframe step success " + e.data.message); + resolve(e.data.message); + } else { + ok(false, "Unexpected message " + e.data); + reject(); + } + } + }); + + return p; + } + + // We have the iframe register for its own scope so that this page is not + // holding any references when we GC. + function register() { + var p = listenForMessage(); + + frame = document.createElement("iframe"); + document.body.appendChild(frame); + frame.src = "bug1151916_driver.html"; + + return p; + } + + function unloadFrame() { + frame.src = "about:blank"; + frame.parentNode.removeChild(frame); + frame = null; + } + + function gc() { + return new Promise(function(resolve) { + SpecialPowers.exactGC(resolve); + }); + } + + function testCaches() { + var p = listenForMessage(); + + frame = document.createElement("iframe"); + document.body.appendChild(frame); + frame.src = "bug1151916_driver.html"; + + return p; + } + + function unregister() { + return navigator.serviceWorker.getRegistration("./bug1151916_driver.html").then(function(reg) { + ok(reg instanceof ServiceWorkerRegistration, "Must have valid registration."); + return reg.unregister(); + }); + } + + function runTest() { + register() + .then(unloadFrame) + .then(gc) + .then(testCaches) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_bug1240436.html b/dom/workers/test/serviceworkers/test_bug1240436.html new file mode 100644 index 000000000..e93535840 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_bug1240436.html @@ -0,0 +1,34 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for encoding of service workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + + function runTest() { + navigator.serviceWorker.register("bug1240436_worker.js") + .then(reg => reg.unregister()) + .then(() => ok(true, "service worker register script succeed")) + .catch(err => ok(false, "service worker register script faled " + err)) + .then(() => SimpleTest.finish()); + } + + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_claim.html b/dom/workers/test/serviceworkers/test_claim.html new file mode 100644 index 000000000..d7015850f --- /dev/null +++ b/dom/workers/test/serviceworkers/test_claim.html @@ -0,0 +1,172 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1130684 - Test service worker clients claim onactivate </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration_1; + var registration_2; + var client; + + function register_1() { + return navigator.serviceWorker.register("claim_worker_1.js", + { scope: "./" }) + .then((swr) => registration_1 = swr); + } + + function register_2() { + return navigator.serviceWorker.register("claim_worker_2.js", + { scope: "./claim_clients/client.html" }) + .then((swr) => registration_2 = swr); + } + + function unregister(reg) { + return reg.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }); + } + + function createClient() { + var p = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (e.data === "READY") { + res(); + } + } + }); + + content = document.getElementById("content"); + ok(content, "parent exists."); + + client = document.createElement("iframe"); + client.setAttribute('src', "claim_clients/client.html"); + content.appendChild(client); + + return p; + } + + function testController() { + ok(navigator.serviceWorker.controller.scriptURL.match("claim_worker_1"), + "Controlling service worker has the correct url."); + } + + function testClientWasClaimed(expected) { + var resolveClientMessage, resolveClientControllerChange; + var messageFromClient = new Promise(function(res, rej) { + resolveClientMessage = res; + }); + var controllerChangeFromClient = new Promise(function(res, rej) { + resolveClientControllerChange = res; + }); + window.onmessage = function(e) { + if (!e.data.event) { + ok(false, "Unknown message received: " + e.data); + } + + if (e.data.event === "controllerchange") { + ok(e.data.controller, + "Client was claimed and received controllerchange event."); + resolveClientControllerChange(); + } + + if (e.data.event === "message") { + ok(e.data.data.resolve_value === undefined, + "Claim should resolve with undefined."); + ok(e.data.data.message === expected.message, + "Client received message from claiming worker."); + ok(e.data.data.match_count_before === expected.match_count_before, + "MatchAll clients count before claim should be " + expected.match_count_before); + ok(e.data.data.match_count_after === expected.match_count_after, + "MatchAll clients count after claim should be " + expected.match_count_after); + resolveClientMessage(); + } + } + + return Promise.all([messageFromClient, controllerChangeFromClient]) + .then(() => window.onmessage = null); + } + + function testClaimFirstWorker() { + // wait for the worker to control us + var controllerChange = new Promise(function(res, rej) { + navigator.serviceWorker.oncontrollerchange = function(e) { + ok(true, "controller changed event received."); + res(); + }; + }); + + var messageFromWorker = new Promise(function(res, rej) { + navigator.serviceWorker.onmessage = function(e) { + ok(e.data.resolve_value === undefined, + "Claim should resolve with undefined."); + ok(e.data.message === "claim_worker_1", + "Received message from claiming worker."); + ok(e.data.match_count_before === 0, + "Worker doesn't control any client before claim."); + ok(e.data.match_count_after === 2, "Worker should claim 2 clients."); + res(); + } + }); + + var clientClaim = testClientWasClaimed({ + message: "claim_worker_1", + match_count_before: 0, + match_count_after: 2 + }); + + return Promise.all([controllerChange, messageFromWorker, clientClaim]) + .then(testController); + } + + function testClaimSecondWorker() { + navigator.serviceWorker.oncontrollerchange = function(e) { + ok(false, "Claim_worker_2 shouldn't claim this window."); + } + + navigator.serviceWorker.onmessage = function(e) { + ok(false, "Claim_worker_2 shouldn't claim this window."); + } + + var clientClaim = testClientWasClaimed({ + message: "claim_worker_2", + match_count_before: 0, + match_count_after: 1 + }); + + return clientClaim.then(testController); + } + + function runTest() { + createClient() + .then(register_1) + .then(testClaimFirstWorker) + .then(register_2) + .then(testClaimSecondWorker) + .then(function() { return unregister(registration_1); }) + .then(function() { return unregister(registration_2); }) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_claim_fetch.html b/dom/workers/test/serviceworkers/test_claim_fetch.html new file mode 100644 index 000000000..8db6d304e --- /dev/null +++ b/dom/workers/test/serviceworkers/test_claim_fetch.html @@ -0,0 +1,98 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1130684 - Test fetch events are intercepted after claim </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + <a ping="ping" href="fetch.txt">link</a> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + + function register() { + return navigator.serviceWorker.register("claim_fetch_worker.js", + { scope: "./" }) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }); + } + + function createClient() { + var p = new Promise(function(res, rej){ + window.onmessage = function(e) { + if(e.data === "READY") { + res(); + } + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/service_worker_controlled.html"); + content.appendChild(iframe); + + return p; + } + + function testFetch(before) { + return fetch("fetch/real-file.txt").then(function(res) { + ok(res.ok, "Response should be valid."); + return res.text().then(function(body) { + if (before) { + ok(body !== "Fetch was intercepted", "Fetch events should not be intercepted."); + } else { + ok(body === "Fetch was intercepted", "Fetch events should be intercepted."); + } + }); + }); + } + + function claimThisPage() { + ok(registration.active, "Worker is active."); + var p = new Promise(function (res, rej) { + navigator.serviceWorker.oncontrollerchange = res; + }); + + registration.active.postMessage("Claim"); + + return p; + } + + function runTest() { + register() + .then(createClient) + .then(testFetch.bind(this, true)) + .then(claimThisPage) + .then(testFetch.bind(this, false)) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_claim_oninstall.html b/dom/workers/test/serviceworkers/test_claim_oninstall.html new file mode 100644 index 000000000..4605cfb76 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_claim_oninstall.html @@ -0,0 +1,78 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1130684 - Test service worker clients.claim oninstall</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + + function register() { + return navigator.serviceWorker.register("claim_oninstall_worker.js", + { scope: "./" }) + .then((swr) => registration = swr); + } + + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }); + } + + function testClaim() { + ok(registration.installing, "Worker should be in installing state"); + + navigator.serviceWorker.oncontrollerchange = function() { + ok(false, "Claim should not succeed when the worker is not active."); + } + + var p = new Promise(function(res, rej) { + var worker = registration.installing; + worker.onstatechange = function(e) { + if (worker.state === 'installed') { + is(worker, registration.waiting, "Worker should be in waiting state"); + } else if (worker.state === 'activated') { + // The worker will become active only if claim will reject inside the + // install handler. + is(worker, registration.active, + "Claim should reject if the worker is not active"); + ok(navigator.serviceWorker.controller === null, "Client is not controlled."); + e.target.onstatechange = null; + res(); + } + } + }); + + return p; + } + + function runTest() { + register() + .then(testClaim) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_client_focus.html b/dom/workers/test/serviceworkers/test_client_focus.html new file mode 100644 index 000000000..b0bf43bb3 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_client_focus.html @@ -0,0 +1,96 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1130686 - Test service worker client.focus </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +<!-- + This test checks that client.focus is available. + Actual focusing is tested by test_notificationclick_focus.html since only notification events have permission to change focus. +--> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + var worker; + + function start() { + return navigator.serviceWorker.register("client_focus_worker.js", + { scope: "./sw_clients/focus_stealing_client.html" }) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function loseFocus() { + var p = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (e.data == "READY") { + ok(true, "iframe created."); + iframe.contentWindow.focus(); + } + } + window.onblur = function() { + ok(true, "blurred"); + res(); + } + }); + + content = document.getElementById("content"); + ok(content, "parent exists."); + + iframe = document.createElement("iframe"); + content.appendChild(iframe); + + iframe.setAttribute('src', "sw_clients/focus_stealing_client.html"); + return p; + } + + function testFocus() { + var p = new Promise(function(res, rej) { + navigator.serviceWorker.onmessage = function(e) { + ok(e.data, "client object is marked as focused."); + ok(document.hasFocus(), "document has focus."); + res(); + } + }); + + ok(registration.active, "active worker exists."); + registration.active.postMessage("go"); + return p; + } + + function runTest() { + start() + .then(loseFocus) + .then(testFocus) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_close.html b/dom/workers/test/serviceworkers/test_close.html new file mode 100644 index 000000000..d2f72b9ef --- /dev/null +++ b/dom/workers/test/serviceworkers/test_close.html @@ -0,0 +1,64 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1131353 - test WorkerGlobalScope.close() on service workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + navigator.serviceWorker.ready.then(setupSW); + navigator.serviceWorker.register("close_test.js", {scope: "."}); + + function setupSW(registration) { + var worker = registration.waiting || + registration.active; + var iframe = document.createElement("iframe"); + iframe.src = "message_receiver.html"; + iframe.onload = function() { + worker.postMessage({ message: "start" }); + }; + document.body.appendChild(iframe); + } + + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "done") { + navigator.serviceWorker.getRegistration().then(function(registration) { + registration.unregister().then(function(result) { + ok(result, "Unregistering the service worker should succeed"); + SimpleTest.finish(); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + SimpleTest.finish(); + }); + }); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_controller.html b/dom/workers/test/serviceworkers/test_controller.html new file mode 100644 index 000000000..789d7746d --- /dev/null +++ b/dom/workers/test/serviceworkers/test_controller.html @@ -0,0 +1,84 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1002570 - test controller instance.</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var content; + var iframe; + var registration; + + function simpleRegister() { + // We use the control scope for the less specific registration. The window will register a worker on controller/ + return navigator.serviceWorker.register("worker.js", { scope: "./control" }) + .then(function(reg) { + registration = reg; + });; + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed: " + e + "\n"); + }); + } + + function testController() { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "done") { + window.onmessage = null; + content.removeChild(iframe); + resolve(); + } + } + }); + + content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "controller/index.html"); + content.appendChild(iframe); + + return p; + } + + // This document just flips the prefs and opens the iframe for the actual test. + function runTest() { + simpleRegister() + .then(testController) + .then(unregister) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_cross_origin_url_after_redirect.html b/dom/workers/test/serviceworkers/test_cross_origin_url_after_redirect.html new file mode 100644 index 000000000..e56bb84ca --- /dev/null +++ b/dom/workers/test/serviceworkers/test_cross_origin_url_after_redirect.html @@ -0,0 +1,50 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test access to a cross origin Request.url property from a service worker for a redirected intercepted iframe</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/requesturl/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/requesturl/index.html"; + } else if (e.data.status == "done") { + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/requesturl/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_csp_upgrade-insecure_intercept.html b/dom/workers/test/serviceworkers/test_csp_upgrade-insecure_intercept.html new file mode 100644 index 000000000..fe4cb991c --- /dev/null +++ b/dom/workers/test/serviceworkers/test_csp_upgrade-insecure_intercept.html @@ -0,0 +1,55 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that a CSP upgraded request can be intercepted by a service worker</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/upgrade-insecure/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/upgrade-insecure/embedder.html"; + } else if (e.data.status == "protocol") { + is(e.data.data, "https:", "Correct protocol expected"); + } else if (e.data.status == "image") { + is(e.data.data, 40, "The image request was upgraded before interception"); + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/upgrade-insecure/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + // This is needed so that we can test upgrading a non-secure load inside an https iframe. + ["security.mixed_content.block_active_content", false], + ["security.mixed_content.block_display_content", false], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_empty_serviceworker.html b/dom/workers/test/serviceworkers/test_empty_serviceworker.html new file mode 100644 index 000000000..e42951896 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_empty_serviceworker.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that registering an empty service worker works</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("empty.js", {scope: "."}); + } + + function done(registration) { + ok(registration.waiting || registration.active, "registration worked"); + registration.unregister().then(function(success) { + ok(success, "unregister worked"); + SimpleTest.finish(); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_error_reporting.html b/dom/workers/test/serviceworkers/test_error_reporting.html new file mode 100644 index 000000000..619d602e8 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_error_reporting.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test Error Reporting of Service Worker Failures</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/SpawnTask.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +/** + * Test that a bunch of service worker coding errors and failure modes that + * might otherwise be hard to diagnose are surfaced as console error messages. + * The driving use-case is minimizing cursing from a developer looking at a + * document in Firefox testing a page that involves service workers. + * + * This test assumes that errors will be reported via + * ServiceWorkerManager::ReportToAllClients and that that method is reliable and + * tested via some other file. + **/ + +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.testing.enabled", true], + ]}); +}); + +/** + * Ensure an error is logged during the initial registration of a SW when a 404 + * is received. + */ +add_task(function* register_404() { + // Start monitoring for the error + let expectedMessage = expect_console_message( + "ServiceWorkerRegisterNetworkError", + [make_absolute_url("network_error/"), "404", make_absolute_url("404.js")]); + + // Register, generating the 404 error. This will reject with a TypeError + // which we need to consume so it doesn't get thrown at our generator. + yield navigator.serviceWorker.register("404.js", { scope: "network_error/" }) + .then( + () => { ok(false, "should have rejected"); }, + (e) => { ok(e.name === "TypeError", "404 failed as expected"); }); + + yield wait_for_expected_message(expectedMessage); +}); + +/** + * Ensure an error is logged when the service worker is being served with a + * MIME type of text/plain rather than a JS type. + */ +add_task(function* register_bad_mime_type() { + let expectedMessage = expect_console_message( + "ServiceWorkerRegisterMimeTypeError", + [make_absolute_url("bad_mime_type/"), "text/plain", + make_absolute_url("sw_bad_mime_type.js")]); + + // consume the expected rejection so it doesn't get thrown at us. + yield navigator.serviceWorker.register("sw_bad_mime_type.js", { scope: "bad_mime_type/" }) + .then( + () => { ok(false, "should have rejected"); }, + (e) => { ok(e.name === "SecurityError", "bad MIME type failed as expected"); }); + + yield wait_for_expected_message(expectedMessage); +}); +</script> + +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_escapedSlashes.html b/dom/workers/test/serviceworkers/test_escapedSlashes.html new file mode 100644 index 000000000..001c66024 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_escapedSlashes.html @@ -0,0 +1,102 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for escaped slashes in navigator.serviceWorker.register</title> + <script type="text/javascript" src="http://mochi.test:8888/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="http://mochi.test:8888/tests/SimpleTest/test.css" /> + <base href="https://mozilla.org/"> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + +var tests = [ + { status: true, + scriptURL: "a.js?foo%2fbar", + scopeURL: null }, + { status: false, + scriptURL: "foo%2fbar", + scopeURL: null }, + { status: true, + scriptURL: "a.js?foo%2Fbar", + scopeURL: null }, + { status: false, + scriptURL: "foo%2Fbar", + scopeURL: null }, + { status: true, + scriptURL: "a.js?foo%5cbar", + scopeURL: null }, + { status: false, + scriptURL: "foo%5cbar", + scopeURL: null }, + { status: true, + scriptURL: "a.js?foo%2Cbar", + scopeURL: null }, + { status: false, + scriptURL: "foo%5Cbar", + scopeURL: null }, + { status: true, + scriptURL: "ok.js", + scopeURL: "/scope?foo%2fbar"}, + { status: false, + scriptURL: "ok.js", + scopeURL: "/foo%2fbar"}, + { status: true, + scriptURL: "ok.js", + scopeURL: "/scope?foo%2Fbar"}, + { status: false, + scriptURL: "ok.js", + scopeURL: "foo%2Fbar"}, + { status: true, + scriptURL: "ok.js", + scopeURL: "/scope?foo%5cbar"}, + { status: false, + scriptURL: "ok.js", + scopeURL: "foo%5cbar"}, + { status: true, + scriptURL: "ok.js", + scopeURL: "/scope?foo%5Cbar"}, + { status: false, + scriptURL: "ok.js", + scopeURL: "foo%5Cbar"}, +]; + +function runTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + navigator.serviceWorker.register(test.scriptURL, test.scopeURL) + .then(reg => { + ok(false, "Register should fail"); + }, err => { + if (!test.status) { + is(err.name, "TypeError", "Registration should fail with TypeError"); + } else { + ok(test.status, "Register should fail"); + } + }) + .then(runTest); +} + +SimpleTest.waitForExplicitFinish(); +onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.enabled", true], + ]}, runTest); +}; + +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_eval_allowed.html b/dom/workers/test/serviceworkers/test_eval_allowed.html new file mode 100644 index 000000000..bfe8ac280 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_eval_allowed.html @@ -0,0 +1,51 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1160458 - CSP activated by default in Service Workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + function register() { + return navigator.serviceWorker.register("eval_worker.js"); + } + + function runTest() { + try { + eval("1"); + ok(false, "should throw"); + } + catch (ex) { + ok(true, "did throw"); + } + register() + .then(function(swr) { + ok(true, "eval restriction didn't get inherited"); + swr.unregister() + .then(function() { + SimpleTest.finish(); + }); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_eval_allowed.html^headers^ b/dom/workers/test/serviceworkers/test_eval_allowed.html^headers^ new file mode 100644 index 000000000..51ffaa71d --- /dev/null +++ b/dom/workers/test/serviceworkers/test_eval_allowed.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self'" diff --git a/dom/workers/test/serviceworkers/test_eventsource_intercept.html b/dom/workers/test/serviceworkers/test_eventsource_intercept.html new file mode 100644 index 000000000..55df62bb7 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_eventsource_intercept.html @@ -0,0 +1,103 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function testFrame(src) { + return new Promise(function(resolve, reject) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.onmessage = function(e) { + if (e.data.status == "callback") { + switch(e.data.data) { + case "ok": + ok(e.data.condition, e.data.message); + break; + case "ready": + iframe.contentWindow.postMessage({status: "callback", data: "eventsource"}, "*"); + break; + case "done": + window.onmessage = null; + iframe.src = "about:blank"; + document.body.removeChild(iframe); + iframe = null; + resolve(); + break; + default: + ok(false, "Something went wrong"); + break; + } + } else { + ok(false, "Something went wrong"); + } + }; + document.body.appendChild(iframe); + }); + } + + function runTest() { + Promise.resolve() + .then(() => { + info("Going to intercept and test opaque responses"); + return testFrame("eventsource/eventsource_register_worker.html" + + "?script=eventsource_opaque_response_intercept_worker.js"); + }) + .then(() => { + return testFrame("eventsource/eventsource_opaque_response.html"); + }) + .then(() => { + info("Going to intercept and test cors responses"); + return testFrame("eventsource/eventsource_register_worker.html" + + "?script=eventsource_cors_response_intercept_worker.js"); + }) + .then(() => { + return testFrame("eventsource/eventsource_cors_response.html"); + }) + .then(() => { + info("Going to intercept and test synthetic responses"); + return testFrame("eventsource/eventsource_register_worker.html" + + "?script=eventsource_synthetic_response_intercept_worker.js"); + }) + .then(() => { + return testFrame("eventsource/eventsource_synthetic_response.html"); + }) + .then(() => { + info("Going to intercept and test mixed content cors responses"); + return testFrame("https://example.com/tests/dom/workers/test/serviceworkers/" + + "eventsource/eventsource_register_worker.html" + + "?script=eventsource_mixed_content_cors_response_intercept_worker.js"); + }) + .then(() => { + return testFrame("https://example.com/tests/dom/workers/test/serviceworkers/" + + "eventsource/eventsource_mixed_content_cors_response.html"); + }) + .then(SimpleTest.finish) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_fetch_event.html b/dom/workers/test/serviceworkers/test_fetch_event.html new file mode 100644 index 000000000..764be87b1 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_fetch_event.html @@ -0,0 +1,83 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 94048 - test install event.</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + SimpleTest.requestCompleteLog(); + + var registration; + function simpleRegister() { + var p = navigator.serviceWorker.register("fetch_event_worker.js", { scope: "./fetch" }); + return p.then(function(swr) { + registration = swr; + return new Promise(function(resolve) { + swr.installing.onstatechange = resolve; + }); + }); + } + + function unregister() { + return registration.unregister().then(function(success) { + ok(success, "Service worker should be unregistered successfully"); + }, function(e) { + dump("SW unregistration error: " + e + "\n"); + }); + } + + function testController() { + var p = new Promise(function(resolve, reject) { + var reloaded = false; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "done") { + if (reloaded) { + window.onmessage = null; + w.close(); + resolve(); + } else { + w.location.reload(); + reloaded = true; + } + } + } + }); + + var w = window.open("fetch/index.html"); + return p; + } + + function runTest() { + simpleRegister() + .then(testController) + .then(unregister) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_fetch_integrity.html b/dom/workers/test/serviceworkers/test_fetch_integrity.html new file mode 100644 index 000000000..50eb05581 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_fetch_integrity.html @@ -0,0 +1,178 @@ +<!DOCTYPE HTML> +<html> +<head> + <title> Test fetch.integrity on console report for serviceWorker and sharedWorker </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/SpawnTask.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<body> +<div id="content" style="display: none"></div> +<script type="text/javascript"> +"use strict"; + +let security_localizer = + stringBundleService.createBundle("chrome://global/locale/security/security.properties"); + +function expect_security_console_message(/* msgId, args, ... */) { + let expectations = []; + // process repeated paired arguments of: msgId, args + for (let i = 0; i < arguments.length; i += 4) { + let msgId = arguments[i]; + let args = arguments[i + 1]; + let filename = arguments[i + 2]; + let windowId = arguments[i + 3]; + expectations.push({ + errorMessage: security_localizer.formatStringFromName(msgId, args, args.length), + sourceName: filename, + windowID: windowId + }); + } + return new Promise(resolve => { + SimpleTest.monitorConsole(resolve, expectations); + }); +} + +// (This doesn't really need to be its own task, but it allows the actual test +// case to be self-contained.) +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}); +}); + +add_task(function* test_integrity_serviceWorker() { + var filename = make_absolute_url("fetch.js"); + var filename2 = make_absolute_url("fake.html"); + + // The SW will claim us once it activates; this is async, start listening now. + let waitForControlled = new Promise((resolve) => { + navigator.serviceWorker.oncontrollerchange = resolve; + }); + + let registration = yield navigator.serviceWorker.register("fetch.js", + { scope: "./" }); + yield waitForControlled; + + info("Test for mNavigationInterceptions.") + // The client_win will reload to another URL after opening filename2. + let client_win = window.open(filename2); + + // XXX windowID should be innerWindowID + let mainWindowID = SpecialPowers.getDOMWindowUtils(window).outerWindowID; + let clientWindowID = SpecialPowers.getDOMWindowUtils(client_win).outerWindowID; + let expectedMessage = expect_security_console_message( + "MalformedIntegrityHash", + ["abc"], + filename, + mainWindowID, + "NoValidMetadata", + [""], + filename, + mainWindowID + ); + let expectedMessage2 = expect_security_console_message( + "MalformedIntegrityHash", + ["abc"], + filename, + clientWindowID, + "NoValidMetadata", + [""], + filename, + clientWindowID + ); + + info("Test for mControlledDocuments and report error message to console."); + // The fetch will succeed because the integrity value is invalid and we are + // looking for the console message regarding the bad integrity value. + yield fetch("fail.html"); + + yield wait_for_expected_message(expectedMessage); + + yield wait_for_expected_message(expectedMessage2); + + yield registration.unregister(); + client_win.close(); +}); + +add_task(function* test_integrity_sharedWorker() { + var filename = make_absolute_url("sharedWorker_fetch.js"); + + info("Attch main window to a SharedWorker."); + let sharedWorker = new SharedWorker(filename); + let waitForConnected = new Promise((resolve) => { + sharedWorker.port.onmessage = function (e) { + if (e.data == "Connected") { + resolve(); + } else { + reject(); + } + } + }); + yield waitForConnected; + + info("Attch another window to the same SharedWorker."); + // Open another window and its also managed by the shared worker. + let client_win = window.open("create_another_sharedWorker.html"); + let waitForBothConnected = new Promise((resolve) => { + sharedWorker.port.onmessage = function (e) { + if (e.data == "BothConnected") { + resolve(); + } else { + reject(); + } + } + }); + yield waitForBothConnected; + + // XXX windowID should be innerWindowID + let mainWindowID = SpecialPowers.getDOMWindowUtils(window).outerWindowID; + let clientWindowID = SpecialPowers.getDOMWindowUtils(client_win).outerWindowID; + let expectedMessage = expect_security_console_message( + "MalformedIntegrityHash", + ["abc"], + filename, + mainWindowID, + "NoValidMetadata", + [""], + filename, + mainWindowID + ); + let expectedMessage2 = expect_security_console_message( + "MalformedIntegrityHash", + ["abc"], + filename, + clientWindowID, + "NoValidMetadata", + [""], + filename, + clientWindowID + ); + + info("Start to fetch a URL with wrong integrity.") + sharedWorker.port.start(); + sharedWorker.port.postMessage("StartFetchWithWrongIntegrity"); + + let waitForSRIFailed = new Promise((resolve) => { + sharedWorker.port.onmessage = function (e) { + if (e.data == "SRI_failed") { + resolve(); + } else { + reject(); + } + } + }); + yield waitForSRIFailed; + + yield wait_for_expected_message(expectedMessage); + + yield wait_for_expected_message(expectedMessage2); + client_win.close(); +}); + +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_file_blob_response.html b/dom/workers/test/serviceworkers/test_file_blob_response.html new file mode 100644 index 000000000..6db0656c6 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_file_blob_response.html @@ -0,0 +1,86 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1253777 - Test interception using file blob response body</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + var scope = './file_blob_response/'; + function start() { + return navigator.serviceWorker.register("file_blob_response_worker.js", + { scope: scope }) + .then(function(swr) { + registration = swr; + return new Promise(function(resolve) { + registration.installing.onstatechange = function(evt) { + if (evt.target.state === 'activated') { + evt.target.onstate = null; + resolve(); + } + } + }); + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e + "\n"); + }); + } + + function withFrame(url) { + return new Promise(function(resolve, reject) { + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + var frame = document.createElement("iframe"); + frame.setAttribute('src', url); + content.appendChild(frame); + + frame.addEventListener('load', function loadCallback(evt) { + frame.removeEventListener('load', loadCallback); + resolve(frame); + }); + }); + } + + function runTest() { + start() + .then(function() { + return withFrame(scope + 'dummy.txt'); + }) + .then(function(frame) { + var result = JSON.parse(frame.contentWindow.document.body.textContent); + frame.remove(); + is(result.value, 'success'); + }) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }) + .then(unregister) + .then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_file_blob_upload.html b/dom/workers/test/serviceworkers/test_file_blob_upload.html new file mode 100644 index 000000000..30a31eb7e --- /dev/null +++ b/dom/workers/test/serviceworkers/test_file_blob_upload.html @@ -0,0 +1,145 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1203680 - Test interception of file blob uploads</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + var iframe; + function start() { + return navigator.serviceWorker.register("empty.js", + { scope: "./sw_clients/" }) + .then((swr) => registration = swr); + } + + function unregister() { + if (iframe) { + iframe.remove(); + iframe = null; + } + + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e + "\n"); + }); + } + + function withFrame() { + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/file_blob_upload_frame.html"); + content.appendChild(iframe); + + return new Promise(function(resolve, reject) { + window.addEventListener('message', function readyCallback(evt) { + window.removeEventListener('message', readyCallback); + if (evt.data.status === 'READY') { + resolve(); + } else { + reject(evt.data.result); + } + }); + }); + } + + function postBlob(body) { + return new Promise(function(resolve, reject) { + window.addEventListener('message', function postBlobCallback(evt) { + window.removeEventListener('message', postBlobCallback); + if (evt.data.status === 'OK') { + is(JSON.stringify(body), JSON.stringify(evt.data.result), + 'body echoed back correctly'); + resolve(); + } else { + reject(evt.data.result); + } + }); + + iframe.contentWindow.postMessage({ type: 'TEST', body: body }, '*'); + }); + } + + function generateMessage(length) { + + var lorem = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis egestas ' + 'vehicula tortor eget ultrices. Sed et luctus est. Nunc eu orci ligula. ' + 'In vel ornare eros, eget lacinia diam. Praesent vel metus mattis, ' + 'cursus nulla sit amet, rhoncus diam. Aliquam nulla tortor, aliquet et ' + 'viverra non, dignissim vel tellus. Praesent sed ex in dolor aliquet ' + 'aliquet. In at facilisis sem, et aliquet eros. Maecenas feugiat nisl ' + 'quis elit blandit posuere. Duis viverra odio sed eros consectetur, ' + 'viverra mattis ligula volutpat.'; + + var result = ''; + + while (result.length < length) { + var remaining = length - result.length; + if (remaining < lorem.length) { + result += lorem.slice(0, remaining); + } else { + result += lorem; + } + } + + return result; + } + + var smallBody = generateMessage(64); + var mediumBody = generateMessage(1024); + + // TODO: Test large bodies over the default pipe size. Currently stalls + // due to bug 1134372. + //var largeBody = generateMessage(100 * 1024); + + function runTest() { + start() + .then(withFrame) + .then(function() { + return postBlob({ hops: 0, message: smallBody }); + }) + .then(function() { + return postBlob({ hops: 1, message: smallBody }); + }) + .then(function() { + return postBlob({ hops: 10, message: smallBody }); + }) + .then(function() { + return postBlob({ hops: 0, message: mediumBody }); + }) + .then(function() { + return postBlob({ hops: 1, message: mediumBody }); + }) + .then(function() { + return postBlob({ hops: 10, message: mediumBody }); + }) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_force_refresh.html b/dom/workers/test/serviceworkers/test_force_refresh.html new file mode 100644 index 000000000..05caf0e6a --- /dev/null +++ b/dom/workers/test/serviceworkers/test_force_refresh.html @@ -0,0 +1,84 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test service worker post message </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + function start() { + return navigator.serviceWorker.register("force_refresh_worker.js", + { scope: "./sw_clients/" }) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + + function testForceRefresh(swr) { + var p = new Promise(function(res, rej) { + var count = 0; + var cachedCount = 0; + window.onmessage = function(e) { + if (e.data === "READY") { + count += 1; + if (count == 2) { + is(cachedCount, 1, "should have received cached message before " + + "second non-cached message"); + res(); + } + iframe.contentWindow.postMessage("REFRESH", "*"); + } else if (e.data === "READY_CACHED") { + cachedCount += 1; + is(count, 1, "should have received non-cached message before " + + "cached message"); + iframe.contentWindow.postMessage("FORCE_REFRESH", "*"); + } + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/refresher_compressed.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + start() + .then(testForceRefresh) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_gzip_redirect.html b/dom/workers/test/serviceworkers/test_gzip_redirect.html new file mode 100644 index 000000000..7ac92122c --- /dev/null +++ b/dom/workers/test/serviceworkers/test_gzip_redirect.html @@ -0,0 +1,84 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test service worker post message </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + function start() { + return navigator.serviceWorker.register("gzip_redirect_worker.js", + { scope: "./sw_clients/" }) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + + function testGzipRedirect(swr) { + var p = new Promise(function(res, rej) { + var navigatorReady = false; + var finalReady = false; + + window.onmessage = function(e) { + if (e.data === "NAVIGATOR_READY") { + ok(!navigatorReady, "should only get navigator ready message once"); + ok(!finalReady, "should get navigator ready before final redirect ready message"); + navigatorReady = true; + iframe.contentWindow.postMessage({ + type: "NAVIGATE", + url: "does_not_exist.html" + }, "*"); + } else if (e.data === "READY") { + ok(navigatorReady, "should only get navigator ready message once"); + ok(!finalReady, "should get final ready message only once"); + finalReady = true; + res(); + } + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/navigator.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + start() + .then(testGzipRedirect) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_hsts_upgrade_intercept.html b/dom/workers/test/serviceworkers/test_hsts_upgrade_intercept.html new file mode 100644 index 000000000..dfce406b8 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_hsts_upgrade_intercept.html @@ -0,0 +1,66 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that an HSTS upgraded request can be intercepted by a service worker</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + var framesLoaded = 0; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/hsts/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "http://example.com/tests/dom/workers/test/serviceworkers/fetch/hsts/index.html"; + } else if (e.data.status == "protocol") { + is(e.data.data, "https:", "Correct protocol expected"); + ok(e.data.securityInfoPresent, "Security info present on intercepted value"); + switch (++framesLoaded) { + case 1: + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/hsts/embedder.html"; + break; + case 2: + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/hsts/image.html"; + break; + } + } else if (e.data.status == "image") { + is(e.data.data, 40, "The image request was upgraded before interception"); + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/hsts/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + SpecialPowers.cleanUpSTSData("http://example.com"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + // This is needed so that we can test upgrading a non-secure load inside an https iframe. + ["security.mixed_content.block_active_content", false], + ["security.mixed_content.block_display_content", false], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_https_fetch.html b/dom/workers/test/serviceworkers/test_https_fetch.html new file mode 100644 index 000000000..e990200f8 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_https_fetch.html @@ -0,0 +1,62 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1133763 - test fetch event in HTTPS origins</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/https/register.html"; + var ios; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + ios.offline = true; + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/https/index.html"; + } else if (e.data.status == "done") { + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/https/synth-sw.html"; + } else if (e.data.status == "done-synth-sw") { + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/https/synth-window.html"; + } else if (e.data.status == "done-synth-window") { + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/https/synth.html"; + } else if (e.data.status == "done-synth") { + ios.offline = false; + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/https/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true] + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_https_fetch_cloned_response.html b/dom/workers/test/serviceworkers/test_https_fetch_cloned_response.html new file mode 100644 index 000000000..1cf1dbef1 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_https_fetch_cloned_response.html @@ -0,0 +1,56 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1133763 - test fetch event in HTTPS origins with a cloned response</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/https/clonedresponse/register.html"; + var ios; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + ios.offline = true; + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/https/clonedresponse/index.html"; + } else if (e.data.status == "done") { + ios.offline = false; + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/https/clonedresponse/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true] + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_https_origin_after_redirect.html b/dom/workers/test/serviceworkers/test_https_origin_after_redirect.html new file mode 100644 index 000000000..3878a1df6 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_https_origin_after_redirect.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/origin/https/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("https://example.com/tests/dom/workers/test/serviceworkers/fetch/origin/https/index-https.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "https://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/origin/https/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_https_origin_after_redirect_cached.html b/dom/workers/test/serviceworkers/test_https_origin_after_redirect_cached.html new file mode 100644 index 000000000..81a1d1da0 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_https_origin_after_redirect_cached.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/origin/https/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("https://example.com/tests/dom/workers/test/serviceworkers/fetch/origin/https/index-cached-https.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "https://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/origin/https/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_https_synth_fetch_from_cached_sw.html b/dom/workers/test/serviceworkers/test_https_synth_fetch_from_cached_sw.html new file mode 100644 index 000000000..7bf3b352a --- /dev/null +++ b/dom/workers/test/serviceworkers/test_https_synth_fetch_from_cached_sw.html @@ -0,0 +1,69 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1156847 - test fetch event generating a synthesized response in HTTPS origins from a cached SW</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" tyle="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/https/register.html"; + var ios; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + ios.offline = true; + + // In order to load synth.html from a cached service worker, we first + // remove the existing window that is keeping the service worker alive, + // and do a GC to ensure that the SW is destroyed. This way, when we + // load synth.html for the second time, we will first recreate the + // service worker from the cache. This is intended to test that we + // properly store and retrieve the security info from the cache. + iframe.parentNode.removeChild(iframe); + iframe = null; + SpecialPowers.exactGC(function() { + iframe = document.createElement("iframe"); + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/https/synth.html"; + document.body.appendChild(iframe); + }); + } else if (e.data.status == "done-synth") { + ios.offline = false; + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/https/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true] + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_imagecache.html b/dom/workers/test/serviceworkers/test_imagecache.html new file mode 100644 index 000000000..8627b54af --- /dev/null +++ b/dom/workers/test/serviceworkers/test_imagecache.html @@ -0,0 +1,55 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1202085 - Test that images from different controllers don't cached together</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/imagecache/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/imagecache/index.html"; + } else if (e.data.status == "result") { + is(e.data.url, "image-40px.png", "Correct url expected"); + is(e.data.width, 40, "Correct width expected"); + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/imagecache/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/imagecache/postmortem.html"; + } else if (e.data.status == "postmortem") { + is(e.data.width, 20, "Correct width expected"); + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_imagecache_max_age.html b/dom/workers/test/serviceworkers/test_imagecache_max_age.html new file mode 100644 index 000000000..eb3c1f166 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_imagecache_max_age.html @@ -0,0 +1,71 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that the image cache respects a synthesized image's Cache headers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + var framesLoaded = 0; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/imagecache-maxage/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/imagecache-maxage/index.html"; + } else if (e.data.status == "result") { + switch (++framesLoaded) { + case 1: + is(e.data.url, "image-20px.png", "Correct url expected"); + is(e.data.url2, "image-20px.png", "Correct url expected"); + is(e.data.width, 20, "Correct width expected"); + is(e.data.width2, 20, "Correct width expected"); + // Wait for 100ms so that the image gets expired. + setTimeout(function() { + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/imagecache-maxage/index.html?new" + }, 100); + break; + case 2: + is(e.data.url, "image-40px.png", "Correct url expected"); + is(e.data.url2, "image-40px.png", "Correct url expected"); + is(e.data.width, 40, "Correct width expected"); + is(e.data.width2, 40, "Correct width expected"); + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/imagecache-maxage/unregister.html"; + break; + default: + ok(false, "This should never happen"); + } + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + SimpleTest.finish(); + } + }; + } + + SimpleTest.requestFlakyTimeout("This test needs to simulate the passing of time"); + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_importscript.html b/dom/workers/test/serviceworkers/test_importscript.html new file mode 100644 index 000000000..5d2d5b352 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_importscript.html @@ -0,0 +1,72 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test service worker - script cache policy</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="content"></div> +<script class="testbody" type="text/javascript"> + function start() { + return navigator.serviceWorker.register("importscript_worker.js", + { scope: "./sw_clients/" }) + .then((swr) => registration = swr); + } + + function unregister() { + return fetch("importscript.sjs?clearcounter").then(function() { + return registration.unregister(); + }).then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function testPostMessage(swr) { + var p = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (e.data === "READY") { + swr.active.postMessage("do magic"); + return; + } + + ok(e.data === "OK", "Worker posted the correct value: " + e.data); + res(); + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/service_worker_controlled.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + start() + .then(testPostMessage) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_importscript_mixedcontent.html b/dom/workers/test/serviceworkers/test_importscript_mixedcontent.html new file mode 100644 index 000000000..a659af92b --- /dev/null +++ b/dom/workers/test/serviceworkers/test_importscript_mixedcontent.html @@ -0,0 +1,53 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1198078 - test that we respect mixed content blocking in importScript() inside service workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/importscript-mixedcontent/register.html"; + var ios; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/importscript-mixedcontent/index.html"; + } else if (e.data.status == "done") { + ok(e.data.data, "good", "Mixed content blocking should work correctly for service workers"); + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/fetch/importscript-mixedcontent/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["security.mixed_content.block_active_content", false], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_install_event.html b/dom/workers/test/serviceworkers/test_install_event.html new file mode 100644 index 000000000..0e59510fe --- /dev/null +++ b/dom/workers/test/serviceworkers/test_install_event.html @@ -0,0 +1,144 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 94048 - test install event.</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + var p = navigator.serviceWorker.register("worker.js", { scope: "./install_event" }); + return p; + } + + function nextRegister(reg) { + ok(reg instanceof ServiceWorkerRegistration, "reg should be a ServiceWorkerRegistration"); + var p = navigator.serviceWorker.register("install_event_worker.js", { scope: "./install_event" }); + return p.then(function(swr) { + ok(reg === swr, "register should resolve to the same registration object"); + var update_found_promise = new Promise(function(resolve, reject) { + swr.addEventListener('updatefound', function(e) { + ok(true, "Received onupdatefound"); + resolve(); + }); + }); + + var worker_activating = new Promise(function(res, reject) { + ok(swr.installing instanceof ServiceWorker, "There should be an installing worker if promise resolves."); + ok(swr.installing.state == "installing", "Installing worker's state should be 'installing'"); + swr.installing.onstatechange = function(e) { + if (e.target.state == "activating") { + e.target.onstatechange = null; + res(); + } + } + }); + + return Promise.all([update_found_promise, worker_activating]); + }, function(e) { + ok(false, "Unexpected Error in nextRegister! " + e); + }); + } + + function installError() { + // Silence worker errors so they don't cause the test to fail. + window.onerror = function(e) {} + return navigator.serviceWorker.register("install_event_error_worker.js", { scope: "./install_event" }) + .then(function(swr) { + ok(swr.installing instanceof ServiceWorker, "There should be an installing worker if promise resolves."); + ok(swr.installing.state == "installing", "Installing worker's state should be 'installing'"); + return new Promise(function(resolve, reject) { + swr.installing.onstatechange = function(e) { + ok(e.target.state == "redundant", "Installation of worker with error should fail."); + resolve(); + } + }); + }).then(function() { + return navigator.serviceWorker.getRegistration("./install_event").then(function(swr) { + var newest = swr.waiting || swr.active; + ok(newest, "Waiting or active worker should still exist"); + ok(newest.scriptURL.match(/install_event_worker.js$/), "Previous worker should remain the newest worker"); + }); + }); + } + + function testActive(worker) { + is(worker.state, "activating", "Should be activating"); + return new Promise(function(resolve, reject) { + worker.onstatechange = function(e) { + e.target.onstatechange = null; + is(e.target.state, "activated", "Activation of worker with error in activate event handler should still succeed."); + resolve(); + } + }); + } + + function activateErrorShouldSucceed() { + // Silence worker errors so they don't cause the test to fail. + window.onerror = function() { } + return navigator.serviceWorker.register("activate_event_error_worker.js", { scope: "./activate_error" }) + .then(function(swr) { + var p = new Promise(function(resolve, reject) { + ok(swr.installing.state == "installing", "activateErrorShouldSucceed(): Installing worker's state should be 'installing'"); + swr.installing.onstatechange = function(e) { + e.target.onstatechange = null; + if (swr.waiting) { + swr.waiting.onstatechange = function(e) { + e.target.onstatechange = null; + testActive(swr.active).then(resolve, reject); + } + } else { + testActive(swr.active).then(resolve, reject); + } + } + }); + + return p.then(function() { + return Promise.resolve(swr); + }); + }).then(function(swr) { + return swr.unregister(); + }); + } + + function unregister() { + return navigator.serviceWorker.getRegistration("./install_event").then(function(reg) { + return reg.unregister(); + }); + } + + function runTest() { + Promise.resolve() + .then(simpleRegister) + .then(nextRegister) + .then(installError) + .then(activateErrorShouldSucceed) + .then(unregister) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_install_event_gc.html b/dom/workers/test/serviceworkers/test_install_event_gc.html new file mode 100644 index 000000000..ccccd2b43 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_install_event_gc.html @@ -0,0 +1,121 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test install event being GC'd before waitUntil fulfills</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +var script = 'blocking_install_event_worker.js'; +var scope = 'sw_clients/simple.html?install-event-gc'; +var registration; + +function register() { + return navigator.serviceWorker.register(script, { scope: scope }) + .then(swr => registration = swr); +} + +function unregister() { + if (!registration) { + return; + } + return registration.unregister(); +} + +function waitForInstallEvent() { + return new Promise((resolve, reject) => { + navigator.serviceWorker.addEventListener('message', evt => { + if (evt.data.type === 'INSTALL_EVENT') { + resolve(); + } + }); + }); +} + +function gcWorker() { + return new Promise(function(resolve, reject) { + // We are able to trigger asynchronous garbage collection and cycle + // collection by emitting "child-cc-request" and "child-gc-request" + // observer notifications. The worker RuntimeService will translate + // these notifications into the appropriate operation on all known + // worker threads. + // + // In the failure case where GC/CC causes us to abort the installation, + // we will know something happened from the statechange event. + const statechangeHandler = evt => { + // Reject rather than resolving to avoid the possibility of us seeing + // an unrelated racing statechange somehow. Since in the success case we + // will still see a state change on termination, we do explicitly need to + // be removed on the success path. + ok(registration.installing, 'service worker is still installing?'); + reject(); + }; + registration.installing.addEventListener('statechange', statechangeHandler); + // In the success case since the service worker installation is effectively + // hung, we instead depend on sending a 'ping' message to the service worker + // and hearing it 'pong' back. Since we issue our postMessage after we + // trigger the GC/CC, our 'ping' will only be processed after the GC/CC and + // therefore the pong will also strictly occur after the cycle collection. + navigator.serviceWorker.addEventListener('message', evt => { + if (evt.data.type === 'pong') { + registration.installing.removeEventListener( + 'statechange', statechangeHandler); + resolve(); + } + }); + // At the current time, the service worker will exist in our same process + // and notifyObservers is synchronous. However, in the future, service + // workers may end up in a separate process and in that case it will be + // appropriate to use notifyObserversInParentProcess or something like it. + // (notifyObserversInParentProcess is a synchronous IPC call to the parent + // process's main thread. IPDL PContent::CycleCollect is an async message. + // Ordering will be maintained if the postMessage goes via PContent as well, + // but that seems unlikely.) + SpecialPowers.notifyObservers(null, 'child-gc-request', null); + SpecialPowers.notifyObservers(null, 'child-cc-request', null); + SpecialPowers.notifyObservers(null, 'child-gc-request', null); + // (Only send the ping after we set the gc/cc/gc in motion.) + registration.installing.postMessage({ type: 'ping' }); + }); +} + +function terminateWorker() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.idle_timeout", 0], + ["dom.serviceWorkers.idle_extended_timeout", 0] + ] + }).then(_ => { + registration.installing.postMessage({ type: 'RESET_TIMER' }); + }); +} + +function runTest() { + Promise.all([ + waitForInstallEvent(), + register() + ]).then(_ => ok(registration.installing, 'service worker is installing')) + .then(gcWorker) + .then(_ => ok(registration.installing, 'service worker is still installing')) + .then(terminateWorker) + .catch(e => ok(false, e)) + .then(unregister) + .then(SimpleTest.finish); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], +]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_installation_simple.html b/dom/workers/test/serviceworkers/test_installation_simple.html new file mode 100644 index 000000000..1b0d6c947 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_installation_simple.html @@ -0,0 +1,212 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 930348 - test stub Navigator ServiceWorker utilities.</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + var p = navigator.serviceWorker.register("worker.js", { scope: "simpleregister/" }); + ok(p instanceof Promise, "register() should return a Promise"); + return Promise.resolve(); + } + + function sameOriginWorker() { + p = navigator.serviceWorker.register("http://some-other-origin/worker.js"); + return p.then(function(w) { + ok(false, "Worker from different origin should fail"); + }, function(e) { + ok(e.name === "SecurityError", "Should fail with a SecurityError"); + }); + } + + function sameOriginScope() { + p = navigator.serviceWorker.register("worker.js", { scope: "http://www.example.com/" }); + return p.then(function(w) { + ok(false, "Worker controlling scope for different origin should fail"); + }, function(e) { + ok(e.name === "SecurityError", "Should fail with a SecurityError"); + }); + } + + function httpsOnly() { + var promise = new Promise(function(resolve) { + SpecialPowers.pushPrefEnv({'set': [["dom.serviceWorkers.testing.enabled", false]] }, resolve); + }); + + return promise.then(function() { + return navigator.serviceWorker.register("/worker.js"); + }).then(function(w) { + ok(false, "non-HTTPS pages cannot register ServiceWorkers"); + }, function(e) { + ok(e.name === "SecurityError", "Should fail with a SecurityError"); + }).then(function() { + return new Promise((resolve) => SpecialPowers.popPrefEnv(resolve)); + }); + } + + function realWorker() { + var p = navigator.serviceWorker.register("worker.js", { scope: "realworker" }); + return p.then(function(wr) { + ok(wr instanceof ServiceWorkerRegistration, "Register a ServiceWorker"); + + info(wr.scope); + ok(wr.scope == (new URL("realworker", document.baseURI)).href, "Scope should match"); + // active, waiting, installing should return valid worker instances + // because the registration is for the realworker scope, so the workers + // should be obtained for that scope and not for + // test_installation_simple.html + var worker = wr.installing; + ok(worker && wr.scope.match(/realworker$/) && + worker.scriptURL.match(/worker.js$/), "Valid worker instance should be available."); + return wr.unregister().then(function(success) { + ok(success, "The worker should be unregistered successfully"); + }, function(e) { + dump("Error unregistering the worker: " + e + "\n"); + }); + }, function(e) { + info("Error: " + e.name); + ok(false, "realWorker Registration should have succeeded!"); + }); + } + + function networkError404() { + return navigator.serviceWorker.register("404.js", { scope: "network_error/"}).then(function(w) { + ok(false, "404 response should fail with TypeError"); + }, function(e) { + ok(e.name === "TypeError", "404 response should fail with TypeError"); + }); + } + + function redirectError() { + return navigator.serviceWorker.register("redirect_serviceworker.sjs", { scope: "redirect_error/" }).then(function(swr) { + ok(false, "redirection should fail"); + }, function (e) { + ok(e.name === "SecurityError", "redirection should fail with SecurityError"); + }); + } + + function parseError() { + var p = navigator.serviceWorker.register("parse_error_worker.js", { scope: "parse_error/" }); + return p.then(function(wr) { + ok(false, "Registration should fail with parse error"); + return navigator.serviceWorker.getRegistration("parse_error/").then(function(swr) { + // See https://github.com/slightlyoff/ServiceWorker/issues/547 + is(swr, undefined, "A failed registration for a scope with no prior controllers should clear itself"); + }); + }, function(e) { + ok(e instanceof Error, "Registration should fail with parse error"); + }); + } + + // FIXME(nsm): test for parse error when Update step doesn't happen (directly from register). + + function updatefound() { + var frame = document.createElement("iframe"); + frame.setAttribute("id", "simpleregister-frame"); + frame.setAttribute("src", new URL("simpleregister/index.html", document.baseURI).href); + document.body.appendChild(frame); + var resolve, reject; + var p = new Promise(function(res, rej) { + resolve = res; + reject = rej; + }); + + var reg; + function continueTest() { + navigator.serviceWorker.register("worker2.js", { scope: "simpleregister/" }) + .then(function(r) { + reg = r; + });; + } + + window.onmessage = function(e) { + if (e.data.type == "ready") { + continueTest(); + } else if (e.data.type == "finish") { + window.onmessage = null; + // We have to make frame navigate away, otherwise it will call + // MaybeStopControlling() when this document is unloaded. At that point + // the pref has been disabled, so the ServiceWorkerManager is not available. + frame.setAttribute("src", new URL("about:blank").href); + reg.unregister().then(function(success) { + ok(success, "The worker should be unregistered successfully"); + resolve(); + }, function(e) { + dump("Error unregistering the worker: " + e + "\n"); + }); + } else if (e.data.type == "check") { + ok(e.data.status, e.data.msg); + } + } + return p; + } + + var readyPromiseResolved = false; + + function readyPromise() { + var frame = document.createElement("iframe"); + frame.setAttribute("id", "simpleregister-frame-ready"); + frame.setAttribute("src", new URL("simpleregister/ready.html", document.baseURI).href); + document.body.appendChild(frame); + + var channel = new MessageChannel(); + frame.addEventListener('load', function() { + frame.contentWindow.postMessage('your port!', '*', [channel.port2]); + }, false); + + channel.port1.onmessage = function() { + readyPromiseResolved = true; + } + + return Promise.resolve(); + } + + function checkReadyPromise() { + ok(readyPromiseResolved, "The ready promise has been resolved!"); + return Promise.resolve(); + } + + function runTest() { + simpleRegister() + .then(readyPromise) + .then(sameOriginWorker) + .then(sameOriginScope) + .then(httpsOnly) + .then(realWorker) + .then(networkError404) + .then(redirectError) + .then(parseError) + .then(updatefound) + .then(checkReadyPromise) + // put more tests here. + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_match_all.html b/dom/workers/test/serviceworkers/test_match_all.html new file mode 100644 index 000000000..f4e65a730 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_match_all.html @@ -0,0 +1,80 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - test match_all not crashing</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + // match_all_worker will call matchAll until the worker shuts down. + // Test passes if the browser doesn't crash on leaked promise objects. + var registration; + var content; + var iframe; + + function simpleRegister() { + return navigator.serviceWorker.register("match_all_worker.js", + { scope: "./sw_clients/" }) + .then((swr) => registration = swr); + } + + function closeAndUnregister() { + content.removeChild(iframe); + + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function openClient() { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data === "READY") { + resolve(); + } + } + }); + + content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/simple.html"); + content.appendChild(iframe); + + return p; + } + + function runTest() { + simpleRegister() + .then(openClient) + .then(closeAndUnregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(function() { + ok(true, "Didn't crash on resolving matchAll promises while worker shuts down."); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_match_all_advanced.html b/dom/workers/test/serviceworkers/test_match_all_advanced.html new file mode 100644 index 000000000..a458ed70b --- /dev/null +++ b/dom/workers/test/serviceworkers/test_match_all_advanced.html @@ -0,0 +1,100 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test matchAll with multiple clients</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var client_iframes = []; + var registration; + + function start() { + return navigator.serviceWorker.register("match_all_advanced_worker.js", + { scope: "./sw_clients/" }).then(function(swr) { + registration = swr; + window.onmessage = function (e) { + if (e.data === "READY") { + ok(registration.active, "Worker is active."); + registration.active.postMessage("RUN"); + } + } + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + + function testMatchAll() { + var p = new Promise(function(res, rej) { + navigator.serviceWorker.onmessage = function (e) { + ok(e.data === client_iframes.length, "MatchAll returned the correct number of clients."); + res(); + } + }); + + content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/service_worker_controlled.html"); + content.appendChild(iframe); + + client_iframes.push(iframe); + return p; + } + + function removeAndTest() { + content = document.getElementById("content"); + ok(content, "Parent exists."); + + content.removeChild(client_iframes.pop()); + content.removeChild(client_iframes.pop()); + + return testMatchAll(); + } + + function runTest() { + start() + .then(testMatchAll) + .then(testMatchAll) + .then(testMatchAll) + .then(removeAndTest) + .then(function(e) { + content = document.getElementById("content"); + while (client_iframes.length) { + content.removeChild(client_iframes.pop()); + } + }).then(unregister).catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(function() { + SimpleTest.finish(); + }); + + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_match_all_client_id.html b/dom/workers/test/serviceworkers/test_match_all_client_id.html new file mode 100644 index 000000000..4c8ed9673 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_match_all_client_id.html @@ -0,0 +1,91 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1058311 - Test matchAll client id </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + var clientURL = "match_all_client/match_all_client_id.html"; + function start() { + return navigator.serviceWorker.register("match_all_client_id_worker.js", + { scope: "./match_all_client/" }) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function getMessageListener() { + return new Promise(function(res, rej) { + window.onmessage = function(e) { + ok(e.data, "Same client id for multiple calls."); + is(e.origin, "http://mochi.test:8888", "Event should have the correct origin"); + + if (!e.data) { + rej(); + return; + } + + info("DONE from: " + e.source); + res(); + } + }); + } + + function testNestedWindow() { + var p = getMessageListener(); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + + content.appendChild(iframe); + iframe.setAttribute('src', clientURL); + + return p.then(() => content.removeChild(iframe)); + } + + function testAuxiliaryWindow() { + var p = getMessageListener(); + var w = window.open(clientURL); + + return p.then(() => w.close()); + } + + function runTest() { + info(window.opener == undefined); + start() + .then(testAuxiliaryWindow) + .then(testNestedWindow) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_match_all_client_properties.html b/dom/workers/test/serviceworkers/test_match_all_client_properties.html new file mode 100644 index 000000000..14e3445a4 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_match_all_client_properties.html @@ -0,0 +1,97 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1058311 - Test matchAll clients properties </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + var clientURL = "match_all_clients/match_all_controlled.html"; + function start() { + return navigator.serviceWorker.register("match_all_properties_worker.js", + { scope: "./match_all_clients/" }) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function getMessageListener() { + return new Promise(function(res, rej) { + window.onmessage = function(e) { + if (e.data.message === undefined) { + info("rejecting promise"); + rej(); + return; + } + + ok(e.data.result, e.data.message); + + if (!e.data.result) { + rej(); + } + if (e.data.message == "DONE") { + info("DONE from: " + e.source); + res(); + } + } + }); + } + + function testNestedWindow() { + var p = getMessageListener(); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + + content.appendChild(iframe); + iframe.setAttribute('src', clientURL); + + return p.then(() => content.removeChild(iframe)); + } + + function testAuxiliaryWindow() { + var p = getMessageListener(); + var w = window.open(clientURL); + + return p.then(() => w.close()); + } + + function runTest() { + info("catalin"); + info(window.opener == undefined); + start() + .then(testAuxiliaryWindow) + .then(testNestedWindow) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_navigator.html b/dom/workers/test/serviceworkers/test_navigator.html new file mode 100644 index 000000000..164f41bcd --- /dev/null +++ b/dom/workers/test/serviceworkers/test_navigator.html @@ -0,0 +1,40 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 930348 - test stub Navigator ServiceWorker utilities.</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function checkEnabled() { + ok(navigator.serviceWorker, "navigator.serviceWorker should exist when ServiceWorkers are enabled."); + ok(typeof navigator.serviceWorker.register === "function", "navigator.serviceWorker.register() should be a function."); + ok(typeof navigator.serviceWorker.getRegistration === "function", "navigator.serviceWorker.getAll() should be a function."); + ok(typeof navigator.serviceWorker.getRegistrations === "function", "navigator.serviceWorker.getAll() should be a function."); + ok(navigator.serviceWorker.ready instanceof Promise, "navigator.serviceWorker.ready should be a Promise."); + ok(navigator.serviceWorker.controller === null, "There should be no controller worker for an uncontrolled document."); + } + + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true] + ]}, function() { + checkEnabled(); + SimpleTest.finish(); + }); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_not_intercept_plugin.html b/dom/workers/test/serviceworkers/test_not_intercept_plugin.html new file mode 100644 index 000000000..a90e068d3 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_not_intercept_plugin.html @@ -0,0 +1,78 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1187766 - Test loading plugins scenarios with fetch interception.</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + SimpleTest.requestCompleteLog(); + + var registration; + function simpleRegister() { + var p = navigator.serviceWorker.register("./fetch/plugin/worker.js", { scope: "./fetch/plugin/" }); + return p.then(function(swr) { + registration = swr; + return new Promise(function(resolve) { + swr.installing.onstatechange = resolve; + }); + }); + } + + function unregister() { + return registration.unregister().then(function(success) { + ok(success, "Service worker should be unregistered successfully"); + }, function(e) { + dump("SW unregistration error: " + e + "\n"); + }); + } + + function testPlugins() { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "done") { + window.onmessage = null; + w.close(); + resolve(); + } + } + }); + + var w = window.open("fetch/plugin/plugins.html"); + return p; + } + + function runTest() { + simpleRegister() + .then(testPlugins) + .then(unregister) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.requestcontext.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_notification_constructor_error.html b/dom/workers/test/serviceworkers/test_notification_constructor_error.html new file mode 100644 index 000000000..6a8ecf8c0 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_notification_constructor_error.html @@ -0,0 +1,52 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug XXXXXXX - Check that Notification constructor throws in ServiceWorkerGlobalScope</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + return navigator.serviceWorker.register("notification_constructor_error.js", { scope: "notification_constructor_error/" }).then(function(swr) { + ok(false, "Registration should fail."); + }, function(e) { + is(e.name, 'TypeError', "Registration should fail with a TypeError."); + }); + } + + function runTest() { + MockServices.register(); + simpleRegister() + .then(function() { + MockServices.unregister(); + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + MockServices.unregister(); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["notification.prompt.testing", true], + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_notification_get.html b/dom/workers/test/serviceworkers/test_notification_get.html new file mode 100644 index 000000000..dbb312e7b --- /dev/null +++ b/dom/workers/test/serviceworkers/test_notification_get.html @@ -0,0 +1,213 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>ServiceWorkerRegistration.getNotifications() on main thread and worker thread.</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript"> + + SimpleTest.requestFlakyTimeout("untriaged"); + + function testFrame(src) { + return new Promise(function(resolve, reject) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.callback = function(result) { + iframe.src = "about:blank"; + document.body.removeChild(iframe); + iframe = null; + SpecialPowers.exactGC(function() { + resolve(result); + }); + }; + document.body.appendChild(iframe); + }); + } + + function registerSW() { + return testFrame('notification/register.html').then(function() { + ok(true, "Registered service worker."); + }); + } + + function unregisterSW() { + return testFrame('notification/unregister.html').then(function() { + ok(true, "Unregistered service worker."); + }); + } + + // To check that the scope is respected when retrieving notifications. + function registerAlternateSWAndAddNotification() { + return testFrame('notification_alt/register.html').then(function() { + ok(true, "Registered alternate service worker."); + return navigator.serviceWorker.getRegistration("./notification_alt/").then(function(reg) { + return reg.showNotification("This is a notification_alt"); + }); + }); + } + + function unregisterAlternateSWAndAddNotification() { + return testFrame('notification_alt/unregister.html').then(function() { + ok(true, "unregistered alternate service worker."); + }); + } + + function testDismiss() { + // Dismissed persistent notifications should be removed from the + // notification list. + var alertsService = SpecialPowers.Cc["@mozilla.org/alerts-service;1"] + .getService(SpecialPowers.Ci.nsIAlertsService); + return navigator.serviceWorker.getRegistration("./notification/") + .then(function(reg) { + return reg.showNotification( + "This is a notification that will be closed", { tag: "dismiss" }) + .then(function() { + return reg; + }); + }).then(function(reg) { + return reg.getNotifications() + .then(function(notifications) { + is(notifications.length, 1, "There should be one visible notification"); + is(notifications[0].tag, "dismiss", "Tag should match"); + + // Simulate dismissing the notification by using the alerts service + // directly, instead of `Notification#close`. + var principal = SpecialPowers.wrap(document).nodePrincipal; + var id = principal.origin + "#tag:dismiss"; + alertsService.closeAlert(id, principal); + + return reg; + }); + }).then(function(reg) { + return reg.getNotifications(); + }).then(function(notifications) { + // Make sure dismissed notifications are no longer retrieved. + is(notifications.length, 0, "There should be no more stored notifications"); + }); + } + + function testGet() { + // Non persistent notifications will not show up in getNotification(). + var n = new Notification("Scope does not match"); + var options = NotificationTest.payload; + return navigator.serviceWorker.getRegistration("./notification/") + .then(function(reg) { + return reg.showNotification("This is a title", options) + .then(function() { + return reg; + }); + }).then(function(reg) { + return registerAlternateSWAndAddNotification().then(function() { + return reg; + }); + }).then(function(reg) { + return reg.getNotifications(); + }).then(function(notifications) { + is(notifications.length, 1, "There should be one stored notification"); + var notification = notifications[0]; + ok(notification instanceof Notification, "Should be a Notification"); + is(notification.title, "This is a title", "Title should match"); + for (var key in options) { + if (key === "data") { + ok(NotificationTest.customDataMatches(notification.data), + "data property should match"); + continue; + } + is(notification[key], options[key], key + " property should match"); + } + notification.close(); + }).then(function() { + return navigator.serviceWorker.getRegistration("./notification/").then(function(reg) { + return reg.getNotifications(); + }); + }).then(function(notifications) { + // Make sure closed notifications are no longer retrieved. + is(notifications.length, 0, "There should be no more stored notifications"); + }).catch(function(e) { + ok(false, "Something went wrong " + e.message); + }).then(unregisterAlternateSWAndAddNotification); + } + + function testGetWorker() { + todo(false, "navigator.serviceWorker is not available on workers yet"); + return Promise.resolve(); + } + + function waitForSWTests(reg, msg) { + return new Promise(function(resolve, reject) { + var content = document.getElementById("content"); + + iframe = document.createElement("iframe"); + + content.appendChild(iframe); + iframe.setAttribute('src', "notification/listener.html"); + + window.onmessage = function(e) { + if (e.data.type == 'status') { + ok(e.data.status, "Service worker test: " + e.data.msg); + } else if (e.data.type == 'finish') { + content.removeChild(iframe); + resolve(); + } + } + + iframe.onload = function(e) { + iframe.onload = null; + reg.active.postMessage(msg); + } + }); + } + + function testGetServiceWorker() { + return navigator.serviceWorker.getRegistration("./notification/") + .then(function(reg) { + return waitForSWTests(reg, 'create'); + }); + } + + // Create a Notification here, make sure ServiceWorker sees it. + function testAcrossThreads() { + return navigator.serviceWorker.getRegistration("./notification/") + .then(function(reg) { + return reg.showNotification("This is a title") + .then(function() { + return reg; + }); + }).then(function(reg) { + return waitForSWTests(reg, 'do-not-create'); + }); + } + + SimpleTest.waitForExplicitFinish(); + + MockServices.register(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ]}, function() { + registerSW() + .then(testGet) + .then(testGetWorker) + .then(testGetServiceWorker) + .then(testAcrossThreads) + .then(testDismiss) + .then(unregisterSW) + .then(function() { + MockServices.unregister(); + SimpleTest.finish(); + }); + }); +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_notificationclick-otherwindow.html b/dom/workers/test/serviceworkers/test_notificationclick-otherwindow.html new file mode 100644 index 000000000..4a785be9a --- /dev/null +++ b/dom/workers/test/serviceworkers/test_notificationclick-otherwindow.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=916893 +--> +<head> + <title>Bug 1114554 - Test ServiceWorkerGlobalScope.notificationclick event.</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1114554">Bug 1114554</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events."); + + function testFrame(src) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.callback = function(result) { + window.callback = null; + document.body.removeChild(iframe); + iframe = null; + ok(result, "Got notificationclick event with correct data."); + MockServices.unregister(); + registration.unregister().then(function() { + SimpleTest.finish(); + }); + }; + document.body.appendChild(iframe); + } + + var registration; + + function runTest() { + MockServices.register(); + testFrame('notificationclick-otherwindow.html'); + navigator.serviceWorker.register("notificationclick.js", { scope: "notificationclick-otherwindow.html" }).then(function(reg) { + registration = reg; + }, function(e) { + ok(false, "registration should have passed!"); + }); + }; + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ]}, runTest); +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_notificationclick.html b/dom/workers/test/serviceworkers/test_notificationclick.html new file mode 100644 index 000000000..d5c3ecf8b --- /dev/null +++ b/dom/workers/test/serviceworkers/test_notificationclick.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=916893 +--> +<head> + <title>Bug 1114554 - Test ServiceWorkerGlobalScope.notificationclick event.</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1114554">Bug 1114554</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events."); + + function testFrame(src) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.callback = function(result) { + window.callback = null; + document.body.removeChild(iframe); + iframe = null; + ok(result, "Got notificationclick event with correct data."); + MockServices.unregister(); + registration.unregister().then(function() { + SimpleTest.finish(); + }); + }; + document.body.appendChild(iframe); + } + + var registration; + + function runTest() { + MockServices.register(); + testFrame('notificationclick.html'); + navigator.serviceWorker.register("notificationclick.js", { scope: "notificationclick.html" }).then(function(reg) { + registration = reg; + }, function(e) { + ok(false, "registration should have passed!"); + }); + }; + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ]}, runTest); +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_notificationclick_focus.html b/dom/workers/test/serviceworkers/test_notificationclick_focus.html new file mode 100644 index 000000000..81d6e269c --- /dev/null +++ b/dom/workers/test/serviceworkers/test_notificationclick_focus.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=916893 +--> +<head> + <title>Bug 1144660 - Test client.focus() permissions on notification click</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1114554">Bug 1114554</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events."); + + function testFrame(src) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.callback = function(result) { + window.callback = null; + document.body.removeChild(iframe); + iframe = null; + ok(result, "All tests passed."); + MockServices.unregister(); + registration.unregister().then(function() { + SimpleTest.finish(); + }); + }; + document.body.appendChild(iframe); + } + + var registration; + + function runTest() { + MockServices.register(); + testFrame('notificationclick_focus.html'); + navigator.serviceWorker.register("notificationclick_focus.js", { scope: "notificationclick_focus.html" }).then(function(reg) { + registration = reg; + }, function(e) { + ok(false, "registration should have passed!"); + }); + }; + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ["dom.disable_open_click_delay", 1000], + ]}, runTest); +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_notificationclose.html b/dom/workers/test/serviceworkers/test_notificationclose.html new file mode 100644 index 000000000..3b81132c4 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_notificationclose.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1265841 +--> +<head> + <title>Bug 1265841 - Test ServiceWorkerGlobalScope.notificationclose event.</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1265841">Bug 1265841</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show, click, and close events."); + + function testFrame(src) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.callback = function(result) { + window.callback = null; + document.body.removeChild(iframe); + iframe = null; + ok(result, "Got notificationclose event with correct data."); + MockServices.unregister(); + registration.unregister().then(function() { + SimpleTest.finish(); + }); + }; + document.body.appendChild(iframe); + } + + var registration; + + function runTest() { + MockServices.register(); + testFrame('notificationclose.html'); + navigator.serviceWorker.register("notificationclose.js", { scope: "notificationclose.html" }).then(function(reg) { + registration = reg; + }, function(e) { + ok(false, "registration should have passed!"); + }); + }; + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ]}, runTest); +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_opaque_intercept.html b/dom/workers/test/serviceworkers/test_opaque_intercept.html new file mode 100644 index 000000000..5cb12e518 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_opaque_intercept.html @@ -0,0 +1,85 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test service worker post message </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + function start() { + return navigator.serviceWorker.register("opaque_intercept_worker.js", + { scope: "./sw_clients/" }) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + + function testOpaqueIntercept(swr) { + var p = new Promise(function(res, rej) { + var ready = false; + var scriptLoaded = false; + window.onmessage = function(e) { + if (e.data === "READY") { + ok(!ready, "ready message should only be received once"); + ok(!scriptLoaded, "ready message should be received before script loaded"); + if (ready) { + res(); + return; + } + ready = true; + iframe.contentWindow.postMessage("REFRESH", "*"); + } else if (e.data === "SCRIPT_LOADED") { + ok(ready, "script loaded should be received after ready"); + ok(!scriptLoaded, "script loaded message should be received only once"); + scriptLoaded = true; + res(); + } + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/refresher.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + start() + .then(testOpaqueIntercept) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_openWindow.html b/dom/workers/test/serviceworkers/test_openWindow.html new file mode 100644 index 000000000..2417648b9 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_openWindow.html @@ -0,0 +1,117 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1172870 +--> +<head> + <title>Bug 1172870 - Test clients.openWindow</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1172870">Bug 1172870</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events."); + + function setup(ctx) { + MockServices.register(); + + return navigator.serviceWorker.register("openWindow_worker.js", {scope: "./"}) + .then(function(swr) { + ok(swr, "Registration successful"); + ctx.registration = swr; + return ctx; + }); + } + + function waitForActiveServiceWorker(ctx) { + return navigator.serviceWorker.ready.then(function(result) { + ok(ctx.registration.active, "Service Worker is active"); + return ctx; + }); + } + + function setupMessageHandler(ctx) { + return new Promise(function(res, rej) { + navigator.serviceWorker.onmessage = function(event) { + navigator.serviceWorker.onmessage = null; + for (i = 0; i < event.data.length; i++) { + ok(event.data[i].result, event.data[i].message); + } + res(ctx); + } + }); + } + + function testPopupNotAllowed(ctx) { + var p = setupMessageHandler(ctx); + ok(ctx.registration.active, "Worker is active."); + ctx.registration.active.postMessage("testNoPopup"); + + return p; + } + + function testPopupAllowed(ctx) { + var p = setupMessageHandler(ctx); + ctx.registration.showNotification("testPopup"); + + return p; + } + + function checkNumberOfWindows(ctx) { + return new Promise(function(res, rej) { + navigator.serviceWorker.onmessage = function(event) { + navigator.serviceWorker.onmessage = null; + ok(event.data.result, event.data.message); + res(ctx); + } + ctx.registration.active.postMessage("CHECK_NUMBER_OF_WINDOWS"); + }); + } + + function clear(ctx) { + MockServices.unregister(); + + return ctx.registration.unregister().then(function(result) { + ctx.registration = null; + ok(result, "Unregister was successful."); + }); + } + + function runTest() { + setup({}) + .then(waitForActiveServiceWorker) + // Permission to allow popups persists for some time after a notification + // click event, so the order here is important. + .then(testPopupNotAllowed) + .then(testPopupAllowed) + .then(checkNumberOfWindows) + .then(clear) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.openWindow.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ["dom.disable_open_click_delay", 1000], + ["dom.serviceWorkers.idle_timeout", 299999], + ["dom.serviceWorkers.idle_extended_timeout", 299999] + ]}, runTest); +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_origin_after_redirect.html b/dom/workers/test/serviceworkers/test_origin_after_redirect.html new file mode 100644 index 000000000..b68537d9d --- /dev/null +++ b/dom/workers/test/serviceworkers/test_origin_after_redirect.html @@ -0,0 +1,58 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/origin/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("/tests/dom/workers/test/serviceworkers/fetch/origin/index.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "http://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/origin/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.openWindow.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_origin_after_redirect_cached.html b/dom/workers/test/serviceworkers/test_origin_after_redirect_cached.html new file mode 100644 index 000000000..69644abfa --- /dev/null +++ b/dom/workers/test/serviceworkers/test_origin_after_redirect_cached.html @@ -0,0 +1,58 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/origin/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("/tests/dom/workers/test/serviceworkers/fetch/origin/index-cached.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "http://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/origin/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.openWindow.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_origin_after_redirect_to_https.html b/dom/workers/test/serviceworkers/test_origin_after_redirect_to_https.html new file mode 100644 index 000000000..dcac11aea --- /dev/null +++ b/dom/workers/test/serviceworkers/test_origin_after_redirect_to_https.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/origin/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("/tests/dom/workers/test/serviceworkers/fetch/origin/index-to-https.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "https://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/origin/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_origin_after_redirect_to_https_cached.html b/dom/workers/test/serviceworkers/test_origin_after_redirect_to_https_cached.html new file mode 100644 index 000000000..3922fdb90 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_origin_after_redirect_to_https_cached.html @@ -0,0 +1,58 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/origin/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("/tests/dom/workers/test/serviceworkers/fetch/origin/index-to-https-cached.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "https://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/origin/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.openWindow.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_post_message.html b/dom/workers/test/serviceworkers/test_post_message.html new file mode 100644 index 000000000..e366423cb --- /dev/null +++ b/dom/workers/test/serviceworkers/test_post_message.html @@ -0,0 +1,80 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test service worker post message </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var magic_value = "MAGIC_VALUE_123"; + var registration; + function start() { + return navigator.serviceWorker.register("message_posting_worker.js", + { scope: "./sw_clients/" }) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + + function testPostMessage(swr) { + var p = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (e.data === "READY") { + swr.active.postMessage(magic_value); + } else if (e.data === magic_value) { + ok(true, "Worker posted the correct value."); + res(); + } else { + ok(false, "Wrong value. Expected: " + magic_value + + ", got: " + e.data); + res(); + } + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/service_worker_controlled.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + start() + .then(testPostMessage) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.openWindow.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_post_message_advanced.html b/dom/workers/test/serviceworkers/test_post_message_advanced.html new file mode 100644 index 000000000..8ea0d2300 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_post_message_advanced.html @@ -0,0 +1,109 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test service worker post message advanced </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + var base = ["string", true, 42]; + var blob = new Blob(["blob_content"]); + var file = new File(["file_content"], "file"); + var obj = { body : "object_content" }; + + function readBlob(blob) { + return new Promise(function(resolve, reject) { + var reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsText(blob); + }); + } + + function equals(v1, v2) { + return Promise.all([v1, v2]).then(function(val) { + ok(val[0] === val[1], "Values should match."); + }); + } + + function blob_equals(b1, b2) { + return equals(readBlob(b1), readBlob(b2)); + } + + function file_equals(f1, f2) { + return equals(f1.name, f2.name).then(blob_equals(f1, f2)); + } + + function obj_equals(o1, o2) { + return equals(o1.body, o2.body); + } + + function start() { + return navigator.serviceWorker.register("message_posting_worker.js", + { scope: "./sw_clients/" }) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function testPostMessageObject(obj, test) { + var p = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (e.data === "READY") { + registration.active.postMessage(obj) + } else { + test(obj, e.data).then(res); + } + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/service_worker_controlled.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + start() + .then(testPostMessageObject.bind(this, base[0], equals)) + .then(testPostMessageObject.bind(this, base[1], equals)) + .then(testPostMessageObject.bind(this, base[2], equals)) + .then(testPostMessageObject.bind(this, blob, blob_equals)) + .then(testPostMessageObject.bind(this, file, file_equals)) + .then(testPostMessageObject.bind(this, obj, obj_equals)) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.openWindow.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_post_message_source.html b/dom/workers/test/serviceworkers/test_post_message_source.html new file mode 100644 index 000000000..543f64b4a --- /dev/null +++ b/dom/workers/test/serviceworkers/test_post_message_source.html @@ -0,0 +1,68 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1142015 - Test service worker post message source </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var magic_value = "MAGIC_VALUE_RANDOM"; + var registration; + function start() { + return navigator.serviceWorker.register("source_message_posting_worker.js", + { scope: "./nonexistent_scope/" }) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + + function testPostMessage(swr) { + var p = new Promise(function(res, rej) { + navigator.serviceWorker.onmessage = function(e) { + ok(e.data === magic_value, "Worker posted the correct value."); + res(); + } + }); + + ok(swr.installing, "Installing worker exists."); + swr.installing.postMessage(magic_value); + return p; + } + + + function runTest() { + start() + .then(testPostMessage) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.openWindow.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_privateBrowsing.html b/dom/workers/test/serviceworkers/test_privateBrowsing.html new file mode 100644 index 000000000..976337711 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_privateBrowsing.html @@ -0,0 +1,113 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for ServiceWorker - Private Browsing</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +</head> +<body> + +<script type="application/javascript"> + +const Ci = Components.interfaces; +var mainWindow; + +var contentPage = "http://mochi.test:8888/chrome/dom/workers/test/empty.html"; +var workerScope = "http://mochi.test:8888/chrome/dom/workers/test/serviceworkers/"; +var workerURL = workerScope + "worker.js"; + +function testOnWindow(aIsPrivate, aCallback) { + var win = mainWindow.OpenBrowserWindow({private: aIsPrivate}); + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad, false); + win.addEventListener("DOMContentLoaded", function onInnerLoad() { + if (win.content.location.href != contentPage) { + win.gBrowser.loadURI(contentPage); + return; + } + + win.removeEventListener("DOMContentLoaded", onInnerLoad, true); + SimpleTest.executeSoon(function() { aCallback(win); }); + }, true); + + if (!aIsPrivate) { + win.gBrowser.loadURI(contentPage); + } + }, true); +} + +function setupWindow() { + mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + runTest(); +} + +var wN; +var registration; +var wP; + +function testPrivateWindow() { + testOnWindow(true, function(aWin) { + wP = aWin; + ok(!("serviceWorker" in wP.content.navigator), "ServiceWorkers are not available for private windows"); + runTest(); + }); +} + +function doTests() { + testOnWindow(false, function(aWin) { + wN = aWin; + ok("serviceWorker" in wN.content.navigator, "ServiceWorkers are available for normal windows"); + + wN.content.navigator.serviceWorker.register(workerURL, + { scope: workerScope }) + .then(function(aRegistration) { + registration = aRegistration; + ok(registration, "Registering a service worker in a normal window should succeed"); + + // Bug 1255621: We should be able to load a controlled document in a private window. + testPrivateWindow(); + }, function(aError) { + ok(false, "Error registering worker in normal window: " + aError); + testPrivateWindow(); + }); + }); +} + +var steps = [ + setupWindow, + doTests +]; + +function cleanup() { + wN.close(); + wP.close(); + + SimpleTest.finish(); +} + +function runTest() { + if (!steps.length) { + registration.unregister().then(cleanup, cleanup); + + return; + } + + var step = steps.shift(); + step(); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["browser.startup.page", 0], + ["browser.startup.homepage_override.mstone", "ignore"], +]}, runTest); + +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_register_base.html b/dom/workers/test/serviceworkers/test_register_base.html new file mode 100644 index 000000000..58b08d27a --- /dev/null +++ b/dom/workers/test/serviceworkers/test_register_base.html @@ -0,0 +1,41 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that registering a service worker uses the docuemnt URI for the secure origin check</title> + <script type="text/javascript" src="http://mochi.test:8888/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="http://mochi.test:8888/tests/SimpleTest/test.css" /> + <base href="https://mozilla.org/"> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + navigator.serviceWorker.register("http://mochi.test:8888/tests/dom/workers/test/serviceworkers/empty.js") + .then(reg => { + ok(false, "Register should fail"); + SimpleTest.finish(); + }, err => { + is(err.name, "SecurityError", "Registration should fail with SecurityError"); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.openWindow.enabled", true], + ]}, runTest); + }; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_register_https_in_http.html b/dom/workers/test/serviceworkers/test_register_https_in_http.html new file mode 100644 index 000000000..2c8e998f7 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_register_https_in_http.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1172948 - Test that registering a service worker from inside an HTTPS iframe embedded in an HTTP iframe doesn't work</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + var iframe = document.createElement("iframe"); + iframe.src = "https://example.com/tests/dom/workers/test/serviceworkers/register_https.html"; + document.body.appendChild(iframe); + + window.onmessage = event => { + switch (event.data.type) { + case "ok": + ok(event.data.status, event.data.msg); + break; + case "done": + SimpleTest.finish(); + break; + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.openWindow.enabled", true], + ]}, runTest); + }; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context.js b/dom/workers/test/serviceworkers/test_request_context.js new file mode 100644 index 000000000..aebba79a7 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context.js @@ -0,0 +1,75 @@ +// Copied from /dom/plugins/test/mochitest/utils.js +function getTestPlugin(pluginName) { + var ph = SpecialPowers.Cc["@mozilla.org/plugin/host;1"] + .getService(SpecialPowers.Ci.nsIPluginHost); + var tags = ph.getPluginTags(); + var name = pluginName || "Test Plug-in"; + for (var tag of tags) { + if (tag.name == name) { + return tag; + } + } + + ok(false, "Could not find plugin tag with plugin name '" + name + "'"); + return null; +} +function setTestPluginEnabledState(newEnabledState, pluginName) { + var oldEnabledState = SpecialPowers.setTestPluginEnabledState(newEnabledState, pluginName); + if (!oldEnabledState) { + return; + } + var plugin = getTestPlugin(pluginName); + while (plugin.enabledState != newEnabledState) { + // Run a nested event loop to wait for the preference change to + // propagate to the child. Yuck! + SpecialPowers.Services.tm.currentThread.processNextEvent(true); + } + SimpleTest.registerCleanupFunction(function() { + SpecialPowers.setTestPluginEnabledState(oldEnabledState, pluginName); + }); +} +setTestPluginEnabledState(SpecialPowers.Ci.nsIPluginTag.STATE_ENABLED); + +function isMulet() { + try { + return SpecialPowers.getBoolPref("b2g.is_mulet"); + } catch(e) { + return false; + } +} + +var iframe; +function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/context/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "todo") { + todo(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/context/index.html?" + gTest; + } else if (e.data.status == "done") { + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/context/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; +} + +SimpleTest.waitForExplicitFinish(); +onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["beacon.enabled", true], + ["browser.send_pings", true], + ["browser.send_pings.max_per_link", -1], + ["dom.caches.enabled", true], + ["dom.requestcontext.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.openWindow.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +}; diff --git a/dom/workers/test/serviceworkers/test_request_context_audio.html b/dom/workers/test/serviceworkers/test_request_context_audio.html new file mode 100644 index 000000000..929a24428 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_audio.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testAudio"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_beacon.html b/dom/workers/test/serviceworkers/test_request_context_beacon.html new file mode 100644 index 000000000..ce44214d6 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_beacon.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testBeacon"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_cache.html b/dom/workers/test/serviceworkers/test_request_context_cache.html new file mode 100644 index 000000000..3d62baabc --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_cache.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testCache"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_cspreport.html b/dom/workers/test/serviceworkers/test_request_context_cspreport.html new file mode 100644 index 000000000..a27e79303 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_cspreport.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testCSPReport"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_embed.html b/dom/workers/test/serviceworkers/test_request_context_embed.html new file mode 100644 index 000000000..f8d374246 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_embed.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testEmbed"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_fetch.html b/dom/workers/test/serviceworkers/test_request_context_fetch.html new file mode 100644 index 000000000..94de8be31 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_fetch.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testFetch"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_font.html b/dom/workers/test/serviceworkers/test_request_context_font.html new file mode 100644 index 000000000..d81a0686b --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_font.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testFont"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_frame.html b/dom/workers/test/serviceworkers/test_request_context_frame.html new file mode 100644 index 000000000..d5dc1f745 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_frame.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testFrame"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_iframe.html b/dom/workers/test/serviceworkers/test_request_context_iframe.html new file mode 100644 index 000000000..d3b0675e0 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_iframe.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testIFrame"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_image.html b/dom/workers/test/serviceworkers/test_request_context_image.html new file mode 100644 index 000000000..810063da4 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_image.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testImage"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_imagesrcset.html b/dom/workers/test/serviceworkers/test_request_context_imagesrcset.html new file mode 100644 index 000000000..95b2b7214 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_imagesrcset.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testImageSrcSet"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_internal.html b/dom/workers/test/serviceworkers/test_request_context_internal.html new file mode 100644 index 000000000..45f454495 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_internal.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testInternal"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_nestedworker.html b/dom/workers/test/serviceworkers/test_request_context_nestedworker.html new file mode 100644 index 000000000..226de691b --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_nestedworker.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testNestedWorker"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_nestedworkerinsharedworker.html b/dom/workers/test/serviceworkers/test_request_context_nestedworkerinsharedworker.html new file mode 100644 index 000000000..48934a57c --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_nestedworkerinsharedworker.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testNestedWorkerInSharedWorker"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_object.html b/dom/workers/test/serviceworkers/test_request_context_object.html new file mode 100644 index 000000000..189c5adbb --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_object.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testObject"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_picture.html b/dom/workers/test/serviceworkers/test_request_context_picture.html new file mode 100644 index 000000000..1b49e7eb9 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_picture.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testPicture"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_ping.html b/dom/workers/test/serviceworkers/test_request_context_ping.html new file mode 100644 index 000000000..460e9efb4 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_ping.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testPing"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_plugin.html b/dom/workers/test/serviceworkers/test_request_context_plugin.html new file mode 100644 index 000000000..862e9d4a5 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_plugin.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testPlugin"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_script.html b/dom/workers/test/serviceworkers/test_request_context_script.html new file mode 100644 index 000000000..ec560ef72 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_script.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testScript"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_sharedworker.html b/dom/workers/test/serviceworkers/test_request_context_sharedworker.html new file mode 100644 index 000000000..93ccdf3ae --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_sharedworker.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testSharedWorker"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_style.html b/dom/workers/test/serviceworkers/test_request_context_style.html new file mode 100644 index 000000000..b557d5c04 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_style.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testStyle"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_track.html b/dom/workers/test/serviceworkers/test_request_context_track.html new file mode 100644 index 000000000..1b161c4d0 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_track.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testTrack"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_video.html b/dom/workers/test/serviceworkers/test_request_context_video.html new file mode 100644 index 000000000..1886e31d7 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_video.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testVideo"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_worker.html b/dom/workers/test/serviceworkers/test_request_context_worker.html new file mode 100644 index 000000000..9de127304 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_worker.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testWorker"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_xhr.html b/dom/workers/test/serviceworkers/test_request_context_xhr.html new file mode 100644 index 000000000..ed0d60bf8 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_xhr.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testXHR"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_request_context_xslt.html b/dom/workers/test/serviceworkers/test_request_context_xslt.html new file mode 100644 index 000000000..a6d837b69 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_request_context_xslt.html @@ -0,0 +1,19 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1121157 - Test that Request objects passed to FetchEvent have the correct context</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="test_request_context.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe></iframe> +<script type="text/javascript"> +var gTest = "testXSLT"; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_sandbox_intercept.html b/dom/workers/test/serviceworkers/test_sandbox_intercept.html new file mode 100644 index 000000000..273df53ff --- /dev/null +++ b/dom/workers/test/serviceworkers/test_sandbox_intercept.html @@ -0,0 +1,50 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1142727 - Test that sandboxed iframes are not intercepted</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<iframe sandbox="allow-scripts allow-same-origin"></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/sandbox/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/sandbox/index.html"; + } else if (e.data.status == "done") { + iframe.src = "/tests/dom/workers/test/serviceworkers/fetch/sandbox/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_sanitize.html b/dom/workers/test/serviceworkers/test_sanitize.html new file mode 100644 index 000000000..842cb38c3 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_sanitize.html @@ -0,0 +1,87 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1080109 - Clear ServiceWorker registrations for all domains</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function start() { + const Cc = SpecialPowers.Cc; + const Ci = SpecialPowers.Ci; + + function testNotIntercepted() { + testFrame("sanitize/frame.html").then(function(body) { + is(body, "FAIL", "Expected frame to not be controlled"); + // No need to unregister since that already happened. + navigator.serviceWorker.getRegistration("sanitize/foo").then(function(reg) { + ok(reg === undefined, "There should no longer be a valid registration"); + }, function(e) { + ok(false, "getRegistration() should not error"); + }).then(function(e) { + SimpleTest.finish(); + }); + }); + } + + registerSW().then(function() { + return testFrame("sanitize/frame.html").then(function(body) { + is(body, "intercepted", "Expected serviceworker to intercept request"); + }); + }).then(function() { + return navigator.serviceWorker.getRegistration("sanitize/foo"); + }).then(function(reg) { + reg.active.onstatechange = function(e) { + e.target.onstatechange = null; + ok(e.target.state, "redundant", "On clearing data, serviceworker should become redundant"); + testNotIntercepted(); + }; + }).then(function() { + SpecialPowers.removeAllServiceWorkerData(); + }); + } + + function testFrame(src) { + return new Promise(function(resolve, reject) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.onmessage = function(message) { + window.onmessage = null; + iframe.src = "about:blank"; + document.body.removeChild(iframe); + iframe = null; + SpecialPowers.exactGC(function() { + resolve(message.data); + }); + }; + document.body.appendChild(iframe); + }); + } + + function registerSW() { + return testFrame("sanitize/register.html"); + } + + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, function() { + start(); + }); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_sanitize_domain.html b/dom/workers/test/serviceworkers/test_sanitize_domain.html new file mode 100644 index 000000000..054b60f37 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_sanitize_domain.html @@ -0,0 +1,90 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1080109 - Clear ServiceWorker registrations for specific domains</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function start() { + const Cc = SpecialPowers.Cc; + const Ci = SpecialPowers.Ci; + + function checkDomainRegistration(domain, exists) { + return testFrame("http://" + domain + "/tests/dom/workers/test/serviceworkers/sanitize/example_check_and_unregister.html").then(function(body) { + if (body === "FAIL") { + ok(false, "Error acquiring registration or unregistering for " + domain); + } else { + if (exists) { + ok(body === true, "Expected " + domain + " to still have a registration."); + } else { + ok(body === false, "Expected " + domain + " to have no registration."); + } + } + }); + } + + registerSW().then(function() { + return testFrame("http://example.com/tests/dom/workers/test/serviceworkers/sanitize/frame.html").then(function(body) { + is(body, "intercepted", "Expected serviceworker to intercept request"); + }); + }).then(function() { + SpecialPowers.removeServiceWorkerDataForExampleDomain(); + }).then(function() { + return checkDomainRegistration("prefixexample.com", true /* exists */) + .then(function(e) { + return checkDomainRegistration("example.com", false /* exists */); + }).then(function(e) { + SimpleTest.finish(); + }); + }) + } + + function testFrame(src) { + return new Promise(function(resolve, reject) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.onmessage = function(message) { + window.onmessage = null; + iframe.src = "about:blank"; + document.body.removeChild(iframe); + iframe = null; + SpecialPowers.exactGC(function() { + resolve(message.data); + }); + }; + document.body.appendChild(iframe); + }); + } + + function registerSW() { + return testFrame("http://example.com/tests/dom/workers/test/serviceworkers/sanitize/register.html") + .then(function(e) { + // Register for prefixexample.com and then ensure it does not get unregistered. + return testFrame("http://prefixexample.com/tests/dom/workers/test/serviceworkers/sanitize/register.html"); + }); + } + + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, function() { + start(); + }); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_scopes.html b/dom/workers/test/serviceworkers/test_scopes.html new file mode 100644 index 000000000..2d8116f83 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_scopes.html @@ -0,0 +1,121 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 984048 - Test scope glob matching.</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var scriptsAndScopes = [ + [ "worker.js", "./sub/dir/"], + [ "worker.js", "./sub/dir" ], + [ "worker.js", "./sub/dir.html" ], + [ "worker.js", "./sub/dir/a" ], + [ "worker.js", "./sub" ], + [ "worker.js", "./star*" ], // '*' has no special meaning + ]; + + function registerWorkers() { + var registerArray = []; + scriptsAndScopes.forEach(function(item) { + registerArray.push(navigator.serviceWorker.register(item[0], { scope: item[1] })); + }); + + // Check register()'s step 4 which uses script's url with "./" as the scope if no scope is passed. + // The other tests already check step 5. + registerArray.push(navigator.serviceWorker.register("scope/scope_worker.js")); + + // Check that SW cannot be registered for a scope "above" the script's location. + registerArray.push(new Promise(function(resolve, reject) { + navigator.serviceWorker.register("scope/scope_worker.js", { scope: "./" }) + .then(function() { + ok(false, "registration scope has to be inside service worker script scope."); + reject(); + }, function() { + ok(true, "registration scope has to be inside service worker script scope."); + resolve(); + }); + })); + return Promise.all(registerArray); + } + + function unregisterWorkers() { + var unregisterArray = []; + scriptsAndScopes.forEach(function(item) { + var p = navigator.serviceWorker.getRegistration(item[1]); + unregisterArray.push(p.then(function(reg) { + return reg.unregister(); + })); + }); + + unregisterArray.push(navigator.serviceWorker.getRegistration("scope/").then(function (reg) { + return reg.unregister(); + })); + + return Promise.all(unregisterArray); + } + + function testScopes() { + return new Promise(function(resolve, reject) { + var getScope = navigator.serviceWorker.getScopeForUrl.bind(navigator.serviceWorker); + var base = new URL(".", document.baseURI); + + function p(s) { + return base + s; + } + + function fail(fn) { + try { + getScope(p("index.html")); + ok(false, "No registration"); + } catch(e) { + ok(true, "No registration"); + } + } + + ok(getScope(p("sub.html")) === p("sub"), "Scope should match"); + ok(getScope(p("sub/dir.html")) === p("sub/dir.html"), "Scope should match"); + ok(getScope(p("sub/dir")) === p("sub/dir"), "Scope should match"); + ok(getScope(p("sub/dir/foo")) === p("sub/dir/"), "Scope should match"); + ok(getScope(p("sub/dir/afoo")) === p("sub/dir/a"), "Scope should match"); + ok(getScope(p("star*wars")) === p("star*"), "Scope should match"); + ok(getScope(p("scope/some_file.html")) === p("scope/"), "Scope should match"); + fail("index.html"); + fail("sua.html"); + fail("star/a.html"); + resolve(); + }); + } + + function runTest() { + registerWorkers() + .then(testScopes) + .then(unregisterWorkers) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_service_worker_allowed.html b/dom/workers/test/serviceworkers/test_service_worker_allowed.html new file mode 100644 index 000000000..eca94ebb4 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_service_worker_allowed.html @@ -0,0 +1,74 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the Service-Worker-Allowed header</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="content"></div> +<script class="testbody" type="text/javascript"> + var gTests = [ + "worker_scope_different.js", + "worker_scope_different2.js", + "worker_scope_too_deep.js", + ]; + + function testPermissiveHeader() { + // Make sure that this registration succeeds, as the prefix check should pass. + return navigator.serviceWorker.register("swa/worker_scope_too_narrow.js", {scope: "swa/"}) + .then(swr => { + ok(true, "Registration should finish successfully"); + return swr.unregister(); + }, err => { + ok(false, "Unexpected error when registering the service worker: " + err); + }); + } + + function testPreciseHeader() { + // Make sure that this registration succeeds, as the prefix check should pass + // given that we parse the use the full pathname from this URL.. + return navigator.serviceWorker.register("swa/worker_scope_precise.js", {scope: "swa/"}) + .then(swr => { + ok(true, "Registration should finish successfully"); + return swr.unregister(); + }, err => { + ok(false, "Unexpected error when registering the service worker: " + err); + }); + } + + function runTest() { + Promise.all(gTests.map(testName => { + return new Promise((resolve, reject) => { + // Make sure that registration fails. + navigator.serviceWorker.register("swa/" + testName, {scope: "swa/"}) + .then(reject, resolve); + }); + })).then(values => { + values.forEach(error => { + is(error.name, "SecurityError", "Registration should fail"); + }); + Promise.all([ + testPermissiveHeader(), + testPreciseHeader(), + ]).then(SimpleTest.finish, SimpleTest.finish); + }, (x) => { + ok(false, "Registration should not succeed, but it did"); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_serviceworker_header.html b/dom/workers/test/serviceworkers/test_serviceworker_header.html new file mode 100644 index 000000000..ac5a6e49f --- /dev/null +++ b/dom/workers/test/serviceworkers/test_serviceworker_header.html @@ -0,0 +1,41 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that service worker scripts are fetched with a Service-Worker: script header</title> + <script type="text/javascript" src="http://mochi.test:8888/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="http://mochi.test:8888/tests/SimpleTest/test.css" /> + <base href="https://mozilla.org/"> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + navigator.serviceWorker.register("http://mochi.test:8888/tests/dom/workers/test/serviceworkers/header_checker.sjs") + .then(reg => { + ok(true, "Register should succeed"); + reg.unregister().then(() => SimpleTest.finish()); + }, err => { + ok(false, "Register should not fail"); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.enabled", true], + ]}, runTest); + }; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_serviceworker_interfaces.html b/dom/workers/test/serviceworkers/test_serviceworker_interfaces.html new file mode 100644 index 000000000..0130ca2d9 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_serviceworker_interfaces.html @@ -0,0 +1,106 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Validate Interfaces Exposed to Service Workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="../worker_driver.js"></script> +</head> +<body> +<script class="testbody" type="text/javascript"> + + function setupSW(registration) { + var worker = registration.waiting || + registration.active; + window.onmessage = function(event) { + if (event.data.type == 'finish') { + registration.unregister().then(function(success) { + ok(success, "The service worker should be unregistered successfully"); + + SimpleTest.finish(); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + SimpleTest.finish(); + }); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + + } else if (event.data.type == 'getPrefs') { + var result = {}; + event.data.prefs.forEach(function(pref) { + result[pref] = SpecialPowers.Services.prefs.getBoolPref(pref); + }); + worker.postMessage({ + type: 'returnPrefs', + prefs: event.data.prefs, + result: result + }); + + } else if (event.data.type == 'getVersion') { + var result = SpecialPowers.Cc['@mozilla.org/xre/app-info;1'].getService(SpecialPowers.Ci.nsIXULAppInfo).version; + worker.postMessage({ + type: 'returnVersion', + result: result + }); + + } else if (event.data.type == 'getUserAgent') { + worker.postMessage({ + type: 'returnUserAgent', + result: navigator.userAgent + }); + } else if (event.data.type == 'getOSCPU') { + worker.postMessage({ + type: 'returnOSCPU', + result: navigator.oscpu + }); + } + } + + worker.onerror = function(event) { + ok(false, 'Worker had an error: ' + event.data); + SimpleTest.finish(); + }; + + var iframe = document.createElement("iframe"); + iframe.src = "message_receiver.html"; + iframe.onload = function() { + worker.postMessage({ script: "test_serviceworker_interfaces.js" }); + }; + document.body.appendChild(iframe); + } + + function runTest() { + navigator.serviceWorker.ready.then(setupSW); + navigator.serviceWorker.register("serviceworker_wrapper.js", {scope: "."}); + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + // The handling of "dom.caches.enabled" here is a bit complicated. What we + // want to happen is that Cache is always enabled in service workers. So + // if service workers are disabled by default we want to force on both + // service workers and "dom.caches.enabled". But if service workers are + // enabled by default, we do not want to mess with the "dom.caches.enabled" + // value, since that would defeat the purpose of the test. Use a subframe + // to decide whether service workers are enabled by default, so we don't + // force creation of our own Navigator object before our prefs are set. + var prefs = [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]; + + var subframe = document.createElement("iframe"); + document.body.appendChild(subframe); + if (!("serviceWorker" in subframe.contentWindow.navigator)) { + prefs.push(["dom.caches.enabled", true]); + } + subframe.remove(); + + SpecialPowers.pushPrefEnv({"set": prefs}, runTest); + }; +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js b/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js new file mode 100644 index 000000000..9dbfcc099 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js @@ -0,0 +1,278 @@ +// This is a list of all interfaces that are exposed to workers. +// Please only add things to this list with great care and proper review +// from the associated module peers. + +// This file lists global interfaces we want exposed and verifies they +// are what we intend. Each entry in the arrays below can either be a +// simple string with the interface name, or an object with a 'name' +// property giving the interface name as a string, and additional +// properties which qualify the exposure of that interface. For example: +// +// [ +// "AGlobalInterface", +// { name: "ExperimentalThing", release: false }, +// { name: "ReallyExperimentalThing", nightly: true }, +// { name: "DesktopOnlyThing", desktop: true }, +// { name: "FancyControl", xbl: true }, +// { name: "DisabledEverywhere", disabled: true }, +// ]; +// +// See createInterfaceMap() below for a complete list of properties. + +// IMPORTANT: Do not change this list without review from +// a JavaScript Engine peer! +var ecmaGlobals = + [ + "Array", + "ArrayBuffer", + "Boolean", + "DataView", + "Date", + "Error", + "EvalError", + "Float32Array", + "Float64Array", + "Function", + "Infinity", + "Int16Array", + "Int32Array", + "Int8Array", + "InternalError", + {name: "Intl", android: false}, + "Iterator", + "JSON", + "Map", + "Math", + "NaN", + "Number", + "Object", + "Promise", + "Proxy", + "RangeError", + "ReferenceError", + "Reflect", + "RegExp", + "Set", + {name: "SharedArrayBuffer", release: false}, + {name: "SIMD", nightly: true}, + {name: "Atomics", release: false}, + "StopIteration", + "String", + "Symbol", + "SyntaxError", + {name: "TypedObject", nightly: true}, + "TypeError", + "Uint16Array", + "Uint32Array", + "Uint8Array", + "Uint8ClampedArray", + "URIError", + "WeakMap", + "WeakSet", + ]; +// IMPORTANT: Do not change the list above without review from +// a JavaScript Engine peer! + +// IMPORTANT: Do not change the list below without review from a DOM peer! +var interfaceNamesInGlobalScope = + [ +// IMPORTANT: Do not change this list without review from a DOM peer! + "Blob", +// IMPORTANT: Do not change this list without review from a DOM peer! + "BroadcastChannel", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Cache", +// IMPORTANT: Do not change this list without review from a DOM peer! + "CacheStorage", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Client", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Clients", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Crypto", +// IMPORTANT: Do not change this list without review from a DOM peer! + "CustomEvent", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Directory", +// IMPORTANT: Do not change this list without review from a DOM peer! + "DOMCursor", +// IMPORTANT: Do not change this list without review from a DOM peer! + "DOMError", +// IMPORTANT: Do not change this list without review from a DOM peer! + "DOMException", +// IMPORTANT: Do not change this list without review from a DOM peer! + "DOMRequest", +// IMPORTANT: Do not change this list without review from a DOM peer! + "DOMStringList", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Event", +// IMPORTANT: Do not change this list without review from a DOM peer! + "EventTarget", +// IMPORTANT: Do not change this list without review from a DOM peer! + "ExtendableEvent", +// IMPORTANT: Do not change this list without review from a DOM peer! + "ExtendableMessageEvent", +// IMPORTANT: Do not change this list without review from a DOM peer! + "FetchEvent", +// IMPORTANT: Do not change this list without review from a DOM peer! + "File", +// IMPORTANT: Do not change this list without review from a DOM peer! + "FileReader", +// IMPORTANT: Do not change this list without review from a DOM peer! + "FileReaderSync", +// IMPORTANT: Do not change this list without review from a DOM peer! + "FormData", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Headers", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBCursor", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBCursorWithValue", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBDatabase", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBFactory", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBIndex", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBKeyRange", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBObjectStore", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBOpenDBRequest", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBRequest", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBTransaction", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBVersionChangeEvent", +// IMPORTANT: Do not change this list without review from a DOM peer! + "ImageBitmap", +// IMPORTANT: Do not change this list without review from a DOM peer! + "ImageBitmapRenderingContext", +// IMPORTANT: Do not change this list without review from a DOM peer! + "ImageData", +// IMPORTANT: Do not change this list without review from a DOM peer! + "MessageChannel", +// IMPORTANT: Do not change this list without review from a DOM peer! + "MessageEvent", +// IMPORTANT: Do not change this list without review from a DOM peer! + "MessagePort", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Notification", +// IMPORTANT: Do not change this list without review from a DOM peer! + "NotificationEvent", +// IMPORTANT: Do not change this list without review from a DOM peer! +// IMPORTANT: Do not change this list without review from a DOM peer! + "Performance", +// IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceEntry", +// IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceMark", +// IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceMeasure", +// IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PerformanceObserver", nightly: true }, +// IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PerformanceObserverEntryList", nightly: true }, +// IMPORTANT: Do not change this list without review from a DOM peer! + "Request", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Response", +// IMPORTANT: Do not change this list without review from a DOM peer! + "ServiceWorker", +// IMPORTANT: Do not change this list without review from a DOM peer! + "ServiceWorkerGlobalScope", +// IMPORTANT: Do not change this list without review from a DOM peer! + "ServiceWorkerRegistration", +// IMPORTANT: Do not change this list without review from a DOM peer! + {name: "StorageManager", nightly: true}, +// IMPORTANT: Do not change this list without review from a DOM peer! + "SubtleCrypto", +// IMPORTANT: Do not change this list without review from a DOM peer! + "TextDecoder", +// IMPORTANT: Do not change this list without review from a DOM peer! + "TextEncoder", +// IMPORTANT: Do not change this list without review from a DOM peer! + "URL", +// IMPORTANT: Do not change this list without review from a DOM peer! + "URLSearchParams", +// IMPORTANT: Do not change this list without review from a DOM peer! + "WebSocket", +// IMPORTANT: Do not change this list without review from a DOM peer! + "WindowClient", +// IMPORTANT: Do not change this list without review from a DOM peer! + "WorkerGlobalScope", +// IMPORTANT: Do not change this list without review from a DOM peer! + "WorkerLocation", +// IMPORTANT: Do not change this list without review from a DOM peer! + "WorkerNavigator", +// IMPORTANT: Do not change this list without review from a DOM peer! + ]; +// IMPORTANT: Do not change the list above without review from a DOM peer! + +function createInterfaceMap(version, userAgent) { + var isNightly = version.endsWith("a1"); + var isRelease = !version.includes("a"); + var isDesktop = !/Mobile|Tablet/.test(userAgent); + var isAndroid = !!navigator.userAgent.includes("Android"); + + var interfaceMap = {}; + + function addInterfaces(interfaces) + { + for (var entry of interfaces) { + if (typeof(entry) === "string") { + interfaceMap[entry] = true; + } else { + ok(!("pref" in entry), "Bogus pref annotation for " + entry.name); + if ((entry.nightly === !isNightly) || + (entry.nightlyAndroid === !(isAndroid && isNightly) && isAndroid) || + (entry.nonReleaseAndroid === !(isAndroid && !isRelease) && isAndroid) || + (entry.desktop === !isDesktop) || + (entry.android === !isAndroid && !entry.nonReleaseAndroid && !entry.nightlyAndroid) || + (entry.release === !isRelease)) { + interfaceMap[entry.name] = false; + } else { + interfaceMap[entry.name] = true; + } + } + } + } + + addInterfaces(ecmaGlobals); + addInterfaces(interfaceNamesInGlobalScope); + + return interfaceMap; +} + +function runTest(version, userAgent) { + var interfaceMap = createInterfaceMap(version, userAgent); + for (var name of Object.getOwnPropertyNames(self)) { + // An interface name should start with an upper case character. + if (!/^[A-Z]/.test(name)) { + continue; + } + ok(interfaceMap[name], + "If this is failing: DANGER, are you sure you want to expose the new interface " + name + + " to all webpages as a property on the service worker? Do not make a change to this file without a " + + " review from a DOM peer for that specific change!!! (or a JS peer for changes to ecmaGlobals)"); + delete interfaceMap[name]; + } + for (var name of Object.keys(interfaceMap)) { + ok(name in self === interfaceMap[name], + name + " should " + (interfaceMap[name] ? "" : " NOT") + " be defined on the global scope"); + if (!interfaceMap[name]) { + delete interfaceMap[name]; + } + } + is(Object.keys(interfaceMap).length, 0, + "The following interface(s) are not enumerated: " + Object.keys(interfaceMap).join(", ")); +} + +workerTestGetVersion(function(version) { + workerTestGetUserAgent(function(userAgent) { + runTest(version, userAgent); + workerTestDone(); + }); +}); diff --git a/dom/workers/test/serviceworkers/test_serviceworker_not_sharedworker.html b/dom/workers/test/serviceworkers/test_serviceworker_not_sharedworker.html new file mode 100644 index 000000000..96dd9f159 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_serviceworker_not_sharedworker.html @@ -0,0 +1,66 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1141274 - test that service workers and shared workers are separate</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + const SCOPE = "http://mochi.test:8888/tests/dom/workers/test/serviceworkers/"; + function runTest() { + navigator.serviceWorker.ready.then(setupSW); + navigator.serviceWorker.register("serviceworker_not_sharedworker.js", + {scope: SCOPE}); + } + + var sw, worker; + function setupSW(registration) { + sw = registration.waiting || registration.active; + worker = new SharedWorker("serviceworker_not_sharedworker.js", SCOPE); + worker.port.start(); + iframe = document.querySelector("iframe"); + iframe.src = "message_receiver.html"; + iframe.onload = function() { + window.onmessage = function(e) { + is(e.data.result, "serviceworker", "We should be talking to a service worker"); + window.onmessage = null; + worker.port.onmessage = function(e) { + is(e.data.result, "sharedworker", "We should be talking to a shared worker"); + registration.unregister().then(function(success) { + ok(success, "unregister should succeed"); + SimpleTest.finish(); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + SimpleTest.finish(); + }); + }; + worker.port.postMessage({msg: "whoareyou"}); + }; + sw.postMessage({msg: "whoareyou"}); + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_serviceworkerinfo.xul b/dom/workers/test/serviceworkers/test_serviceworkerinfo.xul new file mode 100644 index 000000000..96e4bb1c3 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_serviceworkerinfo.xul @@ -0,0 +1,115 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for ServiceWorkerInfo" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome_helpers.js"/> + <script type="application/javascript"> + <![CDATA[ + + let IFRAME_URL = EXAMPLE_URL + "serviceworkerinfo_iframe.html"; + + function wait_for_active_worker(registration) { + ok(registration, "Registration is valid."); + return new Promise(function(res, rej) { + if (registration.activeWorker) { + res(registration); + return; + } + let listener = { + onChange: function() { + if (registration.activeWorker) { + registration.removeListener(listener); + res(registration); + } + } + } + registration.addListener(listener); + }); + } + + function test() { + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({'set': [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.idle_extended_timeout", 1000000], + ["dom.serviceWorkers.idle_timeout", 0], + ["dom.serviceWorkers.testing.enabled", true], + ]}, function () { + Task.spawn(function *() { + let iframe = $("iframe"); + let promise = new Promise(function (resolve) { + iframe.onload = function () { + resolve(); + }; + }); + iframe.src = IFRAME_URL; + yield promise; + + info("Check that a service worker eventually shuts down."); + promise = Promise.all([ + waitForRegister(EXAMPLE_URL), + waitForServiceWorkerShutdown() + ]); + iframe.contentWindow.postMessage("register", "*"); + let [registration] = yield promise; + + // Make sure the worker is active. + registration = yield wait_for_active_worker(registration); + + let activeWorker = registration.activeWorker; + ok(activeWorker !== null, "Worker is not active!"); + ok(activeWorker.debugger === null); + + info("Attach a debugger to the service worker, and check that the " + + "service worker is restarted."); + activeWorker.attachDebugger(); + let workerDebugger = activeWorker.debugger; + ok(workerDebugger !== null); + + // Verify debugger properties + ok(workerDebugger.principal instanceof Ci.nsIPrincipal); + is(workerDebugger.url, EXAMPLE_URL + "worker.js"); + + info("Verify that getRegistrationByPrincipal return the same " + + "nsIServiceWorkerRegistrationInfo"); + let reg = swm.getRegistrationByPrincipal(workerDebugger.principal, + workerDebugger.url); + is(reg, registration); + + info("Check that getWorkerByID returns the same nsIWorkerDebugger"); + is(activeWorker, reg.getWorkerByID(workerDebugger.serviceWorkerID)); + + info("Detach the debugger from the service worker, and check that " + + "the service worker eventually shuts down again."); + promise = waitForServiceWorkerShutdown(); + activeWorker.detachDebugger(); + yield promise; + ok(activeWorker.debugger === null); + + promise = waitForUnregister(EXAMPLE_URL); + iframe.contentWindow.postMessage("unregister", "*"); + registration = yield promise; + + SimpleTest.finish(); + }); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + <iframe id="iframe"></iframe> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/serviceworkers/test_serviceworkermanager.xul b/dom/workers/test/serviceworkers/test_serviceworkermanager.xul new file mode 100644 index 000000000..ead935a3c --- /dev/null +++ b/dom/workers/test/serviceworkers/test_serviceworkermanager.xul @@ -0,0 +1,80 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for ServiceWorkerManager" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome_helpers.js"/> + <script type="application/javascript"> + <![CDATA[ + + let IFRAME_URL = EXAMPLE_URL + "serviceworkermanager_iframe.html"; + + function test() { + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({'set': [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, function () { + Task.spawn(function* () { + let registrations = swm.getAllRegistrations(); + is(registrations.length, 0); + + let iframe = $("iframe"); + let promise = waitForIframeLoad(iframe); + iframe.src = IFRAME_URL; + yield promise; + + info("Check that the service worker manager notifies its listeners " + + "when a service worker is registered."); + promise = waitForRegister(EXAMPLE_URL); + iframe.contentWindow.postMessage("register", "*"); + let registration = yield promise; + + registrations = swm.getAllRegistrations(); + is(registrations.length, 1); + is(registrations.queryElementAt(0, Ci.nsIServiceWorkerRegistrationInfo), + registration); + + info("Check that the service worker manager does not notify its " + + "listeners when a service worker is registered with the same " + + "scope as an existing registration."); + let listener = { + onRegister: function () { + ok(false, "Listener should not have been notified."); + } + }; + swm.addListener(listener); + iframe.contentWindow.postMessage("register", "*"); + + info("Check that the service worker manager notifies its listeners " + + "when a service worker is unregistered."); + promise = waitForUnregister(EXAMPLE_URL); + iframe.contentWindow.postMessage("unregister", "*"); + registration = yield promise; + swm.removeListener(listener); + + registrations = swm.getAllRegistrations(); + is(registrations.length, 0); + + SimpleTest.finish(); + }); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + <iframe id="iframe"></iframe> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/serviceworkers/test_serviceworkerregistrationinfo.xul b/dom/workers/test/serviceworkers/test_serviceworkerregistrationinfo.xul new file mode 100644 index 000000000..c879dc01b --- /dev/null +++ b/dom/workers/test/serviceworkers/test_serviceworkerregistrationinfo.xul @@ -0,0 +1,115 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for ServiceWorkerRegistrationInfo" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome_helpers.js"/> + <script type="application/javascript"> + <![CDATA[ + + let IFRAME_URL = EXAMPLE_URL + "serviceworkerregistrationinfo_iframe.html"; + + function test() { + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({'set': [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, function () { + Task.spawn(function* () { + let iframe = $("iframe"); + let promise = waitForIframeLoad(iframe); + iframe.src = IFRAME_URL; + yield promise; + + // The change handler is not guaranteed to be called within the same + // tick of the event loop as the one in which the change happened. + // Because of this, the exact state of the service worker registration + // is only known until the handler returns. + // + // Because then-handlers are resolved asynchronously, the following + // checks are done using callbacks, which are called synchronously + // when then handler is called. These callbacks can return a promise, + // which is used to resolve the promise returned by the function. + + info("Check that a service worker registration notifies its " + + "listeners when its state changes."); + promise = waitForRegister(EXAMPLE_URL, function (registration) { + is(registration.scriptSpec, ""); + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker === null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + is(registration.scriptSpec, EXAMPLE_URL + "worker.js"); + ok(registration.installingWorker !== null); + is(registration.installingWorker.scriptSpec, EXAMPLE_URL + "worker.js"); + ok(registration.waitingWorker === null); + ok(registration.activeWorker === null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + ok(registration.installingWorker === null); + ok(registration.waitingWorker !== null); + ok(registration.activeWorker === null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker !== null); + + return registration; + }); + }); + }); + }); + iframe.contentWindow.postMessage("register", "*"); + let registration = yield promise; + + promise = waitForServiceWorkerRegistrationChange(registration, function () { + is(registration.scriptSpec, EXAMPLE_URL + "worker2.js"); + ok(registration.installingWorker !== null); + is(registration.installingWorker.scriptSpec, EXAMPLE_URL + "worker2.js"); + ok(registration.waitingWorker === null); + ok(registration.activeWorker !== null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + ok(registration.installingWorker === null); + ok(registration.waitingWorker !== null); + ok(registration.activeWorker !== null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker !== null); + + return registration; + }); + }); + }); + iframe.contentWindow.postMessage("register", "*"); + yield promise; + + iframe.contentWindow.postMessage("unregister", "*"); + yield waitForUnregister(EXAMPLE_URL); + + SimpleTest.finish(); + }); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + <iframe id="iframe"></iframe> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/serviceworkers/test_skip_waiting.html b/dom/workers/test/serviceworkers/test_skip_waiting.html new file mode 100644 index 000000000..7707d6035 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_skip_waiting.html @@ -0,0 +1,95 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1131352 - Add ServiceWorkerGlobalScope skipWaiting()</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration, iframe, content; + + function start() { + return navigator.serviceWorker.register("worker.js", + {scope: "./skip_waiting_scope/"}); + } + + function waitForActivated(swr) { + registration = swr; + var promise = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data === "READY") { + ok(true, "Active worker is activated now"); + resolve(); + } else { + ok(false, "Wrong value. Somenting went wrong"); + resolve(); + } + } + }); + + iframe = document.createElement("iframe"); + iframe.setAttribute("src", "skip_waiting_scope/index.html"); + + content = document.getElementById("content"); + content.appendChild(iframe); + + return promise; + } + + function checkWhetherItSkippedWaiting() { + var promise = new Promise(function(resolve, reject) { + window.onmessage = function (evt) { + if (evt.data.event === "controllerchange") { + ok(evt.data.controllerScriptURL.match("skip_waiting_installed_worker"), + "The controller changed after skiping the waiting step"); + resolve(); + } else { + ok(false, "Wrong value. Somenting went wrong"); + resolve(); + } + }; + }); + + navigator.serviceWorker.register("skip_waiting_installed_worker.js", + {scope: "./skip_waiting_scope/"}) + .then(swr => { + registration = swr; + }); + + return promise; + } + + function clean() { + content.removeChild(iframe); + + return registration.unregister(); + } + + function runTest() { + start() + .then(waitForActivated) + .then(checkWhetherItSkippedWaiting) + .then(clean) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_strict_mode_warning.html b/dom/workers/test/serviceworkers/test_strict_mode_warning.html new file mode 100644 index 000000000..5b66673b9 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_strict_mode_warning.html @@ -0,0 +1,42 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1170550 - test registration of service worker scripts with a strict mode warning</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + navigator.serviceWorker + .register("strict_mode_warning.js", {scope: "strict_mode_warning"}) + .then((reg) => { + ok(true, "Registration should not fail for warnings"); + return reg.unregister(); + }) + .then(() => { + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_third_party_iframes.html b/dom/workers/test/serviceworkers/test_third_party_iframes.html new file mode 100644 index 000000000..33e815379 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_third_party_iframes.html @@ -0,0 +1,175 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <title>Bug 1152899 - Disallow the interception of third-party iframes using service workers when the third-party cookie preference is set</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript;version=1.7"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(2); + +let index = 0; +function next() { + info("Step " + index); + if (index >= steps.length) { + SimpleTest.finish(); + return; + } + try { + let i = index++; + steps[i](); + } catch(ex) { + ok(false, "Caught exception", ex); + } +} + +onload = next; + +let iframe; +let basePath = "/tests/dom/workers/test/serviceworkers/thirdparty/"; +let origin = window.location.protocol + "//" + window.location.host; +let thirdPartyOrigin = "https://example.com"; + +function testIframeLoaded() { + ok(true, "Iframe loaded"); + iframe.removeEventListener("load", testIframeLoaded); + let message = { + source: "parent", + href: origin + basePath + "iframe2.html" + }; + iframe.contentWindow.postMessage(message.toSource(), "*"); +} + +function loadThirdPartyIframe() { + let message = { + source: "parent", + href: thirdPartyOrigin + basePath + "iframe2.html" + } + iframe.contentWindow.postMessage(message.toSource(), "*"); +} + +function runTest(aExpectedResponses) { + iframe = document.querySelector("iframe"); + iframe.src = thirdPartyOrigin + basePath + "register.html"; + let responsesIndex = 0; + window.onmessage = function(e) { + let status = e.data.status; + let expected = aExpectedResponses[responsesIndex]; + if (status == expected.status) { + ok(true, "Received expected " + expected.status); + if (expected.next) { + expected.next(); + } + } else { + ok(false, "Expected " + expected.status + " got " + status); + } + responsesIndex++; + }; +} + +function testShouldIntercept(done) { + runTest([{ + status: "ok" + }, { + status: "registrationdone", + next: function() { + iframe.addEventListener("load", testIframeLoaded); + iframe.src = origin + basePath + "iframe1.html"; + } + }, { + status: "networkresponse", + next: loadThirdPartyIframe + }, { + status: "swresponse", + next: function() { + iframe.src = thirdPartyOrigin + basePath + "unregister.html"; + } + }, { + status: "unregistrationdone", + next: function() { + window.onmessage = null; + ok(true, "Test finished successfully"); + done(); + } + }]); +} + +function testShouldNotIntercept(done) { + runTest([{ + status: "ok" + }, { + status: "registrationdone", + next: function() { + iframe.addEventListener("load", testIframeLoaded); + iframe.src = origin + basePath + "iframe1.html"; + } + }, { + status: "networkresponse", + next: loadThirdPartyIframe + }, { + status: "networkresponse", + next: function() { + iframe.src = thirdPartyOrigin + basePath + "unregister.html"; + } + }, { + status: "unregistrationdone", + next: function() { + window.onmessage = null; + ok(true, "Test finished successfully"); + done(); + } + }]); +} + +const COOKIE_BEHAVIOR_ACCEPT = 0; +const COOKIE_BEHAVIOR_REJECTFOREIGN = 1; +const COOKIE_BEHAVIOR_REJECT = 2; +const COOKIE_BEHAVIOR_LIMITFOREIGN = 3; + +let steps = [() => { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["browser.dom.window.dump.enabled", true], + ["network.cookie.cookieBehavior", COOKIE_BEHAVIOR_ACCEPT] + ]}, next); +}, () => { + testShouldIntercept(next); +}, () => { + SpecialPowers.pushPrefEnv({"set": [ + ["network.cookie.cookieBehavior", COOKIE_BEHAVIOR_REJECTFOREIGN] + ]}, next); +}, () => { + testShouldNotIntercept(next); +}, () => { + SpecialPowers.pushPrefEnv({"set": [ + ["network.cookie.cookieBehavior", COOKIE_BEHAVIOR_REJECT] + ]}, next); +}, () => { + testShouldIntercept(next); +}, () => { + SpecialPowers.pushPrefEnv({"set": [ + ["network.cookie.cookieBehavior", COOKIE_BEHAVIOR_LIMITFOREIGN] + ]}, next); +}, () => { + testShouldIntercept(next); +}]; + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_unregister.html b/dom/workers/test/serviceworkers/test_unregister.html new file mode 100644 index 000000000..8366f50c1 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_unregister.html @@ -0,0 +1,138 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 984048 - Test unregister</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + return navigator.serviceWorker.register("worker.js", { scope: "unregister/" }).then(function(swr) { + if (swr.installing) { + return new Promise(function(resolve, reject) { + swr.installing.onstatechange = function(e) { + if (swr.waiting) { + swr.waiting.onstatechange = function(e) { + if (swr.active) { + resolve(); + } else if (swr.waiting && swr.waiting.state == "redundant") { + reject("Should not go into redundant"); + } + } + } else { + if (swr.active) { + resolve(); + } else { + reject("No waiting and no active!"); + } + } + } + }); + } else { + return Promise.reject("Installing should be non-null"); + } + }); + } + + function testControlled() { + var testPromise = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (!("controlled" in e.data)) { + ok(false, "Something went wrong."); + rej(); + return; + } + + ok(e.data.controlled, "New window should be controlled."); + res(); + } + }) + + var div = document.getElementById("content"); + ok(div, "Parent exists"); + + var ifr = document.createElement("iframe"); + ifr.setAttribute('src', "unregister/index.html"); + div.appendChild(ifr); + + return testPromise.then(function() { + div.removeChild(ifr); + }); + } + + function unregister() { + return navigator.serviceWorker.getRegistration("unregister/") + .then(function(reg) { + if (!reg) { + info("Registration already removed"); + return; + } + + info("getRegistration() succeeded " + reg.scope); + return reg.unregister().then(function(v) { + ok(v, "Unregister should resolve to true"); + }, function(e) { + ok(false, "Unregister failed with " + e.name); + }); + }); + } + + function testUncontrolled() { + var testPromise = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (!("controlled" in e.data)) { + ok(false, "Something went wrong."); + rej(); + return; + } + + ok(!e.data.controlled, "New window should not be controlled."); + res(); + } + }); + + var div = document.getElementById("content"); + ok(div, "Parent exists"); + + var ifr = document.createElement("iframe"); + ifr.setAttribute('src', "unregister/index.html"); + div.appendChild(ifr); + + return testPromise.then(function() { + div.removeChild(ifr); + }); + } + + function runTest() { + simpleRegister() + .then(testControlled) + .then(unregister) + .then(testUncontrolled) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_unresolved_fetch_interception.html b/dom/workers/test/serviceworkers/test_unresolved_fetch_interception.html new file mode 100644 index 000000000..7296a3ddf --- /dev/null +++ b/dom/workers/test/serviceworkers/test_unresolved_fetch_interception.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<html> +<!-- + Test that an unresolved respondWith promise will reset the channel when + the service worker is terminated due to idling, and that appropriate error + messages are logged for both the termination of the serice worker and the + resetting of the channel. + --> +<head> + <title>Test for Bug 1188545</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/SpawnTask.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1188545">Mozilla Bug 118845</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="text/javascript"> +// (This doesn't really need to be its own task, but it allows the actual test +// case to be self-contained.) +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}); +}); + +add_task(function* grace_timeout_termination_with_interrupted_intercept() { + // Setup timeouts so that the service worker will go into grace timeout after + // a zero-length idle timeout. + yield SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.idle_timeout", 0], + ["dom.serviceWorkers.idle_extended_timeout", 299999]]}); + + // The SW will claim us once it activates; this is async, start listening now. + let waitForControlled = new Promise((resolve) => { + navigator.serviceWorker.oncontrollerchange = resolve; + }); + + let registration = yield navigator.serviceWorker.register( + "unresolved_fetch_worker.js", { scope: "./"} ); + yield waitForControlled; + ok(navigator.serviceWorker.controller, "Controlled"); // double check! + + // We want to make sure the SW is active and processing the fetch before we + // try and kill it. It sends us a message when it has done so. + let waitForFetchActive = new Promise((resolve) => { + navigator.serviceWorker.onmessage = resolve; + }); + + // Issue a fetch which the SW will respondWith() a never resolved promise. + // The fetch, however, will terminate when the SW is killed, so check that. + let hangingFetch = fetch("does_not_exist.html") + .then(() => { ok(false, "should have rejected "); }, + () => { ok(true, "hung fetch terminates when worker dies"); }); + + yield waitForFetchActive; + + let expectedMessage = expect_console_message( + // Termination error + "ServiceWorkerGraceTimeoutTermination", + [make_absolute_url("./")], + // The interception failure error generated by the RespondWithHandler + // destructor when it notices it didn't get a response before being + // destroyed. It logs via the intercepted channel nsIConsoleReportCollector + // that is eventually flushed to our document and its console. + "InterceptionFailedWithURL", + [make_absolute_url("does_not_exist.html")] + ); + + // Zero out the grace timeout too so the worker will get terminated after two + // zero-length timer firings. Note that we need to do something to get the + // SW to renew its keepalive for this to actually cause the timers to be + // rescheduled... + yield SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.idle_extended_timeout", 0]]}); + // ...which we do by postMessaging it. + navigator.serviceWorker.controller.postMessage("doomity doom doom"); + + // Now wait for signs that the worker was terminated by the fetch failing. + yield hangingFetch; + + // The worker should now be dead and the error logged, wait/assert. + yield wait_for_expected_message(expectedMessage); + + // roll back all of our test case specific preferences and otherwise cleanup + yield SpecialPowers.popPrefEnv(); + yield SpecialPowers.popPrefEnv(); + yield registration.unregister(); +}); +</script> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/test_workerUnregister.html b/dom/workers/test/serviceworkers/test_workerUnregister.html new file mode 100644 index 000000000..947861c17 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_workerUnregister.html @@ -0,0 +1,82 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982728 - Test ServiceWorkerGlobalScope.unregister</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="container"></div> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + return navigator.serviceWorker.register("worker_unregister.js", { scope: "unregister/" }).then(function(swr) { + if (swr.installing) { + return new Promise(function(resolve, reject) { + swr.installing.onstatechange = function(e) { + if (swr.waiting) { + swr.waiting.onstatechange = function(e) { + if (swr.active) { + resolve(); + } else if (swr.waiting && swr.waiting.state == "redundant") { + reject("Should not go into redundant"); + } + } + } else { + if (swr.active) { + resolve(); + } else { + reject("No waiting and no active!"); + } + } + } + }); + } else { + return Promise.reject("Installing should be non-null"); + } + }); + } + + function waitForMessages(sw) { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data === "DONE") { + ok(true, "The worker has unregistered itself"); + } else if (e.data === "ERROR") { + ok(false, "The worker has unregistered itself"); + } else if (e.data === "FINISH") { + resolve(); + } + } + }); + + var frame = document.createElement("iframe"); + frame.setAttribute("src", "unregister/unregister.html"); + document.body.appendChild(frame); + + return p; + } + + function runTest() { + simpleRegister().then(waitForMessages).catch(function(e) { + ok(false, "Something went wrong."); + }).then(function() { + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_workerUpdate.html b/dom/workers/test/serviceworkers/test_workerUpdate.html new file mode 100644 index 000000000..5621d6cb8 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_workerUpdate.html @@ -0,0 +1,62 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1065366 - Test ServiceWorkerGlobalScope.update</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="container"></div> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + return navigator.serviceWorker.register("worker_update.js", { scope: "workerUpdate/" }); + } + + var registration; + function waitForMessages(sw) { + registration = sw; + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data === "FINISH") { + ok(true, "The worker has updated itself"); + resolve(); + } else if (e.data === "FAIL") { + ok(false, "The worker failed to update itself"); + resolve(); + } + } + }); + + var frame = document.createElement("iframe"); + frame.setAttribute("src", "workerUpdate/update.html"); + document.body.appendChild(frame); + + return p; + } + + function runTest() { + simpleRegister().then(waitForMessages).catch(function(e) { + ok(false, "Something went wrong."); + }).then(function() { + return registration.unregister(); + }).then(function() { + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_workerupdatefoundevent.html b/dom/workers/test/serviceworkers/test_workerupdatefoundevent.html new file mode 100644 index 000000000..3361eba08 --- /dev/null +++ b/dom/workers/test/serviceworkers/test_workerupdatefoundevent.html @@ -0,0 +1,85 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1131327 - Test ServiceWorkerRegistration.onupdatefound on ServiceWorker</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + var promise; + + function start() { + return navigator.serviceWorker.register("worker_updatefoundevent.js", + { scope: "./updatefoundevent.html" }) + .then((swr) => registration = swr); + } + + function startWaitForUpdateFound() { + registration.onupdatefound = function(e) { + } + + promise = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + + if (e.data == "finish") { + ok(true, "Received updatefound"); + resolve(); + } + } + }); + + content = document.getElementById("content"); + iframe = document.createElement("iframe"); + content.appendChild(iframe); + iframe.setAttribute("src", "./updatefoundevent.html"); + + return Promise.resolve(); + } + + function registerNext() { + return navigator.serviceWorker.register("worker_updatefoundevent2.js", + { scope: "./updatefoundevent.html" }); + } + + function waitForUpdateFound() { + return promise; + } + + function unregister() { + window.onmessage = null; + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }); + } + + function runTest() { + start() + .then(startWaitForUpdateFound) + .then(registerNext) + .then(waitForUpdateFound) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/test_xslt.html b/dom/workers/test/serviceworkers/test_xslt.html new file mode 100644 index 000000000..44270753b --- /dev/null +++ b/dom/workers/test/serviceworkers/test_xslt.html @@ -0,0 +1,128 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182113 - Test service worker XSLT interception</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + var worker; + + function start() { + return navigator.serviceWorker.register("xslt_worker.js", + { scope: "./" }) + .then((swr) => { + registration = swr; + + // Ensure the registration is active before continuing + var worker = registration.installing; + return new Promise((resolve) => { + if (worker.state === 'activated') { + resolve(); + return; + } + worker.addEventListener('statechange', () => { + if (worker.state === 'activated') { + resolve(); + } + }); + }); + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function getXmlString(xmlObject) { + serializer = new XMLSerializer(); + return serializer.serializeToString(iframe.contentDocument); + } + + function synthetic() { + content = document.getElementById("content"); + ok(content, "parent exists."); + + iframe = document.createElement("iframe"); + content.appendChild(iframe); + + iframe.setAttribute('src', "xslt/test.xml"); + + var p = new Promise(function(res, rej) { + iframe.onload = function(e) { + dump("Set request mode\n"); + registration.active.postMessage("synthetic"); + xmlString = getXmlString(iframe.contentDocument); + ok(!xmlString.includes("Error"), "Load synthetic cross origin XSLT should be allowed"); + res(); + }; + }); + + return p; + } + + function cors() { + var p = new Promise(function(res, rej) { + iframe.onload = function(e) { + xmlString = getXmlString(iframe.contentDocument); + ok(!xmlString.includes("Error"), "Load CORS cross origin XSLT should be allowed"); + res(); + }; + }); + + registration.active.postMessage("cors"); + iframe.setAttribute('src', "xslt/test.xml"); + + return p; + } + + function opaque() { + var p = new Promise(function(res, rej) { + iframe.onload = function(e) { + xmlString = getXmlString(iframe.contentDocument); + ok(xmlString.includes("Error"), "Load opaque cross origin XSLT should not be allowed"); + res(); + }; + }); + + registration.active.postMessage("opaque"); + iframe.setAttribute('src', "xslt/test.xml"); + + return p; + } + + function runTest() { + start() + .then(synthetic) + .then(opaque) + .then(cors) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/thirdparty/iframe1.html b/dom/workers/test/serviceworkers/thirdparty/iframe1.html new file mode 100644 index 000000000..43fe8c572 --- /dev/null +++ b/dom/workers/test/serviceworkers/thirdparty/iframe1.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <title>SW third party iframe test</title> + + <script type="text/javascript;version=1.7"> + function messageListener(event) { + let message = eval(event.data); + + dump("got message " + JSON.stringify(message) + "\n"); + if (message.source == "parent") { + document.getElementById("iframe2").src = message.href; + } + else if (message.source == "iframe") { + parent.postMessage(event.data, "*"); + } + } + </script> + +</head> + +<body onload="window.addEventListener('message', messageListener, false);"> + <iframe id="iframe2"></iframe> +</body> + +</html> diff --git a/dom/workers/test/serviceworkers/thirdparty/iframe2.html b/dom/workers/test/serviceworkers/thirdparty/iframe2.html new file mode 100644 index 000000000..fac6a9395 --- /dev/null +++ b/dom/workers/test/serviceworkers/thirdparty/iframe2.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({ + source: "iframe", + status: "networkresponse" + }, "*"); +</script> diff --git a/dom/workers/test/serviceworkers/thirdparty/register.html b/dom/workers/test/serviceworkers/thirdparty/register.html new file mode 100644 index 000000000..59b8c5c41 --- /dev/null +++ b/dom/workers/test/serviceworkers/thirdparty/register.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + var isDone = false; + function done(reg) { + if (!isDone) { + ok(reg.waiting || reg.active, + "Either active or waiting worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + isDone = true; + } + } + + navigator.serviceWorker.register("sw.js", {scope: "."}) + .then(function(registration) { + if (registration.installing) { + registration.installing.onstatechange = function(e) { + done(registration); + }; + } else { + done(registration); + } + }); +</script> diff --git a/dom/workers/test/serviceworkers/thirdparty/sw.js b/dom/workers/test/serviceworkers/thirdparty/sw.js new file mode 100644 index 000000000..ca45698c8 --- /dev/null +++ b/dom/workers/test/serviceworkers/thirdparty/sw.js @@ -0,0 +1,14 @@ +self.addEventListener("fetch", function(event) { + dump("fetch " + event.request.url + "\n"); + if (event.request.url.indexOf("iframe2.html") >= 0) { + var body = + "<script>" + + "window.parent.postMessage({" + + "source: 'iframe', status: 'swresponse'" + + "}, '*');" + + "</script>"; + event.respondWith(new Response(body, { + headers: {'Content-Type': 'text/html'} + })); + } +}); diff --git a/dom/workers/test/serviceworkers/thirdparty/unregister.html b/dom/workers/test/serviceworkers/thirdparty/unregister.html new file mode 100644 index 000000000..2cb6ee0ce --- /dev/null +++ b/dom/workers/test/serviceworkers/thirdparty/unregister.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + if(!registration) { + return; + } + registration.unregister().then(() => { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + }); + }); +</script> diff --git a/dom/workers/test/serviceworkers/unregister/index.html b/dom/workers/test/serviceworkers/unregister/index.html new file mode 100644 index 000000000..db23d2cb7 --- /dev/null +++ b/dom/workers/test/serviceworkers/unregister/index.html @@ -0,0 +1,26 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 984048 - Test unregister</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + info("unregister/index.html should not to be launched directly!"); + } + + parent.postMessage({ controlled: !!navigator.serviceWorker.controller }, "*"); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/serviceworkers/unregister/unregister.html b/dom/workers/test/serviceworkers/unregister/unregister.html new file mode 100644 index 000000000..6fda82026 --- /dev/null +++ b/dom/workers/test/serviceworkers/unregister/unregister.html @@ -0,0 +1,22 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test worker::unregister</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + + navigator.serviceWorker.onmessage = function(e) { parent.postMessage(e.data, "*"); } + navigator.serviceWorker.controller.postMessage("GO"); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/unresolved_fetch_worker.js b/dom/workers/test/serviceworkers/unresolved_fetch_worker.js new file mode 100644 index 000000000..71fbad781 --- /dev/null +++ b/dom/workers/test/serviceworkers/unresolved_fetch_worker.js @@ -0,0 +1,19 @@ +var keepPromiseAlive; +onfetch = function(event) { + event.waitUntil( + clients.matchAll() + .then(clients => { + clients.forEach(client => { + client.postMessage("continue"); + }); + }) + ); + + // Never resolve, and keep it alive on our global so it can't get GC'ed and + // make this test weird and intermittent. + event.respondWith((keepPromiseAlive = new Promise(function(res, rej) {}))); +} + +onactivate = function(event) { + event.waitUntil(clients.claim()); +} diff --git a/dom/workers/test/serviceworkers/updatefoundevent.html b/dom/workers/test/serviceworkers/updatefoundevent.html new file mode 100644 index 000000000..78088c7cd --- /dev/null +++ b/dom/workers/test/serviceworkers/updatefoundevent.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> +<head> + <title>Bug 1131327 - Test ServiceWorkerRegistration.onupdatefound on ServiceWorker</title> +</head> +<body> +<script> + navigator.serviceWorker.onmessage = function(e) { + dump("NSM iframe got message " + e.data + "\n"); + window.parent.postMessage(e.data, "*"); + }; +</script> +</body> diff --git a/dom/workers/test/serviceworkers/worker.js b/dom/workers/test/serviceworkers/worker.js new file mode 100644 index 000000000..2aba167d1 --- /dev/null +++ b/dom/workers/test/serviceworkers/worker.js @@ -0,0 +1 @@ +// empty worker, always succeed! diff --git a/dom/workers/test/serviceworkers/worker2.js b/dom/workers/test/serviceworkers/worker2.js new file mode 100644 index 000000000..3072d0817 --- /dev/null +++ b/dom/workers/test/serviceworkers/worker2.js @@ -0,0 +1 @@ +// worker2.js diff --git a/dom/workers/test/serviceworkers/worker3.js b/dom/workers/test/serviceworkers/worker3.js new file mode 100644 index 000000000..449fc2f97 --- /dev/null +++ b/dom/workers/test/serviceworkers/worker3.js @@ -0,0 +1 @@ +// worker3.js diff --git a/dom/workers/test/serviceworkers/workerUpdate/update.html b/dom/workers/test/serviceworkers/workerUpdate/update.html new file mode 100644 index 000000000..8f984ccc4 --- /dev/null +++ b/dom/workers/test/serviceworkers/workerUpdate/update.html @@ -0,0 +1,24 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test worker::update</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + + navigator.serviceWorker.onmessage = function(e) { parent.postMessage(e.data, "*"); } + navigator.serviceWorker.ready.then(function() { + navigator.serviceWorker.controller.postMessage("GO"); + }); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/serviceworkers/worker_unregister.js b/dom/workers/test/serviceworkers/worker_unregister.js new file mode 100644 index 000000000..7a3e764f4 --- /dev/null +++ b/dom/workers/test/serviceworkers/worker_unregister.js @@ -0,0 +1,16 @@ +onmessage = function(e) { + clients.matchAll().then(function(c) { + if (c.length === 0) { + // We cannot proceed. + return; + } + + registration.unregister().then(function() { + c[0].postMessage('DONE'); + }, function() { + c[0].postMessage('ERROR'); + }).then(function() { + c[0].postMessage('FINISH'); + }); + }); +} diff --git a/dom/workers/test/serviceworkers/worker_update.js b/dom/workers/test/serviceworkers/worker_update.js new file mode 100644 index 000000000..9f3e55b18 --- /dev/null +++ b/dom/workers/test/serviceworkers/worker_update.js @@ -0,0 +1,19 @@ +// For now this test only calls update to verify that our registration +// job queueing works properly when called from the worker thread. We should +// test actual update scenarios with a SJS test. +onmessage = function(e) { + self.registration.update().then(function(v) { + return v === undefined ? 'FINISH' : 'FAIL'; + }).catch(function(e) { + return 'FAIL'; + }).then(function(result) { + clients.matchAll().then(function(c) { + if (c.length == 0) { + dump("!!!!!!!!!!! WORKER HAS NO CLIENTS TO FINISH TEST !!!!!!!!!!!!\n"); + return; + } + + c[0].postMessage(result); + }); + }); +} diff --git a/dom/workers/test/serviceworkers/worker_updatefoundevent.js b/dom/workers/test/serviceworkers/worker_updatefoundevent.js new file mode 100644 index 000000000..a297bf455 --- /dev/null +++ b/dom/workers/test/serviceworkers/worker_updatefoundevent.js @@ -0,0 +1,23 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +onactivate = function(e) { + e.waitUntil(new Promise(function(resolve, reject) { + registration.onupdatefound = function(e) { + clients.matchAll().then(function(clients) { + if (!clients.length) { + reject("No clients found"); + } + + if (registration.scope.match(/updatefoundevent\.html$/)) { + clients[0].postMessage("finish"); + resolve(); + } else { + dump("Scope did not match"); + } + }, reject); + } + })); +} diff --git a/dom/workers/test/serviceworkers/worker_updatefoundevent2.js b/dom/workers/test/serviceworkers/worker_updatefoundevent2.js new file mode 100644 index 000000000..da4c592aa --- /dev/null +++ b/dom/workers/test/serviceworkers/worker_updatefoundevent2.js @@ -0,0 +1 @@ +// Not useful. diff --git a/dom/workers/test/serviceworkers/xslt/test.xml b/dom/workers/test/serviceworkers/xslt/test.xml new file mode 100644 index 000000000..83c777633 --- /dev/null +++ b/dom/workers/test/serviceworkers/xslt/test.xml @@ -0,0 +1,6 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/xsl" href="test.xsl"?> +<result> + <Title>Example</Title> + <Error>Error</Error> +</result> diff --git a/dom/workers/test/serviceworkers/xslt/xslt.sjs b/dom/workers/test/serviceworkers/xslt/xslt.sjs new file mode 100644 index 000000000..db681ab50 --- /dev/null +++ b/dom/workers/test/serviceworkers/xslt/xslt.sjs @@ -0,0 +1,12 @@ +function handleRequest(request, response) { + response.setHeader("Content-Type", "application/xslt+xml", false); + response.setHeader("Access-Control-Allow-Origin", "*"); + + var body = request.queryString; + if (!body) { + response.setStatusLine(null, 500, "Invalid querystring"); + return; + } + + response.write(unescape(body)); +} diff --git a/dom/workers/test/serviceworkers/xslt_worker.js b/dom/workers/test/serviceworkers/xslt_worker.js new file mode 100644 index 000000000..bf9bdbc56 --- /dev/null +++ b/dom/workers/test/serviceworkers/xslt_worker.js @@ -0,0 +1,52 @@ +var testType = 'synthetic'; + +var xslt = "<?xml version=\"1.0\"?> " + + "<xsl:stylesheet version=\"1.0\"" + + " xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">" + + " <xsl:template match=\"node()|@*\">" + + " <xsl:copy>" + + " <xsl:apply-templates select=\"node()|@*\"/>" + + " </xsl:copy>" + + " </xsl:template>" + + " <xsl:template match=\"Error\"/>" + + "</xsl:stylesheet>"; + +onfetch = function(event) { + if (event.request.url.includes('test.xsl')) { + if (testType == 'synthetic') { + if (event.request.mode != 'cors') { + event.respondWith(Response.error()); + return; + } + + event.respondWith(Promise.resolve( + new Response(xslt, { headers: {'Content-Type': 'application/xslt+xml'}}) + )); + } + else if (testType == 'cors') { + if (event.request.mode != 'cors') { + event.respondWith(Response.error()); + return; + } + + var url = "http://example.com/tests/dom/workers/test/serviceworkers/xslt/xslt.sjs?" + escape(xslt); + event.respondWith(fetch(url, { mode: 'cors' })); + } + else if (testType == 'opaque') { + if (event.request.mode != 'cors') { + event.respondWith(Response.error()); + return; + } + + var url = "http://example.com/tests/dom/workers/test/serviceworkers/xslt/xslt.sjs?" + escape(xslt); + event.respondWith(fetch(url, { mode: 'no-cors' })); + } + else { + event.respondWith(Response.error()); + } + } +}; + +onmessage = function(event) { + testType = event.data; +}; diff --git a/dom/workers/test/sharedWorker_console.js b/dom/workers/test/sharedWorker_console.js new file mode 100644 index 000000000..932235eb7 --- /dev/null +++ b/dom/workers/test/sharedWorker_console.js @@ -0,0 +1,11 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +onconnect = function(evt) { + console.profile("Hello profiling from a SharedWorker!"); + console.log("Hello world from a SharedWorker!"); + evt.ports[0].postMessage('ok!'); +} diff --git a/dom/workers/test/sharedWorker_lifetime.js b/dom/workers/test/sharedWorker_lifetime.js new file mode 100644 index 000000000..3d9a837bb --- /dev/null +++ b/dom/workers/test/sharedWorker_lifetime.js @@ -0,0 +1,5 @@ +onconnect = function(e) { + setTimeout(function() { + e.ports[0].postMessage("Still alive!"); + }, 500); +} diff --git a/dom/workers/test/sharedWorker_ports.js b/dom/workers/test/sharedWorker_ports.js new file mode 100644 index 000000000..64672e6ab --- /dev/null +++ b/dom/workers/test/sharedWorker_ports.js @@ -0,0 +1,24 @@ +var port; +onconnect = function(evt) { + evt.source.postMessage({ type: "connected" }); + + if (!port) { + port = evt.source; + evt.source.onmessage = function(evtFromPort) { + port.postMessage({type: "status", + test: "Port from the main-thread!" == evtFromPort.data, + msg: "The message is coming from the main-thread"}); + port.postMessage({type: "status", + test: (evtFromPort.ports.length == 1), + msg: "1 port transferred"}); + + evtFromPort.ports[0].onmessage = function(evtFromPort2) { + port.postMessage({type: "status", + test: (evtFromPort2.data.type == "connected"), + msg: "The original message received" }); + port.postMessage({type: "finish"}); + close(); + } + } + } +} diff --git a/dom/workers/test/sharedWorker_privateBrowsing.js b/dom/workers/test/sharedWorker_privateBrowsing.js new file mode 100644 index 000000000..9d7ec886f --- /dev/null +++ b/dom/workers/test/sharedWorker_privateBrowsing.js @@ -0,0 +1,5 @@ +var counter = 0; +onconnect = function(evt) { + evt.ports[0].postMessage(++counter); +} + diff --git a/dom/workers/test/sharedWorker_sharedWorker.js b/dom/workers/test/sharedWorker_sharedWorker.js new file mode 100644 index 000000000..5e8e93392 --- /dev/null +++ b/dom/workers/test/sharedWorker_sharedWorker.js @@ -0,0 +1,93 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +if (!("self" in this)) { + throw new Error("No 'self' exists on SharedWorkerGlobalScope!"); +} +if (this !== self) { + throw new Error("'self' not equal to global object!"); +} +if (!(self instanceof SharedWorkerGlobalScope)) { + throw new Error("self not a SharedWorkerGlobalScope instance!"); +} + +var propsToCheck = [ + "location", + "navigator", + "close", + "importScripts", + "setTimeout", + "clearTimeout", + "setInterval", + "clearInterval", + "dump", + "atob", + "btoa" +]; + +for (var index = 0; index < propsToCheck.length; index++) { + var prop = propsToCheck[index]; + if (!(prop in self)) { + throw new Error("SharedWorkerGlobalScope has no '" + prop + "' property!"); + } +} + +onconnect = function(event) { + if (!("SharedWorkerGlobalScope" in self)) { + throw new Error("SharedWorkerGlobalScope should be visible!"); + } + if (!(self instanceof SharedWorkerGlobalScope)) { + throw new Error("The global should be a SharedWorkerGlobalScope!"); + } + if (!(self instanceof WorkerGlobalScope)) { + throw new Error("The global should be a WorkerGlobalScope!"); + } + if ("DedicatedWorkerGlobalScope" in self) { + throw new Error("DedicatedWorkerGlobalScope should not be visible!"); + } + if (!(event instanceof MessageEvent)) { + throw new Error("'connect' event is not a MessageEvent!"); + } + if (!("ports" in event)) { + throw new Error("'connect' event doesn't have a 'ports' property!"); + } + if (event.ports.length != 1) { + throw new Error("'connect' event has a 'ports' property with length '" + + event.ports.length + "'!"); + } + if (!event.ports[0]) { + throw new Error("'connect' event has a null 'ports[0]' property!"); + } + if (!(event.ports[0] instanceof MessagePort)) { + throw new Error("'connect' event has a 'ports[0]' property that isn't a " + + "MessagePort!"); + } + if (!(event.ports[0] == event.source)) { + throw new Error("'connect' event source property is incorrect!"); + } + if (event.data) { + throw new Error("'connect' event has data: " + event.data); + } + + // The expression closures should trigger a warning in debug builds, but NOT + // fire error events at us. If we ever actually remove expression closures + // (in bug 1083458), we'll need something else to test this case. + (function() "Expected console warning: expression closures are deprecated"); + + event.ports[0].onmessage = function(event) { + if (!(event instanceof MessageEvent)) { + throw new Error("'message' event is not a MessageEvent!"); + } + if (!("ports" in event)) { + throw new Error("'message' event doesn't have a 'ports' property!"); + } + if (event.ports === null) { + throw new Error("'message' event has a null 'ports' property!"); + } + event.target.postMessage(event.data); + throw new Error(event.data); + }; +}; diff --git a/dom/workers/test/simpleThread_worker.js b/dom/workers/test/simpleThread_worker.js new file mode 100644 index 000000000..543d8b3dd --- /dev/null +++ b/dom/workers/test/simpleThread_worker.js @@ -0,0 +1,53 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +function messageListener(event) { + var exception; + try { + event.bubbles = true; + } + catch(e) { + exception = e; + } + + if (!(exception instanceof TypeError)) { + throw exception; + } + + switch (event.data) { + case "no-op": + break; + case "components": + postMessage(Components.toString()); + break; + case "start": + for (var i = 0; i < 1000; i++) { } + postMessage("started"); + break; + case "stop": + self.postMessage('no-op'); + postMessage("stopped"); + self.removeEventListener("message", messageListener, false); + break; + default: + throw 'Bad message: ' + event.data; + } +} + +if (!("DedicatedWorkerGlobalScope" in self)) { + throw new Error("DedicatedWorkerGlobalScope should be visible!"); +} +if (!(self instanceof DedicatedWorkerGlobalScope)) { + throw new Error("The global should be a SharedWorkerGlobalScope!"); +} +if (!(self instanceof WorkerGlobalScope)) { + throw new Error("The global should be a WorkerGlobalScope!"); +} +if ("SharedWorkerGlobalScope" in self) { + throw new Error("SharedWorkerGlobalScope should not be visible!"); +} + +addEventListener("message", { handleEvent: messageListener }); diff --git a/dom/workers/test/suspend_iframe.html b/dom/workers/test/suspend_iframe.html new file mode 100644 index 000000000..86d9d6bc1 --- /dev/null +++ b/dom/workers/test/suspend_iframe.html @@ -0,0 +1,47 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for DOM Worker Threads Suspending</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<div id="output"></div> +<script class="testbody" type="text/javascript"> + + var output = document.getElementById("output"); + + var worker; + + function terminateWorker() { + if (worker) { + worker.postMessage("stop"); + worker = null; + } + } + + function startWorker(messageCallback, errorCallback) { + var lastData; + worker = new Worker("suspend_worker.js"); + + worker.onmessage = function(event) { + output.textContent = (lastData ? lastData + " -> " : "") + event.data; + lastData = event.data; + messageCallback(event.data); + }; + + worker.onerror = function(event) { + this.terminate(); + errorCallback(event.message); + }; + } + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/suspend_worker.js b/dom/workers/test/suspend_worker.js new file mode 100644 index 000000000..43eb24a7a --- /dev/null +++ b/dom/workers/test/suspend_worker.js @@ -0,0 +1,13 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var counter = 0; + +var interval = setInterval(function() { + postMessage(++counter); +}, 100); + +onmessage = function(event) { + clearInterval(interval); +} diff --git a/dom/workers/test/terminate_worker.js b/dom/workers/test/terminate_worker.js new file mode 100644 index 000000000..f1a49e032 --- /dev/null +++ b/dom/workers/test/terminate_worker.js @@ -0,0 +1,9 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function(event) { + throw "No messages should reach me!"; +} + +setInterval(function() { postMessage("Still alive!"); }, 100); diff --git a/dom/workers/test/test_404.html b/dom/workers/test/test_404.html new file mode 100644 index 000000000..e2e83a35e --- /dev/null +++ b/dom/workers/test/test_404.html @@ -0,0 +1,41 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads +--> +<head> + <title>Test for DOM Worker Threads</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("nonexistent_worker.js"); + + worker.onmessage = function(event) { + ok(false, "Shouldn't ever get a message!"); + SimpleTest.finish(); + } + + worker.onerror = function(event) { + is(event.target, worker); + is(event.message, 'NetworkError: Failed to load worker script at "nonexistent_worker.js"'); + event.preventDefault(); + SimpleTest.finish(); + }; + + worker.postMessage("dummy"); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/test_WorkerDebugger.initialize.xul b/dom/workers/test/test_WorkerDebugger.initialize.xul new file mode 100644 index 000000000..9e40bb78c --- /dev/null +++ b/dom/workers/test/test_WorkerDebugger.initialize.xul @@ -0,0 +1,58 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebugger.initialize" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebugger.initialize_worker.js"; + const CHILD_WORKER_URL = "WorkerDebugger.initialize_childWorker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebugger.initialize_debugger.js"; + + function test() { + Task.spawn(function* () { + SimpleTest.waitForExplicitFinish(); + + info("Create a worker that creates a child worker, wait for their " + + "debuggers to be registered, and initialize them."); + let promise = waitForMultiple([ + waitForRegister(WORKER_URL, DEBUGGER_URL), + waitForRegister(CHILD_WORKER_URL, DEBUGGER_URL) + ]); + let worker = new Worker(WORKER_URL); + yield promise; + + info("Check that the debuggers are initialized before the workers " + + "start running."); + yield waitForMultiple([ + waitForWorkerMessage(worker, "debugger"), + waitForWorkerMessage(worker, "worker"), + waitForWorkerMessage(worker, "child:debugger"), + waitForWorkerMessage(worker, "child:worker") + ]); + + SimpleTest.finish(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebugger.postMessage.xul b/dom/workers/test/test_WorkerDebugger.postMessage.xul new file mode 100644 index 000000000..7affbed21 --- /dev/null +++ b/dom/workers/test/test_WorkerDebugger.postMessage.xul @@ -0,0 +1,61 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebugger.postMessage" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebugger.postMessage_worker.js"; + const CHILD_WORKER_URL = "WorkerDebugger.postMessage_childWorker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebugger.postMessage_debugger.js"; + + function test() { + Task.spawn(function* () { + SimpleTest.waitForExplicitFinish(); + + info("Create a worker that creates a child worker, wait for their " + + "debuggers to be registered, and initialize them."); + let promise = waitForMultiple([ + waitForRegister(WORKER_URL, DEBUGGER_URL), + waitForRegister(CHILD_WORKER_URL, DEBUGGER_URL) + ]); + let worker = new Worker(WORKER_URL); + let [dbg, childDbg] = yield promise; + + info("Send a request to the worker debugger. This should cause the " + + "the worker debugger to send a response."); + promise = waitForDebuggerMessage(dbg, "pong"); + dbg.postMessage("ping"); + yield promise; + + info("Send a request to the child worker debugger. This should cause " + + "the child worker debugger to send a response."); + promise = waitForDebuggerMessage(childDbg, "pong"); + childDbg.postMessage("ping"); + yield promise; + + SimpleTest.finish(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebugger.xul b/dom/workers/test/test_WorkerDebugger.xul new file mode 100644 index 000000000..f3397bd54 --- /dev/null +++ b/dom/workers/test/test_WorkerDebugger.xul @@ -0,0 +1,122 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebugger" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebugger_worker.js"; + const CHILD_WORKER_URL = "WorkerDebugger_childWorker.js"; + const SHARED_WORKER_URL = "WorkerDebugger_sharedWorker.js"; + + function test() { + Task.spawn(function* () { + SimpleTest.waitForExplicitFinish(); + + info("Create a top-level chrome worker that creates a non-top-level " + + "content worker and wait for their debuggers to be registered."); + let promise = waitForMultiple([ + waitForRegister(WORKER_URL), + waitForRegister(CHILD_WORKER_URL) + ]); + worker = new ChromeWorker(WORKER_URL); + let [dbg, childDbg] = yield promise; + + info("Check that the top-level chrome worker debugger has the " + + "correct properties."); + is(dbg.isChrome, true, + "Chrome worker debugger should be chrome."); + is(dbg.parent, null, + "Top-level debugger should not have parent."); + is(dbg.type, Ci.nsIWorkerDebugger.TYPE_DEDICATED, + "Chrome worker debugger should be dedicated."); + is(dbg.window, window, + "Top-level dedicated worker debugger should have window."); + + info("Check that the non-top-level content worker debugger has the " + + "correct properties."); + is(childDbg.isChrome, false, + "Content worker debugger should be content."); + is(childDbg.parent, dbg, + "Non-top-level worker debugger should have parent."); + is(childDbg.type, Ci.nsIWorkerDebugger.TYPE_DEDICATED, + "Content worker debugger should be dedicated."); + is(childDbg.window, null, + "Non-top-level worker debugger should not have window."); + + info("Terminate the top-level chrome worker and the non-top-level " + + "content worker, and wait for their debuggers to be " + + "unregistered and closed."); + promise = waitForMultiple([ + waitForUnregister(CHILD_WORKER_URL), + waitForDebuggerClose(childDbg), + waitForUnregister(WORKER_URL), + waitForDebuggerClose(dbg), + ]); + worker.terminate(); + yield promise; + + info("Create a shared worker and wait for its debugger to be " + + "registered"); + promise = waitForRegister(SHARED_WORKER_URL); + worker = new SharedWorker(SHARED_WORKER_URL); + let sharedDbg = yield promise; + + info("Check that the shared worker debugger has the correct " + + "properties."); + is(sharedDbg.isChrome, false, + "Shared worker debugger should be content."); + is(sharedDbg.parent, null, + "Shared worker debugger should not have parent."); + is(sharedDbg.type, Ci.nsIWorkerDebugger.TYPE_SHARED, + "Shared worker debugger should be shared."); + is(sharedDbg.window, null, + "Shared worker debugger should not have window."); + + info("Create a shared worker with the same URL and check that its " + + "debugger is not registered again."); + let listener = { + onRegistered: function () { + ok(false, + "Shared worker debugger should not be registered again."); + }, + }; + wdm.addListener(listener); + worker = new SharedWorker(SHARED_WORKER_URL); + + info("Send a message to the shared worker to tell it to close " + + "itself, and wait for its debugger to be closed."); + promise = waitForMultiple([ + waitForUnregister(SHARED_WORKER_URL), + waitForDebuggerClose(sharedDbg) + ]); + worker.port.start(); + worker.port.postMessage("close"); + yield promise; + + wdm.removeListener(listener); + SimpleTest.finish(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebuggerGlobalScope.createSandbox.xul b/dom/workers/test/test_WorkerDebuggerGlobalScope.createSandbox.xul new file mode 100644 index 000000000..0440c482b --- /dev/null +++ b/dom/workers/test/test_WorkerDebuggerGlobalScope.createSandbox.xul @@ -0,0 +1,52 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebuggerGlobalScope.createSandbox" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebuggerGlobalScope.createSandbox_worker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebuggerGlobalScope.createSandbox_debugger.js"; + + function test() { + Task.spawn(function* () { + SimpleTest.waitForExplicitFinish(); + + info("Create a worker, wait for its debugger to be registered, and " + + "initialize it."); + let promise = waitForRegister(WORKER_URL, DEBUGGER_URL); + let worker = new Worker(WORKER_URL); + let dbg = yield promise; + + info("Send a request to the worker debugger. This should cause the " + + "worker debugger to send a response from within a sandbox."); + promise = waitForDebuggerMessage(dbg, "pong"); + dbg.postMessage("ping"); + yield promise; + + SimpleTest.finish(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> + diff --git a/dom/workers/test/test_WorkerDebuggerGlobalScope.enterEventLoop.xul b/dom/workers/test/test_WorkerDebuggerGlobalScope.enterEventLoop.xul new file mode 100644 index 000000000..081395308 --- /dev/null +++ b/dom/workers/test/test_WorkerDebuggerGlobalScope.enterEventLoop.xul @@ -0,0 +1,126 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebuggerGlobalScope.enterEventLoop" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebuggerGlobalScope.enterEventLoop_worker.js"; + const CHILD_WORKER_URL = "WorkerDebuggerGlobalScope.enterEventLoop_childWorker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebuggerGlobalScope.enterEventLoop_debugger.js"; + + function test() { + Task.spawn(function* () { + SimpleTest.waitForExplicitFinish(); + + info("Create a worker that creates a child worker, wait for their " + + "debuggers to be registered, and initialize them."); + let promise = waitForMultiple([ + waitForRegister(WORKER_URL, DEBUGGER_URL), + waitForRegister(CHILD_WORKER_URL, DEBUGGER_URL) + ]); + let worker = new Worker(WORKER_URL); + let [dbg, childDbg] = yield promise; + + info("Send a request to the child worker. This should cause the " + + "child worker debugger to enter a nested event loop."); + promise = waitForDebuggerMessage(childDbg, "paused"); + worker.postMessage("child:ping"); + yield promise; + + info("Send a request to the child worker debugger. This should cause " + + "the child worker debugger to enter a second nested event loop."); + promise = waitForDebuggerMessage(childDbg, "paused"); + childDbg.postMessage("eval"); + yield promise; + + info("Send a request to the child worker debugger. This should cause " + + "the child worker debugger to leave its second nested event " + + "loop. The child worker debugger should not send a response " + + "for its previous request until after it has left the nested " + + "event loop."); + promise = waitForMultiple([ + waitForDebuggerMessage(childDbg, "resumed"), + waitForDebuggerMessage(childDbg, "evalled") + ]); + childDbg.postMessage("resume"); + yield promise; + + info("Send a request to the child worker debugger. This should cause " + + "the child worker debugger to leave its first nested event loop." + + "The child worker should not send a response for its earlier " + + "request until after the child worker debugger has left the " + + "nested event loop."); + promise = waitForMultiple([ + waitForDebuggerMessage(childDbg, "resumed"), + waitForWorkerMessage(worker, "child:pong") + ]); + childDbg.postMessage("resume"); + yield promise; + + info("Send a request to the worker. This should cause the worker " + + "debugger to enter a nested event loop."); + promise = waitForDebuggerMessage(dbg, "paused"); + worker.postMessage("ping"); + yield promise; + + info("Terminate the worker. This should not cause the worker " + + "debugger to terminate as well."); + worker.terminate(); + + worker.onmessage = function () { + ok(false, "Worker should have been terminated."); + }; + + info("Send a request to the worker debugger. This should cause the " + + "worker debugger to enter a second nested event loop."); + promise = waitForDebuggerMessage(dbg, "paused"); + dbg.postMessage("eval"); + yield promise; + + info("Send a request to the worker debugger. This should cause the " + + "worker debugger to leave its second nested event loop. The " + + "worker debugger should not send a response for the previous " + + "request until after leaving the nested event loop."); + promise = waitForMultiple([ + waitForDebuggerMessage(dbg, "resumed"), + waitForDebuggerMessage(dbg, "evalled") + ]); + dbg.postMessage("resume"); + yield promise; + + info("Send a request to the worker debugger. This should cause the " + + "worker debugger to leave its first nested event loop. The " + + "worker should not send a response for its earlier request, " + + "since it has been terminated."); + promise = waitForMultiple([ + waitForDebuggerMessage(dbg, "resumed"), + ]); + dbg.postMessage("resume"); + yield promise; + + SimpleTest.finish(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebuggerGlobalScope.reportError.xul b/dom/workers/test/test_WorkerDebuggerGlobalScope.reportError.xul new file mode 100644 index 000000000..88b078674 --- /dev/null +++ b/dom/workers/test/test_WorkerDebuggerGlobalScope.reportError.xul @@ -0,0 +1,98 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebuggerGlobalScope.reportError" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebuggerGlobalScope.reportError_worker.js"; + const CHILD_WORKER_URL = "WorkerDebuggerGlobalScope.reportError_childWorker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebuggerGlobalScope.reportError_debugger.js"; + + function test() { + Task.spawn(function* () { + SimpleTest.waitForExplicitFinish(); + + info("Create a worker that creates a child worker, wait for their " + + "debuggers to be registered, and initialize them."); + let promise = waitForMultiple([ + waitForRegister(WORKER_URL, DEBUGGER_URL), + waitForRegister(CHILD_WORKER_URL, DEBUGGER_URL) + ]); + let worker = new Worker(WORKER_URL); + let [dbg, childDbg] = yield promise; + + worker.onmessage = function () { + ok(false, "Debugger error events should not be fired at workers."); + }; + + info("Send a request to the worker debugger. This should cause the " + + "worker debugger to report an error."); + promise = waitForDebuggerError(dbg); + dbg.postMessage("report"); + let error = yield promise; + is(error.fileName, DEBUGGER_URL, + "fileName should be name of file from which error is reported."); + is(error.lineNumber, 6, + "lineNumber should be line number from which error is reported."); + is(error.message, "reported", "message should be reported."); + + info("Send a request to the worker debugger. This should cause the " + + "worker debugger to throw an error."); + promise = waitForDebuggerError(dbg); + dbg.postMessage("throw"); + error = yield promise; + is(error.fileName, DEBUGGER_URL, + "fileName should be name of file from which error is thrown"); + is(error.lineNumber, 9, + "lineNumber should be line number from which error is thrown"); + is(error.message, "Error: thrown", "message should be Error: thrown"); + + info("Send a reqeust to the child worker debugger. This should cause " + + "the child worker debugger to report an error."); + promise = waitForDebuggerError(childDbg); + childDbg.postMessage("report"); + error = yield promise; + is(error.fileName, DEBUGGER_URL, + "fileName should be name of file from which error is reported."); + is(error.lineNumber, 6, + "lineNumber should be line number from which error is reported."); + is(error.message, "reported", "message should be reported."); + + info("Send a message to the child worker debugger. This should cause " + + "the child worker debugger to throw an error."); + promise = waitForDebuggerError(childDbg); + childDbg.postMessage("throw"); + error = yield promise; + is(error.fileName, DEBUGGER_URL, + "fileName should be name of file from which error is thrown"); + is(error.lineNumber, 9, + "lineNumber should be line number from which error is thrown"); + is(error.message, "Error: thrown", "message should be Error: thrown"); + + SimpleTest.finish(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> + diff --git a/dom/workers/test/test_WorkerDebuggerGlobalScope.setImmediate.xul b/dom/workers/test/test_WorkerDebuggerGlobalScope.setImmediate.xul new file mode 100644 index 000000000..3ecac681b --- /dev/null +++ b/dom/workers/test/test_WorkerDebuggerGlobalScope.setImmediate.xul @@ -0,0 +1,54 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebuggerGlobalScope.setImmediate" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebuggerGlobalScope.setImmediate_worker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebuggerGlobalScope.setImmediate_debugger.js"; + + function test() { + Task.spawn(function* () { + SimpleTest.waitForExplicitFinish(); + + let promise = waitForRegister(WORKER_URL, DEBUGGER_URL); + let worker = new Worker(WORKER_URL); + let dbg = yield promise; + + info("Send a request to the worker debugger. This should cause a " + + "the worker debugger to send two responses. The worker debugger " + + "should send the second response before the first one, since " + + "the latter is delayed until the next tick of the event loop."); + promise = waitForMultiple([ + waitForDebuggerMessage(dbg, "pong2"), + waitForDebuggerMessage(dbg, "pong1") + ]); + dbg.postMessage("ping"); + yield promise; + + SimpleTest.finish(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebuggerManager.xul b/dom/workers/test/test_WorkerDebuggerManager.xul new file mode 100644 index 000000000..6807226bd --- /dev/null +++ b/dom/workers/test/test_WorkerDebuggerManager.xul @@ -0,0 +1,106 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebuggerManager" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebuggerManager_worker.js"; + const CHILD_WORKER_URL = "WorkerDebuggerManager_childWorker.js"; + + function test() { + Task.spawn(function* () { + SimpleTest.waitForExplicitFinish(); + + info("Check that worker debuggers are not enumerated before they are " + + "registered."); + ok(!findDebugger(WORKER_URL), + "Worker debugger should not be enumerated before it is registered."); + ok(!findDebugger(CHILD_WORKER_URL), + "Child worker debugger should not be enumerated before it is " + + "registered."); + + info("Create a worker that creates a child worker, and wait for " + + "their debuggers to be registered."); + let promise = waitForMultiple([ + waitForRegister(WORKER_URL), + waitForRegister(CHILD_WORKER_URL) + ]); + let worker = new Worker(WORKER_URL); + let [dbg, childDbg] = yield promise; + + info("Check that worker debuggers are enumerated after they are " + + "registered."); + ok(findDebugger(WORKER_URL), + "Worker debugger should be enumerated after it is registered."); + ok(findDebugger(CHILD_WORKER_URL), + "Child worker debugger should be enumerated after it is " + + "registered."); + + info("Check that worker debuggers are not closed before they are " + + "unregistered."); + is(dbg.isClosed, false, + "Worker debugger should not be closed before it is unregistered."); + is(childDbg.isClosed, false, + "Child worker debugger should not be closed before it is " + + "unregistered"); + + info("Terminate the worker and the child worker, and wait for their " + + "debuggers to be unregistered."); + promise = waitForMultiple([ + waitForUnregister(CHILD_WORKER_URL), + waitForUnregister(WORKER_URL), + ]); + worker.terminate(); + yield promise; + + info("Check that worker debuggers are not enumerated after they are " + + "unregistered."); + ok(!findDebugger(WORKER_URL), + "Worker debugger should not be enumerated after it is " + + "unregistered."); + ok(!findDebugger(CHILD_WORKER_URL), + "Child worker debugger should not be enumerated after it is " + + "unregistered."); + + info("Check that worker debuggers are closed after they are " + + "unregistered."); + is(dbg.isClosed, true, + "Worker debugger should be closed after it is unregistered."); + is(childDbg.isClosed, true, + "Child worker debugger should be closed after it is unregistered."); + + info("Check that property accesses on worker debuggers throws " + + "after they are closed."); + assertThrows(() => dbg.url, + "Property accesses on worker debugger should throw " + + "after it is closed."); + assertThrows(() => childDbg.url, + "Property accesses on child worker debugger should " + + "throw after it is closed."); + + SimpleTest.finish(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebugger_console.xul b/dom/workers/test/test_WorkerDebugger_console.xul new file mode 100644 index 000000000..0852002ea --- /dev/null +++ b/dom/workers/test/test_WorkerDebugger_console.xul @@ -0,0 +1,97 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebuggerGlobalScope.console methods" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebugger.console_worker.js"; + const CHILD_WORKER_URL = "WorkerDebugger.console_childWorker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebugger.console_debugger.js"; + + consoleMessagesReceived = 0; + function test() { + function consoleListener() { + SpecialPowers.addObserver(this, "console-api-log-event", false); + } + + consoleListener.prototype = { + observe: function(aSubject, aTopic, aData) { + if (aTopic == "console-api-log-event") { + var obj = aSubject.wrappedJSObject; + if (obj.arguments[0] == "Hello from the debugger script!" && + !consoleMessagesReceived) { + consoleMessagesReceived++; + ok(true, "Something has been received"); + SpecialPowers.removeObserver(this, "console-api-log-event"); + } + } + } + } + + var cl = new consoleListener(); + + Task.spawn(function* () { + SimpleTest.waitForExplicitFinish(); + + info("Create a worker that creates a child worker, wait for their " + + "debuggers to be registered, and initialize them."); + let promise = waitForMultiple([ + waitForRegister(WORKER_URL, DEBUGGER_URL), + waitForRegister(CHILD_WORKER_URL, DEBUGGER_URL) + ]); + let worker = new Worker(WORKER_URL); + let [dbg, childDbg] = yield promise; + + info("Send a request to the worker debugger. This should cause the " + + "the worker debugger to send a response."); + dbg.addListener({ + onMessage: function(msg) { + try { + msg = JSON.parse(msg); + } catch(e) { + ok(false, "Something went wrong"); + return; + } + + if (msg.type == 'finish') { + ok(consoleMessagesReceived, "We received something via debugger console!"); + dbg.removeListener(this); + SimpleTest.finish(); + return; + } + + if (msg.type == 'status') { + ok(msg.what, msg.msg); + return; + } + + ok(false, "Something went wrong"); + } + }); + + dbg.postMessage("do magic"); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebugger_frozen.xul b/dom/workers/test/test_WorkerDebugger_frozen.xul new file mode 100644 index 000000000..6b22e7702 --- /dev/null +++ b/dom/workers/test/test_WorkerDebugger_frozen.xul @@ -0,0 +1,90 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebugger with frozen workers" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const CACHE_SUBFRAMES = "browser.sessionhistory.cache_subframes"; + const MAX_TOTAL_VIEWERS = "browser.sessionhistory.max_total_viewers"; + + const IFRAME1_URL = "WorkerDebugger_frozen_iframe1.html"; + const IFRAME2_URL = "WorkerDebugger_frozen_iframe2.html"; + + const WORKER1_URL = "WorkerDebugger_frozen_worker1.js"; + const WORKER2_URL = "WorkerDebugger_frozen_worker2.js"; + + function test() { + Task.spawn(function* () { + SimpleTest.waitForExplicitFinish(); + + var oldMaxTotalViewers = SpecialPowers.getIntPref(MAX_TOTAL_VIEWERS); + + SpecialPowers.setBoolPref(CACHE_SUBFRAMES, true); + SpecialPowers.setIntPref(MAX_TOTAL_VIEWERS, 10); + + let iframe = $("iframe"); + + let promise = waitForMultiple([ + waitForRegister(WORKER1_URL), + waitForWindowMessage(window, "ready"), + ]); + iframe.src = IFRAME1_URL; + let [dbg1] = yield promise; + is(dbg1.isClosed, false, + "debugger for worker on page 1 should not be closed"); + + promise = waitForMultiple([ + waitForUnregister(WORKER1_URL), + waitForDebuggerClose(dbg1), + waitForRegister(WORKER2_URL), + waitForWindowMessage(window, "ready"), + ]); + iframe.src = IFRAME2_URL; + let [,, dbg2] = yield promise; + is(dbg1.isClosed, true, + "debugger for worker on page 1 should be closed"); + is(dbg2.isClosed, false, + "debugger for worker on page 2 should not be closed"); + + promise = Promise.all([ + waitForUnregister(WORKER2_URL), + waitForDebuggerClose(dbg2), + waitForRegister(WORKER1_URL) + ]); + iframe.contentWindow.history.back(); + [,, dbg1] = yield promise; + is(dbg1.isClosed, false, + "debugger for worker on page 1 should not be closed"); + is(dbg2.isClosed, true, + "debugger for worker on page 2 should be closed"); + + SpecialPowers.clearUserPref(CACHE_SUBFRAMES); + SpecialPowers.setIntPref(MAX_TOTAL_VIEWERS, oldMaxTotalViewers); + + SimpleTest.finish(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + <iframe id="iframe"></iframe> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebugger_promise.xul b/dom/workers/test/test_WorkerDebugger_promise.xul new file mode 100644 index 000000000..24ed07133 --- /dev/null +++ b/dom/workers/test/test_WorkerDebugger_promise.xul @@ -0,0 +1,70 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. ++ http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebugger with DOM Promises" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebugger_promise_worker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebugger_promise_debugger.js"; + + function test() { + Task.spawn(function* () { + SimpleTest.waitForExplicitFinish(); + + let promise = waitForRegister(WORKER_URL, DEBUGGER_URL); + let worker = new Worker(WORKER_URL); + let dbg = yield promise; + + info("Send a request to the worker. This should cause the worker " + + "to send a response."); + promise = waitForWorkerMessage(worker, "resolved"); + worker.postMessage("resolve"); + yield promise; + + info("Send a request to the debugger. This should cause the debugger " + + "to send a response."); + promise = waitForDebuggerMessage(dbg, "resolved"); + dbg.postMessage("resolve"); + yield promise; + + info("Send a request to the worker. This should cause the debugger " + + "to enter a nested event loop."); + promise = waitForDebuggerMessage(dbg, "paused"); + worker.postMessage("pause"); + yield promise; + + info("Send a request to the debugger. This should cause the debugger " + + "to leave the nested event loop."); + promise = waitForMultiple([ + waitForDebuggerMessage(dbg, "resumed"), + waitForWorkerMessage(worker, "resumed") + ]); + dbg.postMessage("resume"); + yield promise; + + SimpleTest.finish(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_WorkerDebugger_suspended.xul b/dom/workers/test/test_WorkerDebugger_suspended.xul new file mode 100644 index 000000000..0ed8bb71a --- /dev/null +++ b/dom/workers/test/test_WorkerDebugger_suspended.xul @@ -0,0 +1,75 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for WorkerDebugger with suspended workers" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + const WORKER_URL = "WorkerDebugger_suspended_worker.js"; + const DEBUGGER_URL = BASE_URL + "WorkerDebugger_suspended_debugger.js"; + + function test() { + Task.spawn(function* () { + SimpleTest.waitForExplicitFinish(); + + info("Create a worker, wait for its debugger to be registered, and " + + "initialize it."); + let promise = waitForRegister(WORKER_URL, DEBUGGER_URL); + let worker = new Worker(WORKER_URL); + let dbg = yield promise; + + info("Send a request to the worker. This should cause both the " + + "worker and the worker debugger to send a response."); + promise = waitForMultiple([ + waitForWorkerMessage(worker, "worker"), + waitForDebuggerMessage(dbg, "debugger") + ]); + worker.postMessage("ping"); + yield promise; + + info("Suspend the workers for this window, and send another request " + + "to the worker. This should cause only the worker debugger to " + + "send a response."); + let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + windowUtils.suspendTimeouts(); + function onmessage() { + ok(false, "The worker should not send a response."); + }; + worker.addEventListener("message", onmessage); + promise = waitForDebuggerMessage(dbg, "debugger"); + worker.postMessage("ping"); + yield promise; + worker.removeEventListener("message", onmessage); + + info("Resume the workers for this window. This should cause the " + + "worker to send a response to the previous request."); + promise = waitForWorkerMessage(worker, "worker"); + windowUtils.resumeTimeouts(); + yield promise; + + SimpleTest.finish(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_atob.html b/dom/workers/test/test_atob.html new file mode 100644 index 000000000..99419174c --- /dev/null +++ b/dom/workers/test/test_atob.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM Worker Threads</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script src="atob_worker.js" language="javascript"></script> +<script class="testbody" type="text/javascript"> + + var dataIndex = 0; + + var worker = new Worker("atob_worker.js"); + worker.onmessage = function(event) { + switch (event.data.type) { + case "done": + is(dataIndex, data.length, "Saw all values"); + SimpleTest.finish(); + return; + case "btoa": + is(btoa(data[dataIndex]), event.data.value, + "Good btoa value " + dataIndex); + break; + case "atob": + is(atob(btoa(data[dataIndex])) + "", event.data.value, + "Good round trip value " + dataIndex); + dataIndex++; + break; + default: + ok(false, "Worker posted a bad message: " + event.message); + worker.terminate(); + SimpleTest.finish(); + } + } + + worker.onerror = function(event) { + ok(false, "Worker threw an error: " + event.message); + worker.terminate(); + SimpleTest.finish(); + } + + worker.postMessage("go"); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_blobConstructor.html b/dom/workers/test/test_blobConstructor.html new file mode 100644 index 000000000..e8cfaf19d --- /dev/null +++ b/dom/workers/test/test_blobConstructor.html @@ -0,0 +1,60 @@ +<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE html>
+<html>
+<!--
+Tests of DOM Worker Blob constructor
+-->
+<head>
+ <title>Test for DOM Worker Blob constructor</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+(function() {
+ onerror = function(e) {
+ ok(false, "Main Thread had an error: " + event.data);
+ SimpleTest.finish();
+ };
+ function f() {
+ onmessage = function(e) {
+ var b = new Blob([e.data, "World"],{type: "text/plain"});
+ var fr = new FileReaderSync();
+ postMessage({text: fr.readAsText(b), type: b.type});
+ };
+ }
+ var b = new Blob([f,"f();"]);
+ var u = URL.createObjectURL(b);
+ var w = new Worker(u);
+ w.onmessage = function(e) {
+ URL.revokeObjectURL(u);
+ is(e.data.text, fr.result);
+ is(e.data.type, "text/plain");
+ SimpleTest.finish();
+ };
+ w.onerror = function(e) {
+ is(e.target, w);
+ ok(false, "Worker had an error: " + e.message);
+ SimpleTest.finish();
+ };
+
+ b = new Blob(["Hello, "]);
+ var fr = new FileReader();
+ fr.readAsText(new Blob([b, "World"],{}));
+ fr.onload = function() {
+ w.postMessage(b);
+ };
+ SimpleTest.waitForExplicitFinish();
+})();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/workers/test/test_blobWorkers.html b/dom/workers/test/test_blobWorkers.html new file mode 100644 index 000000000..6e2a83f53 --- /dev/null +++ b/dom/workers/test/test_blobWorkers.html @@ -0,0 +1,32 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <script type="text/javascript"> + const message = "hi"; + + const workerScript = + "onmessage = function(event) {" + + " postMessage(event.data);" + + "};"; + + var worker = new Worker(URL.createObjectURL(new Blob([workerScript]))); + worker.onmessage = function(event) { + is(event.data, message, "Got correct message"); + SimpleTest.finish(); + }; + worker.postMessage(message); + + SimpleTest.waitForExplicitFinish(); + </script> + </body> +</html> + diff --git a/dom/workers/test/test_bug1002702.html b/dom/workers/test/test_bug1002702.html new file mode 100644 index 000000000..3db6d2580 --- /dev/null +++ b/dom/workers/test/test_bug1002702.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 1002702</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + +var port = new SharedWorker('data:application/javascript,1').port; +port.close(); +SpecialPowers.forceGC(); +ok(true, "No crash \\o/"); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/test_bug1010784.html b/dom/workers/test/test_bug1010784.html new file mode 100644 index 000000000..f746f35e6 --- /dev/null +++ b/dom/workers/test/test_bug1010784.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1010784 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1010784</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1010784">Mozilla Bug 1010784</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + + var worker = new Worker("file_bug1010784_worker.js"); + + worker.onmessage = function(event) { + is(event.data, "done", "Got correct result"); + SimpleTest.finish(); + } + + worker.postMessage("testXHR.txt"); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1014466.html b/dom/workers/test/test_bug1014466.html new file mode 100644 index 000000000..59f5a6185 --- /dev/null +++ b/dom/workers/test/test_bug1014466.html @@ -0,0 +1,42 @@ +<!-- +2 Any copyright is dedicated to the Public Domain. +3 http://creativecommons.org/publicdomain/zero/1.0/ +4 --> +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1014466 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1014466</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1014466">Mozilla Bug 1014466</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + + var worker = new Worker("bug1014466_worker.js"); + + worker.onmessage = function(event) { + if (event.data.type == 'finish') { + SimpleTest.finish(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } + }; + + worker.postMessage(true); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1020226.html b/dom/workers/test/test_bug1020226.html new file mode 100644 index 000000000..b6db2aeb4 --- /dev/null +++ b/dom/workers/test/test_bug1020226.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1020226 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1020226</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1020226">Mozilla Bug 1020226</a> +<p id="display"></p> +<div id="content" style="display: none"> + +<iframe id="iframe" src="bug1020226_frame.html" onload="finishTest();"> +</iframe> +</div> +<pre id="test"> +<script type="application/javascript"> +function finishTest() { + document.getElementById("iframe").onload = null; + window.onmessage = function(e) { + info("Got message"); + document.getElementById("iframe").src = "about:blank"; + // We aren't really interested in the test, it shouldn't crash when the + // worker is GCed later. + ok(true, "Should not crash"); + SimpleTest.finish(); + }; +} + +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1036484.html b/dom/workers/test/test_bug1036484.html new file mode 100644 index 000000000..17b9d490f --- /dev/null +++ b/dom/workers/test/test_bug1036484.html @@ -0,0 +1,54 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads: bug 1036484 +--> +<head> + <title>Test for DOM Worker Threads: bug 1036484</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function test(script) { + var worker = new Worker(script); + + worker.onmessage = function(event) { + ok(false, "Shouldn't ever get a message!"); + } + + worker.onerror = function(event) { + is(event.target, worker); + ok(event.message.startsWith("NetworkError: Failed to load worker script")) + event.preventDefault(); + runTests(); + }; + + worker.postMessage("dummy"); +} + +var tests = [ '404_server.sjs', '404_server.sjs?js' ]; +function runTests() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var script = tests.shift(); + test(script); +} + +SimpleTest.waitForExplicitFinish(); +runTests(); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/test_bug1060621.html b/dom/workers/test/test_bug1060621.html new file mode 100644 index 000000000..758bf996e --- /dev/null +++ b/dom/workers/test/test_bug1060621.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for URLSearchParams object in workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("bug1060621_worker.js"); + + worker.onmessage = function(event) { + ok(true, "The operation is done. We should not leak."); + SimpleTest.finish(); + }; + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1062920.html b/dom/workers/test/test_bug1062920.html new file mode 100644 index 000000000..31061a2b1 --- /dev/null +++ b/dom/workers/test/test_bug1062920.html @@ -0,0 +1,70 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for navigator property override</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function checkValues() { + var worker = new Worker("bug1062920_worker.js"); + + worker.onmessage = function(event) { + var ifr = document.createElement('IFRAME'); + ifr.src = "about:blank"; + + ifr.addEventListener('load', function() { + var nav = ifr.contentWindow.navigator; + is(event.data.appCodeName, nav.appCodeName, "appCodeName should match"); + is(event.data.appName, nav.appName, "appName should match"); + is(event.data.appVersion, nav.appVersion, "appVersion should match"); + is(event.data.platform, nav.platform, "platform should match"); + is(event.data.userAgent, nav.userAgent, "userAgent should match"); + is(event.data.product, nav.product, "product should match"); + runTests(); + }, false); + + document.getElementById('content').appendChild(ifr); + }; + } + + function replaceAndCheckValues() { + SpecialPowers.pushPrefEnv({"set": [ + ["general.appname.override", "appName overridden"], + ["general.appversion.override", "appVersion overridden"], + ["general.platform.override", "platform overridden"], + ["general.useragent.override", "userAgent overridden"] + ]}, checkValues); + } + + var tests = [ + checkValues, + replaceAndCheckValues + ]; + + function runTests() { + if (tests.length == 0) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); + } + + SimpleTest.waitForExplicitFinish(); + runTests(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1062920.xul b/dom/workers/test/test_bug1062920.xul new file mode 100644 index 000000000..635c1b9f9 --- /dev/null +++ b/dom/workers/test/test_bug1062920.xul @@ -0,0 +1,69 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="DOM Worker Threads Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + + function checkValues() { + var worker = new Worker("bug1062920_worker.js"); + + worker.onmessage = function(event) { + is(event.data.appCodeName, navigator.appCodeName, "appCodeName should match"); + is(event.data.appName, navigator.appName, "appName should match"); + isnot(event.data.appName, "appName overridden", "appName is not overridden"); + is(event.data.appVersion, navigator.appVersion, "appVersion should match"); + isnot(event.data.appVersion, "appVersion overridden", "appVersion is not overridden"); + is(event.data.platform, navigator.platform, "platform should match"); + isnot(event.data.platform, "platform overridden", "platform is not overridden"); + is(event.data.userAgent, navigator.userAgent, "userAgent should match"); + is(event.data.product, navigator.product, "product should match"); + runTests(); + }; + } + + function replaceAndCheckValues() { + SpecialPowers.pushPrefEnv({"set": [ + ["general.appname.override", "appName overridden"], + ["general.appversion.override", "appVersion overridden"], + ["general.platform.override", "platform overridden"], + ["general.useragent.override", "userAgent overridden"] + ]}, checkValues); + } + + var tests = [ + replaceAndCheckValues, + checkValues + ]; + + function runTests() { + if (tests.length == 0) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); + } + + SimpleTest.waitForExplicitFinish(); + runTests(); + + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_bug1063538.html b/dom/workers/test/test_bug1063538.html new file mode 100644 index 000000000..7c32b8ed3 --- /dev/null +++ b/dom/workers/test/test_bug1063538.html @@ -0,0 +1,49 @@ +<!-- +2 Any copyright is dedicated to the Public Domain. +3 http://creativecommons.org/publicdomain/zero/1.0/ +4 --> +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1063538 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1063538</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1063538">Mozilla Bug 1063538</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +function runTest() { + var worker = new Worker("bug1063538_worker.js"); + + worker.onmessage = function(e) { + if (e.data.type == 'finish') { + ok(e.data.progressFired, "Progress was fired."); + SimpleTest.finish(); + } + }; + + worker.postMessage(true); +} + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + SpecialPowers.pushPrefEnv({"set": [["network.jar.block-remote-files", false]]}, function() { + SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], runTest); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1104064.html b/dom/workers/test/test_bug1104064.html new file mode 100644 index 000000000..9f83fd007 --- /dev/null +++ b/dom/workers/test/test_bug1104064.html @@ -0,0 +1,28 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 1104064</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> + +var worker = new Worker("bug1104064_worker.js"); +worker.onmessage = function() { + ok(true, "setInterval has been called twice."); + SimpleTest.finish(); +} +worker.postMessage("go"); + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/test_bug1132395.html b/dom/workers/test/test_bug1132395.html new file mode 100644 index 000000000..30ca9b0ae --- /dev/null +++ b/dom/workers/test/test_bug1132395.html @@ -0,0 +1,40 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for 1132395</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script class="testbody" type="text/javascript"> + +// This test is full of dummy debug messages. This is because I need to follow +// an hard-to-reproduce timeout failure. + +info("test started"); +var sw = new SharedWorker('bug1132395_sharedWorker.js'); +sw.port.onmessage = function(event) { + info("sw.onmessage received"); + ok(true, "We didn't crash."); + SimpleTest.finish(); +} + +sw.onerror = function(event) { + ok(false, "Failed to create a ServiceWorker"); + SimpleTest.finish(); +} + +info("sw.postmessage called"); +sw.port.postMessage('go'); + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1132924.html b/dom/workers/test/test_bug1132924.html new file mode 100644 index 000000000..8c54813ef --- /dev/null +++ b/dom/workers/test/test_bug1132924.html @@ -0,0 +1,28 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for 1132924</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +var w = new Worker('bug1132924_worker.js'); +w.onmessage = function(event) { + ok(true, "We are still alive."); + SimpleTest.finish(); +} + +w.postMessage('go'); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug1278777.html b/dom/workers/test/test_bug1278777.html new file mode 100644 index 000000000..a91902d26 --- /dev/null +++ b/dom/workers/test/test_bug1278777.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1278777 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1278777</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1278777">Mozilla Bug 1278777</a> + <script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var worker = new Worker('worker_bug1278777.js'); +worker.onerror = function() { + ok(false, "We should not see any error."); + SimpleTest.finish(); +} + +worker.onmessage = function(e) { + ok(e.data, "Everything seems ok."); + SimpleTest.finish(); +} + + </script> +</body> +</html> diff --git a/dom/workers/test/test_bug1301094.html b/dom/workers/test/test_bug1301094.html new file mode 100644 index 000000000..ea396b32e --- /dev/null +++ b/dom/workers/test/test_bug1301094.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1301094 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1301094</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1301094">Mozilla Bug 1301094</a> + <input id="file" type="file"></input> + <script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var url = SimpleTest.getTestFileURL("script_createFile.js"); +script = SpecialPowers.loadChromeScript(url); + +var mainThreadOk, workerOk; + +function maybeFinish() { + if (mainThreadOk & workerOk) { + SimpleTest.finish(); + } +} + +function onOpened(message) { + var input = document.getElementById('file'); + SpecialPowers.wrap(input).mozSetDndFilesAndDirectories([message.data]); + + var worker = new Worker('worker_bug1301094.js'); + worker.onerror = function() { + ok(false, "We should not see any error."); + SimpleTest.finish(); + } + + worker.onmessage = function(e) { + ok(e.data, "Everything seems OK on the worker-side."); + + workerOk = true; + maybeFinish(); + } + + is(input.files.length, 1, "We have something"); + ok(input.files[0] instanceof Blob, "We have one Blob"); + worker.postMessage(input.files[0]); + + var xhr = new XMLHttpRequest(); + xhr.open("POST", 'worker_bug1301094.js', false); + xhr.onload = function() { + ok(xhr.responseText, "Everything seems OK on the main-thread-side."); + mainThreadOk = true; + maybeFinish(); + }; + + var fd = new FormData(); + fd.append('file', input.files[0]); + xhr.send(fd); +} + +script.addMessageListener("file.opened", onOpened); +script.sendAsyncMessage("file.open"); + + </script> +</body> +</html> diff --git a/dom/workers/test/test_bug1317725.html b/dom/workers/test/test_bug1317725.html new file mode 100644 index 000000000..c23587318 --- /dev/null +++ b/dom/workers/test/test_bug1317725.html @@ -0,0 +1,62 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 1317725</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<input type="file" id="file" /> + +<script type="text/js-worker" id="worker-src"> +onmessage = function(e) { + var data = new FormData(); + data.append('Filedata', e.data.slice(0, 127), encodeURI(e.data.name)); + var xhr = new XMLHttpRequest(); + xhr.open('POST', location.href, false); + xhr.send(data); + postMessage("No crash \\o/"); +} +</script> + +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var url = SimpleTest.getTestFileURL("script_createFile.js"); +script = SpecialPowers.loadChromeScript(url); + +function onOpened(message) { + var input = document.getElementById('file'); + SpecialPowers.wrap(input).mozSetFileArray([message.data]); + + var blob = new Blob([ document.getElementById("worker-src").textContent ], + { type: "text/javascript" }); + var worker = new Worker(URL.createObjectURL(blob)); + worker.onerror = function(e) { + ok(false, "We should not see any error."); + SimpleTest.finish(); + } + + worker.onmessage = function(e) { + ok(e.data, "Everything seems OK on the worker-side."); + SimpleTest.finish(); + } + + is(input.files.length, 1, "We have something"); + ok(input.files[0] instanceof Blob, "We have one Blob"); + worker.postMessage(input.files[0]); +} + +script.addMessageListener("file.opened", onOpened); +script.sendAsyncMessage("file.open"); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug949946.html b/dom/workers/test/test_bug949946.html new file mode 100644 index 000000000..547bdbda4 --- /dev/null +++ b/dom/workers/test/test_bug949946.html @@ -0,0 +1,26 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 949946</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + +new SharedWorker('sharedWorker_sharedWorker.js'); +new SharedWorker('sharedWorker_sharedWorker.js', ':'); +new SharedWorker('sharedWorker_sharedWorker.js', '|||'); +ok(true, "3 SharedWorkers created!"); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug978260.html b/dom/workers/test/test_bug978260.html new file mode 100644 index 000000000..49e84c659 --- /dev/null +++ b/dom/workers/test/test_bug978260.html @@ -0,0 +1,35 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM Worker Threads</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + SimpleTest.waitForExplicitFinish(); + + var xhr = new XMLHttpRequest(); + xhr.onload = function () { + var worker = new Worker("bug978260_worker.js"); + worker.onmessage = function(event) { + is(event.data, "loaded"); + SimpleTest.finish(); + } + } + + xhr.open('GET', '/', false); + xhr.send(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_bug998474.html b/dom/workers/test/test_bug998474.html new file mode 100644 index 000000000..892b42ef9 --- /dev/null +++ b/dom/workers/test/test_bug998474.html @@ -0,0 +1,40 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 998474</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="boom();"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + +function boom() +{ + var worker = new SharedWorker("bug998474_worker.js"); + + setTimeout(function() { + port = worker.port; + port.postMessage(""); + + setTimeout(function() { + port.start(); + ok(true, "Still alive!"); + SimpleTest.finish(); + }, 150); + }, 150); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_chromeWorker.html b/dom/workers/test/test_chromeWorker.html new file mode 100644 index 000000000..644593949 --- /dev/null +++ b/dom/workers/test/test_chromeWorker.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Test for DOM Worker Threads</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + try { + var worker = new ChromeWorker("simpleThread_worker.js"); + ok(false, "ChromeWorker constructor should be blocked!"); + } + catch (e) { + ok(true, "ChromeWorker constructor wasn't blocked!"); + } + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/test_chromeWorker.xul b/dom/workers/test/test_chromeWorker.xul new file mode 100644 index 000000000..35df768be --- /dev/null +++ b/dom/workers/test/test_chromeWorker.xul @@ -0,0 +1,45 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="DOM Worker Threads Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + function test() + { + waitForWorkerFinish(); + + var worker = new ChromeWorker("chromeWorker_worker.js"); + worker.onmessage = function(event) { + is(event.data, "Done!", "Wrong message!"); + finish(); + } + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + worker.terminate(); + finish(); + } + worker.postMessage("go"); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_chromeWorkerJSM.xul b/dom/workers/test/test_chromeWorkerJSM.xul new file mode 100644 index 000000000..01a88bc2a --- /dev/null +++ b/dom/workers/test/test_chromeWorkerJSM.xul @@ -0,0 +1,56 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="DOM Worker Threads Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + function test() + { + waitForWorkerFinish(); + + var worker; + + function done() + { + worker = null; + finish(); + } + + function messageCallback(event) { + is(event.data, "Done", "Correct message"); + done(); + } + + function errorCallback(event) { + ok(false, "Worker had an error: " + event.message); + done(); + } + + Components.utils.import("chrome://mochitests/content/chrome/dom/workers/test/WorkerTest.jsm"); + + worker = WorkerTest.go(window.location.href, messageCallback, + errorCallback); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_clearTimeouts.html b/dom/workers/test/test_clearTimeouts.html new file mode 100644 index 000000000..d9bc6a9f9 --- /dev/null +++ b/dom/workers/test/test_clearTimeouts.html @@ -0,0 +1,31 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM Worker Threads</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + new Worker("clearTimeouts_worker.js").onmessage = function(event) { + event.target.terminate(); + + is(event.data, "ready", "Correct message"); + setTimeout(function() { SimpleTest.finish(); }, 1000); + } + + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("untriaged"); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_console.html b/dom/workers/test/test_console.html new file mode 100644 index 000000000..af82a7de0 --- /dev/null +++ b/dom/workers/test/test_console.html @@ -0,0 +1,44 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Console +--> +<head> + <title>Test for DOM Worker Console</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + var worker = new Worker("console_worker.js"); + + worker.onmessage = function(event) { + is(event.target, worker, "Worker and target match!"); + ok(event.data.status, event.data.event); + + if (!event.data.status || event.data.last) + SimpleTest.finish(); + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + } + + worker.postMessage(true); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_consoleAndBlobs.html b/dom/workers/test/test_consoleAndBlobs.html new file mode 100644 index 000000000..e765500fa --- /dev/null +++ b/dom/workers/test/test_consoleAndBlobs.html @@ -0,0 +1,43 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for console API and blobs</title> + <script src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + </head> + <body> + <script type="text/javascript"> + + function consoleListener() { + SpecialPowers.addObserver(this, "console-api-log-event", false); + } + + var order = 0; + consoleListener.prototype = { + observe: function(aSubject, aTopic, aData) { + ok(true, "Something has been received"); + is(aTopic, "console-api-log-event"); + + var obj = aSubject.wrappedJSObject; + if (obj.arguments[0] && obj.arguments[0].msg === 'consoleAndBlobs') { + SpecialPowers.removeObserver(this, "console-api-log-event"); + is(obj.arguments[0].blob.size, 3, "The size is correct"); + is(obj.arguments[0].blob.type, 'foo/bar', "The type is correct"); + SimpleTest.finish(); + } + } + } + + var cl = new consoleListener(); + + new Worker('worker_consoleAndBlobs.js'); + SimpleTest.waitForExplicitFinish(); + + </script> + </body> +</html> diff --git a/dom/workers/test/test_consoleReplaceable.html b/dom/workers/test/test_consoleReplaceable.html new file mode 100644 index 000000000..3886b679d --- /dev/null +++ b/dom/workers/test/test_consoleReplaceable.html @@ -0,0 +1,44 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Console +--> +<head> + <title>Test for DOM Worker Console</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + var worker = new Worker("consoleReplaceable_worker.js"); + + worker.onmessage = function(event) { + is(event.target, worker, "Worker and target match!"); + ok(event.data.status, event.data.event); + + if (event.data.last) + SimpleTest.finish(); + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + } + + worker.postMessage(true); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_consoleSharedWorkers.html b/dom/workers/test/test_consoleSharedWorkers.html new file mode 100644 index 000000000..74e1ec742 --- /dev/null +++ b/dom/workers/test/test_consoleSharedWorkers.html @@ -0,0 +1,56 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for console API in SharedWorker</title> + <script src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + </head> + <body> + <script type="text/javascript"> + + function consoleListener() { + SpecialPowers.addObserver(this, "console-api-log-event", false); + SpecialPowers.addObserver(this, "console-api-profiler", false); + } + + var order = 0; + consoleListener.prototype = { + observe: function(aSubject, aTopic, aData) { + ok(true, "Something has been received"); + + if (aTopic == "console-api-profiler") { + var obj = aSubject.wrappedJSObject; + is (obj.arguments[0], "Hello profiling from a SharedWorker!", "A message from a SharedWorker \\o/"); + is (order++, 0, "First a profiler message."); + + SpecialPowers.removeObserver(this, "console-api-profiler"); + return; + } + + if (aTopic == "console-api-log-event") { + var obj = aSubject.wrappedJSObject; + is (obj.arguments[0], "Hello world from a SharedWorker!", "A message from a SharedWorker \\o/"); + is (obj.ID, "http://mochi.test:8888/tests/dom/workers/test/sharedWorker_console.js", "The ID is SharedWorker"); + is (obj.innerID, "SharedWorker", "The ID is SharedWorker"); + is (order++, 1, "Then a log message."); + + SpecialPowers.removeObserver(this, "console-api-log-event"); + SimpleTest.finish(); + return; + } + } + } + + var cl = new consoleListener(); + new SharedWorker('sharedWorker_console.js'); + + SimpleTest.waitForExplicitFinish(); + + </script> + </body> +</html> diff --git a/dom/workers/test/test_contentWorker.html b/dom/workers/test/test_contentWorker.html new file mode 100644 index 000000000..e745ca4a0 --- /dev/null +++ b/dom/workers/test/test_contentWorker.html @@ -0,0 +1,48 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM Worker privileged properties</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + + var workerFilename = "content_worker.js"; + var worker = new Worker(workerFilename); + + var props = { + 'ctypes': 1, + 'OS': 1 + }; + + worker.onmessage = function(event) { + if (event.data.testfinished) { + SimpleTest.finish(); + return; + } + var prop = event.data.prop; + ok(prop in props, "checking " + prop); + is(event.data.value, undefined, prop + " should be undefined"); + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_csp.html b/dom/workers/test/test_csp.html new file mode 100644 index 000000000..a24217f04 --- /dev/null +++ b/dom/workers/test/test_csp.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM Worker + CSP</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +<script type="text/javascript" src="test_csp.js"></script> +</html> diff --git a/dom/workers/test/test_csp.html^headers^ b/dom/workers/test/test_csp.html^headers^ new file mode 100644 index 000000000..1c9321079 --- /dev/null +++ b/dom/workers/test/test_csp.html^headers^ @@ -0,0 +1,2 @@ +Cache-Control: no-cache +Content-Security-Policy: default-src 'self' blob: diff --git a/dom/workers/test/test_csp.js b/dom/workers/test/test_csp.js new file mode 100644 index 000000000..dcbcd8c3a --- /dev/null +++ b/dom/workers/test/test_csp.js @@ -0,0 +1,48 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var tests = 3; + +SimpleTest.waitForExplicitFinish(); + +testDone = function(event) { + if (!--tests) SimpleTest.finish(); +} + +// Workers don't inherit CSP +worker = new Worker("csp_worker.js"); +worker.postMessage({ do: "eval" }); +worker.onmessage = function(event) { + is(event.data, 42, "Eval succeeded!"); + testDone(); +} + +// blob: workers *do* inherit CSP +xhr = new XMLHttpRequest; +xhr.open("GET", "csp_worker.js"); +xhr.responseType = "blob"; +xhr.send(); +xhr.onload = (e) => { + uri = URL.createObjectURL(e.target.response); + worker = new Worker(uri); + worker.postMessage({ do: "eval" }) + worker.onmessage = function(event) { + is(event.data, "Error: call to eval() blocked by CSP", "Eval threw"); + testDone(); + } +} + +xhr = new XMLHttpRequest; +xhr.open("GET", "csp_worker.js"); +xhr.responseType = "blob"; +xhr.send(); +xhr.onload = (e) => { + uri = URL.createObjectURL(e.target.response); + worker = new Worker(uri); + worker.postMessage({ do: "nest", uri: uri, level: 3 }) + worker.onmessage = function(event) { + is(event.data, "Error: call to eval() blocked by CSP", "Eval threw in nested worker"); + testDone(); + } +} diff --git a/dom/workers/test/test_dataURLWorker.html b/dom/workers/test/test_dataURLWorker.html new file mode 100644 index 000000000..1ff72424f --- /dev/null +++ b/dom/workers/test/test_dataURLWorker.html @@ -0,0 +1,31 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <script type="text/javascript"> + const message = "hi"; + const url = "DATA:text/plain," + + "onmessage = function(event) {" + + " postMessage(event.data);" + + "};"; + + var worker = new Worker(url); + worker.onmessage = function(event) { + is(event.data, message, "Got correct message"); + SimpleTest.finish(); + }; + worker.postMessage(message); + + SimpleTest.waitForExplicitFinish(); + </script> + </body> +</html> + diff --git a/dom/workers/test/test_errorPropagation.html b/dom/workers/test/test_errorPropagation.html new file mode 100644 index 000000000..7e1aafe25 --- /dev/null +++ b/dom/workers/test/test_errorPropagation.html @@ -0,0 +1,66 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <meta charset="utf-8"> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <iframe id="workerFrame" src="errorPropagation_iframe.html" + onload="workerFrameLoaded();"></iframe> + <script type="text/javascript"> + const workerCount = 3; + + const errorMessage = "Error: expectedError"; + const errorFilename = "http://mochi.test:8888/tests/dom/workers/test/" + + "errorPropagation_worker.js"; + const errorLineno = 48; + + var workerFrame; + + scopeErrorCount = 0; + workerErrorCount = 0; + windowErrorCount = 0; + + function messageListener(event) { + if (event.type == "scope") { + scopeErrorCount++; + } + else if (event.type == "worker") { + workerErrorCount++; + } + else if (event.type == "window") { + windowErrorCount++; + } + else { + ok(false, "Bad event type: " + event.type); + } + + is(event.data.message, errorMessage, "Correct message event.message"); + is(event.data.filename, errorFilename, + "Correct message event.filename"); + is(event.data.lineno, errorLineno, "Correct message event.lineno"); + + if (windowErrorCount == 1) { + is(scopeErrorCount, workerCount, "Good number of scope errors"); + is(workerErrorCount, workerCount, "Good number of worker errors"); + workerFrame.stop(); + SimpleTest.finish(); + } + } + + function workerFrameLoaded() { + workerFrame = document.getElementById("workerFrame").contentWindow; + workerFrame.start(workerCount, messageListener); + } + + SimpleTest.waitForExplicitFinish(); + </script> + </body> +</html> + diff --git a/dom/workers/test/test_errorwarning.html b/dom/workers/test/test_errorwarning.html new file mode 100644 index 000000000..04523c839 --- /dev/null +++ b/dom/workers/test/test_errorwarning.html @@ -0,0 +1,95 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Test javascript.options.strict in Workers +--> +<head> + <title>Test javascript.options.strict in Workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + + var errors = 0; + function errorHandler(e) { + ok(true, "An error has been received!"); + errors++; + } + + function test_noErrors() { + errors = 0; + + var worker = new Worker('errorwarning_worker.js'); + worker.onerror = errorHandler; + worker.onmessage = function(e) { + if (e.data.type == 'ignore') + return; + + if (e.data.type == 'error') { + errorHandler(); + return; + } + + if (e.data.type == 'finish') { + ok(errors == 0, "Here we are with 0 errors!"); + runTests(); + return; + } + } + + onerror = errorHandler; + worker.postMessage({ loop: 5, errors: false }); + } + + function test_errors() { + errors = 0; + + var worker = new Worker('errorwarning_worker.js'); + worker.onerror = errorHandler; + worker.onmessage = function(e) { + if (e.data.type == 'ignore') + return; + + if (e.data.type == 'error') { + errorHandler(); + return; + } + + if (e.data.type == 'finish') { + ok(errors != 0, "Here we are with errors!"); + runTests(); + return; + } + } + + onerror = errorHandler; + worker.postMessage({ loop: 5, errors: true }); + } + + var tests = [ test_noErrors, test_errors ]; + function runTests() { + var test = tests.shift(); + if (test) { + test(); + } else { + SimpleTest.finish(); + } + } + + runTests(); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_eventDispatch.html b/dom/workers/test/test_eventDispatch.html new file mode 100644 index 000000000..b3c3123f0 --- /dev/null +++ b/dom/workers/test/test_eventDispatch.html @@ -0,0 +1,33 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <script type="text/javascript"> + const message = "Hi"; + + var messageCount = 0; + + var worker = new Worker("eventDispatch_worker.js"); + worker.onmessage = function(event) { + is(event.data, message, "Got correct data."); + if (!messageCount++) { + event.target.postMessage(event.data); + return; + } + SimpleTest.finish(); + } + worker.postMessage(message); + + SimpleTest.waitForExplicitFinish(); + </script> + </body> +</html> + diff --git a/dom/workers/test/test_extension.xul b/dom/workers/test/test_extension.xul new file mode 100644 index 000000000..ba68ef03a --- /dev/null +++ b/dom/workers/test/test_extension.xul @@ -0,0 +1,55 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="DOM Worker Threads Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + function test() { + const message = "woohoo"; + + var workertest = + Cc["@mozilla.org/test/workertest;1"].createInstance(Ci.nsIWorkerTest); + + workertest.callback = { + onmessage: function(data) { + is(data, message, "Correct message"); + workertest.callback = null; + workertest = null; + SimpleTest.finish(); + }, + onerror: function(data) { + ok(false, "Worker had an error: " + data.message); + workertest.callback = null; + workertest = null; + SimpleTest.finish(); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWorkerTestCallback]) + }; + + workertest.postMessage(message); + + SimpleTest.waitForExplicitFinish(); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_extensionBootstrap.xul b/dom/workers/test/test_extensionBootstrap.xul new file mode 100644 index 000000000..18bdde1d3 --- /dev/null +++ b/dom/workers/test/test_extensionBootstrap.xul @@ -0,0 +1,66 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="DOM Worker Threads Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + + function test() { + const message = "woohoo"; + + var observer = { + observe: function(subject, topic, data) { + is(topic, "message", "Correct type of event"); + is(data, message, "Correct message"); + + AddonManager.getAddonByID("workerbootstrap-test@mozilla.org", + function(addon) { + addon.uninstall(); + + const stages = [ "install", "startup", "shutdown", "uninstall" ]; + const symbols = [ "Worker", "ChromeWorker" ]; + + for (var stage of stages) { + for (var symbol of symbols) { + is(Services.prefs.getBoolPref("workertest.bootstrap." + stage + + "." + symbol), + true, + "Symbol '" + symbol + "' present during '" + stage + "'"); + } + } + + SimpleTest.finish(); + }); + }, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]) + }; + + var workertestbootstrap = Cc["@mozilla.org/test/workertestbootstrap;1"]. + createInstance(Ci.nsIObserver); + + workertestbootstrap.observe(observer, "postMessage", message); + + SimpleTest.waitForExplicitFinish(); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/test_fibonacci.html b/dom/workers/test/test_fibonacci.html new file mode 100644 index 000000000..d93eb12d4 --- /dev/null +++ b/dom/workers/test/test_fibonacci.html @@ -0,0 +1,52 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads with Fibonacci +--> +<head> + <title>Test for DOM Worker Threads with Fibonacci</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=450452">DOM Worker Threads Fibonacci</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + const seqNum = 5; + + function recursivefib(n) { + return n < 2 ? n : recursivefib(n - 1) + recursivefib(n - 2); + } + + var worker = new Worker("fibonacci_worker.js"); + + worker.onmessage = function(event) { + is(event.target, worker); + is(event.data, recursivefib(seqNum)); + SimpleTest.finish(); + }; + + worker.onerror = function(event) { + is(event.target, worker); + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + }; + + worker.postMessage(seqNum); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/test_file.xul b/dom/workers/test/test_file.xul new file mode 100644 index 000000000..800e88fbc --- /dev/null +++ b/dom/workers/test/test_file.xul @@ -0,0 +1,97 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=123456 +--> +<window title="Mozilla Bug 123456" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=123456" + target="_blank">Mozilla Bug 123456</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 123456 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data and optionally adds the specified file extension. + */ + function createFileWithData(fileData, /** optional */ extension) { + var testFile = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties) + .get("ProfD", Components.interfaces.nsIFile); + var fileExtension = (extension == undefined) ? "" : "." + extension; + testFile.append("workerFile" + fileNum++ + fileExtension); + + var outStream = Components.classes["@mozilla.org/network/file-output-stream;1"] + .createInstance(Components.interfaces.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + /** + * Create a worker to access file properties. + */ + function accessFileProperties(file, expectedSize, expectedType) { + waitForWorkerFinish(); + + var worker = new Worker("file_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + worker.onmessage = function(event) { + is(event.data.size, expectedSize, "size proproperty accessed from worker is not the same as on main thread."); + is(event.data.type, expectedType, "type proproperty accessed from worker is incorrect."); + is(event.data.name, file.name, "name proproperty accessed from worker is incorrect."); + is(event.data.lastModifiedDate.toString(), file.lastModifiedDate.toString(), "lastModifiedDate proproperty accessed from worker is incorrect."); + finish(); + }; + + worker.postMessage(file); + } + + // Empty file. + accessFileProperties(createFileWithData(""), 0, ""); + + // Typical use case. + accessFileProperties(createFileWithData("Hello"), 5, ""); + + // Longish file. + var text = ""; + for (var i = 0; i < 10000; ++i) { + text += "long"; + } + accessFileProperties(createFileWithData(text), 40000, ""); + + // Type detection based on extension. + accessFileProperties(createFileWithData("text", "txt"), 4, "text/plain"); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_fileBlobPosting.xul b/dom/workers/test/test_fileBlobPosting.xul new file mode 100644 index 000000000..358054598 --- /dev/null +++ b/dom/workers/test/test_fileBlobPosting.xul @@ -0,0 +1,86 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=664783 +--> +<window title="Mozilla Bug 664783" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=664783" + target="_blank">Mozilla Bug 664783</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 664783 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data. + */ + function createFileWithData(fileData) { + var testFile = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties) + .get("ProfD", Components.interfaces.nsIFile); + testFile.append("workerBlobPosting" + fileNum++); + + var outStream = Components.classes["@mozilla.org/network/file-output-stream;1"] + .createInstance(Components.interfaces.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + /** + * Create a worker which posts the same blob given. Used to test cloning of blobs. + * Checks the size, type, name and path of the file posted from the worker to ensure it + * is the same as the original. + */ + function postBlob(file) { + var worker = new Worker("filePosting_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + worker.onmessage = function(event) { + console.log(event.data); + is(event.data.size, file.size, "size of file posted from worker does not match file posted to worker."); + finish(); + }; + + var blob = file.slice(); + worker.postMessage(blob); + waitForWorkerFinish(); + } + + // Empty file. + postBlob(createFileWithData("")); + + // Typical use case. + postBlob(createFileWithData("Hello")); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_fileBlobSubWorker.xul b/dom/workers/test/test_fileBlobSubWorker.xul new file mode 100644 index 000000000..6a8dba636 --- /dev/null +++ b/dom/workers/test/test_fileBlobSubWorker.xul @@ -0,0 +1,98 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=664783 +--> +<window title="Mozilla Bug 664783" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=664783" + target="_blank">Mozilla Bug 664783</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 664783 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data and optionally adds the specified file extension. + */ + function createFileWithData(fileData, /** optional */ extension) { + var testFile = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties) + .get("ProfD", Components.interfaces.nsIFile); + var fileExtension = (extension == undefined) ? "" : "." + extension; + testFile.append("workerBlobSubWorker" + fileNum++ + fileExtension); + + var outStream = Components.classes["@mozilla.org/network/file-output-stream;1"] + .createInstance(Components.interfaces.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + /** + * Create a worker to access blob properties. + */ + function accessFileProperties(file, expectedSize) { + var worker = new Worker("fileBlobSubWorker_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + worker.onmessage = function(event) { + if (event.data == undefined) { + ok(false, "Worker had an error."); + } else { + is(event.data.size, expectedSize, "size proproperty accessed from worker is not the same as on main thread."); + } + finish(); + }; + + var blob = file.slice(); + worker.postMessage(blob); + waitForWorkerFinish(); + } + + // Empty file. + accessFileProperties(createFileWithData(""), 0); + + // Typical use case. + accessFileProperties(createFileWithData("Hello"), 5); + + // Longish file. + var text = ""; + for (var i = 0; i < 10000; ++i) { + text += "long"; + } + accessFileProperties(createFileWithData(text), 40000); + + // Type detection based on extension. + accessFileProperties(createFileWithData("text", "txt"), 4); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_filePosting.xul b/dom/workers/test/test_filePosting.xul new file mode 100644 index 000000000..ff8520d7e --- /dev/null +++ b/dom/workers/test/test_filePosting.xul @@ -0,0 +1,86 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=664783 +--> +<window title="Mozilla Bug 664783" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=664783" + target="_blank">Mozilla Bug 664783</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 664783 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data. + */ + function createFileWithData(fileData) { + var testFile = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties) + .get("ProfD", Components.interfaces.nsIFile); + testFile.append("workerFilePosting" + fileNum++); + + var outStream = Components.classes["@mozilla.org/network/file-output-stream;1"] + .createInstance(Components.interfaces.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + /** + * Create a worker which posts the same file given. Used to test cloning of files. + * Checks the size, type, name and path of the file posted from the worker to ensure it + * is the same as the original. + */ + function postFile(file) { + var worker = new Worker("file_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + worker.onmessage = function(event) { + is(event.data.size, file.size, "size of file posted from worker does not match file posted to worker."); + is(event.data.type, file.type, "type of file posted from worker does not match file posted to worker."); + is(event.data.name, file.name, "name of file posted from worker does not match file posted to worker."); + finish(); + }; + + worker.postMessage(file); + waitForWorkerFinish(); + } + + // Empty file. + postFile(createFileWithData("")); + + // Typical use case. + postFile(createFileWithData("Hello")); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_fileReadSlice.xul b/dom/workers/test/test_fileReadSlice.xul new file mode 100644 index 000000000..da2e16719 --- /dev/null +++ b/dom/workers/test/test_fileReadSlice.xul @@ -0,0 +1,94 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=664783 +--> +<window title="Mozilla Bug 664783" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=664783" + target="_blank">Mozilla Bug 664783</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + if (navigator.platform.startsWith("Win")) { + SimpleTest.expectAssertions(0, 1); + } + + /** Test for Bug 664783 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data. + */ + function createFileWithData(fileData) { + var testFile = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties) + .get("ProfD", Components.interfaces.nsIFile); + testFile.append("workerReadSlice" + fileNum++); + + var outStream = Components.classes["@mozilla.org/network/file-output-stream;1"] + .createInstance(Components.interfaces.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + /** + * Creates a worker which slices a blob to the given start and end offset and + * reads the content as text. + */ + function readSlice(blob, start, end, expectedText) { + var worker = new Worker("fileReadSlice_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + worker.onmessage = function(event) { + is(event.data, expectedText, "Text from sliced blob in worker is incorrect."); + finish(); + }; + + var params = {blob: blob, start: start, end: end}; + worker.postMessage(params); + waitForWorkerFinish(); + } + + // Empty file. + readSlice(createFileWithData(""), 0, 0, ""); + + // Typical use case. + readSlice(createFileWithData("HelloBye"), 5, 8, "Bye"); + + // End offset too large. + readSlice(createFileWithData("HelloBye"), 5, 9, "Bye"); + + // Start of file. + readSlice(createFileWithData("HelloBye"), 0, 5, "Hello"); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_fileReader.html b/dom/workers/test/test_fileReader.html new file mode 100644 index 000000000..26e73bdb6 --- /dev/null +++ b/dom/workers/test/test_fileReader.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for FileReader in workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> +<script type="text/javascript;version=1.7"> + +const minFileSize = 20000; +SimpleTest.waitForExplicitFinish(); + +// Create strings containing data we'll test with. We'll want long +// strings to ensure they span multiple buffers while loading +var testTextData = "asd b\tlah\u1234w\u00a0r"; +while (testTextData.length < minFileSize) { + testTextData = testTextData + testTextData; +} + +var testASCIIData = "abcdef 123456\n"; +while (testASCIIData.length < minFileSize) { + testASCIIData = testASCIIData + testASCIIData; +} + +var testBinaryData = ""; +for (var i = 0; i < 256; i++) { + testBinaryData += String.fromCharCode(i); +} +while (testBinaryData.length < minFileSize) { + testBinaryData = testBinaryData + testBinaryData; +} + +var dataurldata0 = testBinaryData.substr(0, testBinaryData.length - + testBinaryData.length % 3); +var dataurldata1 = testBinaryData.substr(0, testBinaryData.length - 2 - + testBinaryData.length % 3); +var dataurldata2 = testBinaryData.substr(0, testBinaryData.length - 1 - + testBinaryData.length % 3); + + +//Set up files for testing +var openerURL = SimpleTest.getTestFileURL("fileapi_chromeScript.js"); +var opener = SpecialPowers.loadChromeScript(openerURL); +opener.addMessageListener("files.opened", onFilesOpened); +opener.sendAsyncMessage("files.open", [ + testASCIIData, + testBinaryData, + null, + convertToUTF8(testTextData), + convertToUTF16(testTextData), + "", + dataurldata0, + dataurldata1, + dataurldata2, +]); + +function onFilesOpened(message) { + var worker = new Worker('worker_fileReader.js'); + worker.postMessage({ blobs: message, + testTextData: testTextData, + testASCIIData: testASCIIData, + testBinaryData: testBinaryData, + dataurldata0: dataurldata0, + dataurldata1: dataurldata1, + dataurldata2: dataurldata2 }); + + worker.onmessage = function(e) { + var msg = e.data; + if (msg.type == 'finish') { + SimpleTest.finish(); + return; + } + + if (msg.type == 'check') { + ok(msg.status, msg.msg); + return; + } + + ok(false, "Unknown message."); + } +} + +function convertToUTF16(s) { + res = ""; + for (var i = 0; i < s.length; ++i) { + c = s.charCodeAt(i); + res += String.fromCharCode(c & 255, c >>> 8); + } + return res; +} + +function convertToUTF8(s) { + return unescape(encodeURIComponent(s)); +} + +</script> +</body> +</html> diff --git a/dom/workers/test/test_fileReaderSync.xul b/dom/workers/test/test_fileReaderSync.xul new file mode 100644 index 000000000..de93c3ed7 --- /dev/null +++ b/dom/workers/test/test_fileReaderSync.xul @@ -0,0 +1,199 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=664783 +--> +<window title="Mozilla Bug 664783" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=664783" + target="_blank">Mozilla Bug 664783</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 664783 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data and optionally adds the specified file extension. + */ + function createFileWithData(fileData, /** optional */ extension) { + var testFile = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties) + .get("ProfD", Components.interfaces.nsIFile); + var fileExtension = (extension == undefined) ? "" : "." + extension; + testFile.append("workerFileReaderSync" + fileNum++ + fileExtension); + + var outStream = Components.classes["@mozilla.org/network/file-output-stream;1"] + .createInstance(Components.interfaces.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + function convertToUTF16(s) { + res = ""; + for (var i = 0; i < s.length; ++i) { + c = s.charCodeAt(i); + res += String.fromCharCode(c & 255, c >>> 8); + } + return res; + } + + /** + * Converts the given string to a data URL of the specified mime type. + */ + function convertToDataURL(mime, s) { + return "data:" + mime + ";base64," + btoa(s); + } + + /** + * Create a worker to read a file containing fileData using FileReaderSync and + * checks the return type against the expected type. Optionally set an encoding + * for reading the file as text. + */ + function readFileData(fileData, expectedText, /** optional */ encoding) { + var worker = new Worker("fileReaderSync_worker.js"); + + worker.onmessage = function(event) { + is(event.data.text, expectedText, "readAsText in worker returned incorrect result."); + is(event.data.bin, fileData, "readAsBinaryString in worker returned incorrect result."); + is(event.data.url, convertToDataURL("application/octet-stream", fileData), "readAsDataURL in worker returned incorrect result."); + is(event.data.arrayBuffer.byteLength, fileData.length, "readAsArrayBuffer returned buffer of incorrect length."); + finish(); + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + var params = {file: createFileWithData(fileData), encoding: encoding}; + + worker.postMessage(params); + + waitForWorkerFinish(); + } + + /** + * Create a worker which reuses a FileReaderSync to read multiple files as DataURLs. + */ + function reuseReaderForURL(files, expected) { + var worker = new Worker("fileReaderSync_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + var k = 0; + worker.onmessage = function(event) { + is(event.data.url, expected[k], "readAsDataURL in worker returned incorrect result when reusing FileReaderSync."); + k++; + finish(); + }; + + for (var i = 0; i < files.length; ++i) { + var params = {file: files[i], encoding: undefined}; + worker.postMessage(params); + waitForWorkerFinish(); + } + } + + /** + * Create a worker which reuses a FileReaderSync to read multiple files as text. + */ + function reuseReaderForText(fileData, expected) { + var worker = new Worker("fileReaderSync_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + var k = 0; + worker.onmessage = function(event) { + is(event.data.text, expected[k++], "readAsText in worker returned incorrect result when reusing FileReaderSync."); + finish(); + }; + + for (var i = 0; i < fileData.length; ++i) { + var params = {file: createFileWithData(fileData[i]), encoding: undefined}; + worker.postMessage(params); + waitForWorkerFinish(); + } + } + + + /** + * Creates a a worker which reads a file containing fileData as an ArrayBuffer. + * Verifies that the ArrayBuffer when interpreted as a string matches the original data. + */ + function readArrayBuffer(fileData) { + var worker = new Worker("fileReaderSync_worker.js"); + + worker.onmessage = function(event) { + var view = new Uint8Array(event.data.arrayBuffer); + is(event.data.arrayBuffer.byteLength, fileData.length, "readAsArrayBuffer returned buffer of incorrect length."); + is(String.fromCharCode.apply(String, view), fileData, "readAsArrayBuffer returned buffer containing incorrect data."); + finish(); + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + var params = {file: createFileWithData(fileData), encoding: undefined}; + + worker.postMessage(params); + + waitForWorkerFinish(); + } + + // Empty file. + readFileData("", ""); + + // Typical use case. + readFileData("text", "text"); + + // Test reading UTF-16 characters. + readFileData(convertToUTF16("text"), "text", "UTF-16"); + + // First read a file of type "text/plain", then read a file of type "application/octet-stream". + reuseReaderForURL([createFileWithData("text", "txt"), createFileWithData("text")], + [convertToDataURL("text/plain", "text"), + convertToDataURL("application/octet-stream", "text")]); + + // First read UTF-16 characters marked using BOM, then read UTF-8 characters. + reuseReaderForText([convertToUTF16("\ufefftext"), "text"], + ["text", "text"]); + + // Reading data as ArrayBuffer. + readArrayBuffer(""); + readArrayBuffer("text"); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_fileReaderSyncErrors.xul b/dom/workers/test/test_fileReaderSyncErrors.xul new file mode 100644 index 000000000..9e416a603 --- /dev/null +++ b/dom/workers/test/test_fileReaderSyncErrors.xul @@ -0,0 +1,84 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=664783 +--> +<window title="Mozilla Bug 664783" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=664783" + target="_blank">Mozilla Bug 664783</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 664783 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data. + */ + function createFileWithData(fileData) { + var testFile = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties) + .get("ProfD", Components.interfaces.nsIFile); + testFile.append("workerFileReaderSyncErrors" + fileNum++); + + var outStream = Components.classes["@mozilla.org/network/file-output-stream;1"] + .createInstance(Components.interfaces.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + /** + * Creates a worker which runs errors cases. + */ + function runWorkerErrors(file) { + var worker = new Worker("fileReaderSyncErrors_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + worker.onmessage = function(event) { + if(event.data == undefined) { + // Worker returns undefined when tests have finished running. + finish(); + } else { + // Otherwise worker will return results of tests to be evaluated. + is(event.data.actual, event.data.expected, event.data.message); + } + }; + + worker.postMessage(file); + waitForWorkerFinish(); + } + + // Run worker which creates exceptions. + runWorkerErrors(createFileWithData("text")); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_fileSlice.xul b/dom/workers/test/test_fileSlice.xul new file mode 100644 index 000000000..31531da2e --- /dev/null +++ b/dom/workers/test/test_fileSlice.xul @@ -0,0 +1,106 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=664783 +--> +<window title="Mozilla Bug 664783" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=664783" + target="_blank">Mozilla Bug 664783</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 664783 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data and optionally adds the specified file extension. + */ + function createFileWithData(fileData, /** optional */ extension) { + var testFile = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties) + .get("ProfD", Components.interfaces.nsIFile); + var fileExtension = (extension == undefined) ? "" : "." + extension; + testFile.append("workerSlice" + fileNum++ + fileExtension); + + var outStream = Components.classes["@mozilla.org/network/file-output-stream;1"] + .createInstance(Components.interfaces.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + /** + * Starts a worker which slices the blob to the given start offset and optional end offset and + * content type. It then verifies that the size and type of the sliced blob is correct. + */ + function createSlice(blob, start, expectedLength, /** optional */ end, /** optional */ contentType) { + var worker = new Worker("fileSlice_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + worker.onmessage = function(event) { + is(event.data.size, expectedLength, "size property of slice is incorrect."); + is(event.data.type, contentType ? contentType : blob.type, "type property of slice is incorrect."); + finish(); + }; + + var params = {blob: blob, start: start, end: end, contentType: contentType}; + worker.postMessage(params); + waitForWorkerFinish(); + } + + // Empty file. + createSlice(createFileWithData(""), 0, 0, 0); + + // Typical use case. + createSlice(createFileWithData("Hello"), 1, 1, 2); + + // Longish file. + var text = ""; + for (var i = 0; i < 10000; ++i) { + text += "long"; + } + createSlice(createFileWithData(text), 2000, 2000, 4000); + + // Slice to different type. + createSlice(createFileWithData("text", "txt"), 0, 2, 2, "image/png"); + + // Length longer than blob. + createSlice(createFileWithData("text"), 0, 4, 20); + + // Start longer than blob. + createSlice(createFileWithData("text"), 20, 0, 4); + + // No optional arguments + createSlice(createFileWithData("text"), 0, 4); + createSlice(createFileWithData("text"), 2, 2); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_fileSubWorker.xul b/dom/workers/test/test_fileSubWorker.xul new file mode 100644 index 000000000..94b41704a --- /dev/null +++ b/dom/workers/test/test_fileSubWorker.xul @@ -0,0 +1,99 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=664783 +--> +<window title="Mozilla Bug 664783" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=664783" + target="_blank">Mozilla Bug 664783</a> + + <div id="content" style="display: none"> + <input id="fileList" type="file"></input> + </div> + + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 664783 **/ + + var fileNum = 0; + + /** + * Create a file which contains the given data and optionally adds the specified file extension. + */ + function createFileWithData(fileData, /** optional */ extension) { + var testFile = Components.classes["@mozilla.org/file/directory_service;1"] + .getService(Components.interfaces.nsIProperties) + .get("ProfD", Components.interfaces.nsIFile); + var fileExtension = (extension == undefined) ? "" : "." + extension; + testFile.append("workerSubWorker" + fileNum++ + fileExtension); + + var outStream = Components.classes["@mozilla.org/network/file-output-stream;1"] + .createInstance(Components.interfaces.nsIFileOutputStream); + outStream.init(testFile, 0x02 | 0x08 | 0x20, // write, create, truncate + 0666, 0); + outStream.write(fileData, fileData.length); + outStream.close(); + + var fileList = document.getElementById('fileList'); + fileList.value = testFile.path; + + return fileList.files[0]; + } + + /** + * Create a worker to access file properties. + */ + function accessFileProperties(file, expectedSize, expectedType) { + var worker = new Worker("fileSubWorker_worker.js"); + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + finish(); + }; + + worker.onmessage = function(event) { + if (event.data == undefined) { + ok(false, "Worker had an error."); + } else { + is(event.data.size, expectedSize, "size proproperty accessed from worker is not the same as on main thread."); + is(event.data.type, expectedType, "type proproperty accessed from worker is incorrect."); + is(event.data.name, file.name, "name proproperty accessed from worker is incorrect."); + } + finish(); + }; + + worker.postMessage(file); + waitForWorkerFinish(); + } + + // Empty file. + accessFileProperties(createFileWithData(""), 0, ""); + + // Typical use case. + accessFileProperties(createFileWithData("Hello"), 5, ""); + + // Longish file. + var text = ""; + for (var i = 0; i < 10000; ++i) { + text += "long"; + } + accessFileProperties(createFileWithData(text), 40000, ""); + + // Type detection based on extension. + accessFileProperties(createFileWithData("text", "txt"), 4, "text/plain"); + + ]]> + </script> +</window> diff --git a/dom/workers/test/test_importScripts.html b/dom/workers/test/test_importScripts.html new file mode 100644 index 000000000..718409ce3 --- /dev/null +++ b/dom/workers/test/test_importScripts.html @@ -0,0 +1,53 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads (Bug 437152) +--> +<head> + <title>Test for DOM Worker Threads (Bug 437152)</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=437152">DOM Worker Threads Bug 437152</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("importScripts_worker.js"); + + worker.onmessage = function(event) { + switch (event.data) { + case "started": + worker.postMessage("stop"); + break; + case "stopped": + ok(true, "worker correctly stopped"); + SimpleTest.finish(); + break; + default: + ok(false, "Unexpected message:" + event.data); + SimpleTest.finish(); + } + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error:" + event.message); + SimpleTest.finish(); + } + + worker.postMessage("start"); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_importScripts_3rdparty.html b/dom/workers/test/test_importScripts_3rdparty.html new file mode 100644 index 000000000..a3d73c5b5 --- /dev/null +++ b/dom/workers/test/test_importScripts_3rdparty.html @@ -0,0 +1,134 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for 3rd party imported script and muted errors</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + +const workerURL = 'http://mochi.test:8888/tests/dom/workers/test/importScripts_3rdParty_worker.js'; + +var tests = [ + function() { + var worker = new Worker("importScripts_3rdParty_worker.js"); + worker.onmessage = function(event) { + ok("result" in event.data && event.data.result, "It seems we don't share data!"); + next(); + }; + + worker.postMessage({ url: location.href, test: 'try', nested: false }); + }, + + function() { + var worker = new Worker("importScripts_3rdParty_worker.js"); + worker.onmessage = function(event) { + ok("result" in event.data && event.data.result, "It seems we don't share data in nested workers!"); + next(); + }; + + worker.postMessage({ url: location.href, test: 'try', nested: true }); + }, + + function() { + var worker = new Worker("importScripts_3rdParty_worker.js"); + worker.onmessage = function(event) { + ok("result" in event.data && event.data.result, "It seems we don't share data via eventListener!"); + next(); + }; + + worker.postMessage({ url: location.href, test: 'eventListener', nested: false }); + }, + + function() { + var worker = new Worker("importScripts_3rdParty_worker.js"); + worker.onmessage = function(event) { + ok("result" in event.data && event.data.result, "It seems we don't share data in nested workers via eventListener!"); + next(); + }; + + worker.postMessage({ url: location.href, test: 'eventListener', nested: true }); + }, + + function() { + var worker = new Worker("importScripts_3rdParty_worker.js"); + worker.onmessage = function(event) { + ok("result" in event.data && event.data.result, "It seems we don't share data via onerror!"); + next(); + }; + worker.onerror = function(event) { + event.preventDefault(); + } + + worker.postMessage({ url: location.href, test: 'onerror', nested: false }); + }, + + function() { + var worker = new Worker("importScripts_3rdParty_worker.js"); + worker.onerror = function(event) { + event.preventDefault(); + ok(event instanceof ErrorEvent, "ErrorEvent received."); + is(event.filename, workerURL, "ErrorEvent.filename is correct"); + next(); + }; + + worker.postMessage({ url: location.href, test: 'none', nested: false }); + }, + + function() { + var worker = new Worker("importScripts_3rdParty_worker.js"); + worker.addEventListener("error", function(event) { + event.preventDefault(); + ok(event instanceof ErrorEvent, "ErrorEvent received."); + is(event.filename, workerURL, "ErrorEvent.filename is correct"); + next(); + }); + + worker.postMessage({ url: location.href, test: 'none', nested: false }); + }, + + function() { + var worker = new Worker("importScripts_3rdParty_worker.js"); + worker.onerror = function(event) { + ok(false, "No error should be received!"); + }; + + worker.onmessage = function(event) { + ok("error" in event.data && event.data.error, "The error has been fully received from a nested worker"); + next(); + }; + worker.postMessage({ url: location.href, test: 'none', nested: true }); + }, + + function() { + var url = URL.createObjectURL(new Blob(["%&%^&%^"])); + var worker = new Worker(url); + worker.onerror = function(event) { + event.preventDefault(); + ok(event instanceof ErrorEvent, "ErrorEvent received."); + next(); + }; + } +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); +} + +SimpleTest.waitForExplicitFinish(); +next(); + +</script> +</body> +</html> diff --git a/dom/workers/test/test_importScripts_mixedcontent.html b/dom/workers/test/test_importScripts_mixedcontent.html new file mode 100644 index 000000000..0a7ce005c --- /dev/null +++ b/dom/workers/test/test_importScripts_mixedcontent.html @@ -0,0 +1,50 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1198078 - test that we respect mixed content blocking in importScript() inside workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1198078">DOM Worker Threads Bug 1198078</a> +<iframe></iframe> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + onmessage = function(event) { + switch (event.data.status) { + case "done": + SimpleTest.finish(); + break; + case "ok": + ok(event.data.data, event.data.msg); + break; + default: + ok(false, "Unexpected message:" + event.data); + SimpleTest.finish(); + } + }; + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.workers.sharedWorkers.enabled", true], + ["security.mixed_content.block_active_content", false], + ]}, function() { + var iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/workers/test/importScripts_mixedcontent.html"; + }); + }; + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_instanceof.html b/dom/workers/test/test_instanceof.html new file mode 100644 index 000000000..f73b3b6a7 --- /dev/null +++ b/dom/workers/test/test_instanceof.html @@ -0,0 +1,40 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker JSON messages +--> +<head> + <title>Test for DOM Worker Navigator</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script src="json_worker.js" language="javascript"></script> +<script class="testbody" language="javascript"> + + var worker = new Worker("instanceof_worker.js"); + + worker.onmessage = function(event) { + ok(event.data.status, event.data.event); + + if (event.data.last) + SimpleTest.finish(); + }; + + worker.postMessage(42); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_json.html b/dom/workers/test/test_json.html new file mode 100644 index 000000000..3a495de09 --- /dev/null +++ b/dom/workers/test/test_json.html @@ -0,0 +1,89 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker JSON messages +--> +<head> + <title>Test for DOM Worker Navigator</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script src="json_worker.js" language="javascript"></script> +<script class="testbody" language="javascript"> + + ok(messages.length, "No messages to test!"); + + var worker = new Worker("json_worker.js"); + + var index = 0; + worker.onmessage = function(event) { + var key = messages[index++]; + + // Loop for the ones we shouldn't receive. + while (key.exception) { + key = messages[index++]; + } + + is(typeof event.data, key.type, "Bad type! " + messages.indexOf(key)); + + if (key.array) { + is(event.data instanceof Array, key.array, + "Array mismatch! " + messages.indexOf(key)); + } + + if (key.isNaN) { + ok(isNaN(event.data), "Should be NaN!" + messages.indexOf(key)); + } + + if (key.isInfinity) { + is(event.data, Infinity, "Should be Infinity!" + messages.indexOf(key)); + } + + if (key.isNegativeInfinity) { + is(event.data, -Infinity, "Should be -Infinity!" + messages.indexOf(key)); + } + + if (key.shouldCompare || key.shouldEqual) { + ok(event.data == key.compareValue, + "Values don't compare! " + messages.indexOf(key)); + } + + if (key.shouldEqual) { + ok(event.data === key.compareValue, + "Values don't equal! " + messages.indexOf(key)); + } + + if (key.jsonValue) { + is(JSON.stringify(event.data), key.jsonValue, + "Object stringification inconsistent!" + messages.indexOf(key)); + } + + if (event.data == "testFinished") { + is(index, messages.length, "Didn't see the right number of messages!"); + SimpleTest.finish(); + } + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + } + + worker.postMessage("start"); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_jsversion.html b/dom/workers/test/test_jsversion.html new file mode 100644 index 000000000..495b8a3fa --- /dev/null +++ b/dom/workers/test/test_jsversion.html @@ -0,0 +1,68 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for JSVersion in workers - Bug 487070</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + + var gExpectedError = false; + + onerror = function(evt) { + ok(gExpectedError, "Error expected!"); + runTest(); + } + + function doMagic() { + var worker = new Worker('jsversion_worker.js'); + worker.onmessage = function(evt) { + ok(evt.data, 'All the tests passed'); + runTest(); + } + worker.postMessage(1); + } + + var tests = [ + // No custom version + function() { + gExpectedError = true; + SpecialPowers.pushPrefEnv({"set":[['dom.workers.latestJSVersion', false]]}, + function() { doMagic(true); }); + }, + + // Enable latest JS Version + function() { + gExpectedError = false; + SpecialPowers.pushPrefEnv({"set":[['dom.workers.latestJSVersion', true]]}, + function() { doMagic(false); }); + } + ]; + + function runTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); + } + + SimpleTest.waitForExplicitFinish(); + runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_loadEncoding.html b/dom/workers/test/test_loadEncoding.html new file mode 100644 index 000000000..47e08f2f5 --- /dev/null +++ b/dom/workers/test/test_loadEncoding.html @@ -0,0 +1,50 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 484305 - Load workers as UTF-8</title> + <meta http-equiv="content-type" content="text/html; charset=KOI8-R"> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=484305">Bug 484305 - Load workers as UTF-8</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var canonical = String.fromCharCode(0x41F, 0x440, 0x438, 0x432, 0x435, 0x442); +ok(document.inputEncoding === "KOI8-R", "Document encoding is KOI8-R"); + +// Worker sends two strings, one with `canonical` encoded in KOI8-R and one as UTF-8. +// Since Worker scripts should always be decoded using UTF-8, even if the owning document's charset is different, the UTF-8 decode should match, while KOI8-R should fail. +var counter = 0; +var worker = new Worker("loadEncoding_worker.js"); +worker.onmessage = function(e) { + if (e.data.encoding === "KOI8-R") { + ok(e.data.text !== canonical, "KOI8-R decoded text should not match"); + } else if (e.data.encoding === "UTF-8") { + ok(e.data.text === canonical, "UTF-8 decoded text should match"); + } + counter++; + if (counter === 2) + SimpleTest.finish(); +} + +worker.onerror = function(e) { + ok(false, "Worker error"); + SimpleTest.finish(); +} +</script> + +</pre> +</body> +</html> diff --git a/dom/workers/test/test_loadError.html b/dom/workers/test/test_loadError.html new file mode 100644 index 000000000..dc109b796 --- /dev/null +++ b/dom/workers/test/test_loadError.html @@ -0,0 +1,77 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Test for DOM Worker Threads</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +"use strict"; + +var loadErrorMessage = 'SecurityError: Failed to load worker script at "about:blank"'; + +function nextTest() { + (function(){ + function workerfunc() { + var subworker = new Worker("about:blank"); + subworker.onerror = function(e) { + e.preventDefault(); + postMessage(e.message); + } + } + var b = new Blob([workerfunc+'workerfunc();']); + var u = URL.createObjectURL(b); + function callworker(i) { + try { + var w = new Worker(u); + URL.revokeObjectURL(u); + is(i, 0, 'worker creation succeeded'); + } catch (e) { + is(i, 1, 'worker creation failed'); + SimpleTest.finish(); + return; + } + w.onmessage = function(e) { + is(e.data, loadErrorMessage, + "Should catch the error when loading inner script"); + if (++i < 2) callworker(i); + else SimpleTest.finish(); + }; + w.onerrror = function(e) { + ok(false, "Should not get any errors from this worker"); + } + } + callworker(0); + })(); +} + +try { + var worker = new Worker("about:blank"); + worker.onerror = function(e) { + e.preventDefault(); + is(e.message, loadErrorMessage, + "Should get the right error from the toplevel script"); + nextTest(); + } + + worker.onmessage = function(event) { + ok(false, "Shouldn't get a message!"); + SimpleTest.finish(); + } +} catch (e) { + ok(false, "This should not happen."); +} + + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/test_location.html b/dom/workers/test/test_location.html new file mode 100644 index 000000000..cbd605307 --- /dev/null +++ b/dom/workers/test/test_location.html @@ -0,0 +1,72 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Location +--> +<head> + <title>Test for DOM Worker Location</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + + var thisFilename = "test_location.html"; + var workerFilename = "location_worker.js"; + + var href = window.location.href + var queryPos = href.lastIndexOf(window.location.search); + var baseHref = href.substr(0, href.substr(0, queryPos).lastIndexOf("/") + 1); + + var path = window.location.pathname; + var basePath = path.substr(0, path.lastIndexOf("/") + 1); + + var strings = { + "toString": baseHref + workerFilename, + "href": baseHref + workerFilename, + "protocol": window.location.protocol, + "host": window.location.host, + "hostname": window.location.hostname, + "port": window.location.port, + "pathname": basePath + workerFilename, + "search": "", + "hash": "", + "origin": "http://mochi.test:8888" + }; + + var lastSlash = href.substr(0, queryPos).lastIndexOf("/") + 1; + is(thisFilename, + href.substr(lastSlash, queryPos - lastSlash), + "Correct filename "); + + var worker = new Worker(workerFilename); + + worker.onmessage = function(event) { + if (event.data.string == "testfinished") { + SimpleTest.finish(); + return; + } + ok(event.data.string in strings, event.data.string); + is(event.data.value, strings[event.data.string], event.data.string); + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_longThread.html b/dom/workers/test/test_longThread.html new file mode 100644 index 000000000..d23989f8b --- /dev/null +++ b/dom/workers/test/test_longThread.html @@ -0,0 +1,59 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads (Bug 437152) +--> +<head> + <title>Test for DOM Worker Threads (Bug 437152)</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=437152">DOM Worker Threads Bug 437152</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + const numThreads = 5; + var doneThreads = 0; + + function onmessage(event) { + switch (event.data) { + case "done": + if (++doneThreads == numThreads) { + ok(true, "All messages received from workers"); + SimpleTest.finish(); + } + break; + default: + ok(false, "Unexpected message"); + SimpleTest.finish(); + } + } + + function onerror(event) { + ok(false, "Worker had an error"); + SimpleTest.finish(); + } + + for (var i = 0; i < numThreads; i++) { + var worker = new Worker("longThread_worker.js"); + worker.onmessage = onmessage; + worker.onerror = onerror; + worker.postMessage("start"); + } + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/test_multi_sharedWorker.html b/dom/workers/test/test_multi_sharedWorker.html new file mode 100644 index 000000000..ba028302f --- /dev/null +++ b/dom/workers/test/test_multi_sharedWorker.html @@ -0,0 +1,242 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for SharedWorker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + <script class="testbody" type="text/javascript;version=1.7"> + "use strict"; + + const basePath = + location.pathname.substring(0, + location.pathname.lastIndexOf("/") + 1); + const baseURL = location.origin + basePath; + + const frameRelativeURL = "multi_sharedWorker_frame.html"; + const frameAbsoluteURL = baseURL + frameRelativeURL; + const workerAbsoluteURL = + baseURL + "multi_sharedWorker_sharedWorker.js"; + + const storedData = "0123456789abcdefghijklmnopqrstuvwxyz"; + const errorMessage = "Error: Expected"; + const errorLineno = 34; + + let testGenerator = (function() { + SimpleTest.waitForExplicitFinish(); + + window.addEventListener("message", function(event) { + if (typeof(event.data) == "string") { + info(event.data); + } else { + sendToGenerator(event); + } + }); + + let frame1 = document.getElementById("frame1"); + frame1.src = frameRelativeURL; + frame1.onload = sendToGenerator; + + yield undefined; + + frame1 = frame1.contentWindow; + + let frame2 = document.getElementById("frame2"); + frame2.src = frameAbsoluteURL; + frame2.onload = sendToGenerator; + + yield undefined; + + frame2 = frame2.contentWindow; + + let data = { + command: "start" + }; + + frame1.postMessage(data, "*"); + frame2.postMessage(data, "*"); + + let event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame1, "First window got the event"); + is(event.data.type, "connect", "Got a connect message"); + + data = { + command: "retrieve" + }; + frame1.postMessage(data, "*"); + + event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame1, "First window got the event"); + is(event.data.type, "result", "Got a result message"); + is(event.data.data, undefined, "No data stored yet"); + + frame2.postMessage(data, "*"); + + event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame2, "Second window got the event"); + is(event.data.type, "result", "Got a result message"); + is(event.data.data, undefined, "No data stored yet"); + + data = { + command: "store", + data: storedData + }; + frame2.postMessage(data, "*"); + + data = { + command: "retrieve" + }; + frame1.postMessage(data, "*"); + + event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame1, "First window got the event"); + is(event.data.type, "result", "Got a result message"); + is(event.data.data, storedData, "Got stored data"); + + // This will generate two MessageEvents, one for each window. + let sawFrame1Error = false; + let sawFrame2Error = false; + + data = { + command: "error" + }; + frame1.postMessage(data, "*"); + + // First event. + event = yield undefined; + + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.data.type, "worker-error", "Got an error message"); + is(event.data.message, errorMessage, "Got correct error message"); + is(event.data.filename, workerAbsoluteURL, "Got correct filename"); + is(event.data.lineno, errorLineno, "Got correct lineno"); + if (event.source == frame1) { + is(sawFrame1Error, false, "Haven't seen error for frame1 yet"); + sawFrame1Error = true; + } else if (event.source == frame2) { + is(sawFrame2Error, false, "Haven't seen error for frame1 yet"); + sawFrame2Error = true; + } else { + ok(false, "Saw error from unknown window"); + } + + // Second event + event = yield undefined; + + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.data.type, "worker-error", "Got an error message"); + is(event.data.message, errorMessage, "Got correct error message"); + is(event.data.filename, workerAbsoluteURL, "Got correct filename"); + is(event.data.lineno, errorLineno, "Got correct lineno"); + if (event.source == frame1) { + is(sawFrame1Error, false, "Haven't seen error for frame1 yet"); + sawFrame1Error = true; + } else if (event.source == frame2) { + is(sawFrame2Error, false, "Haven't seen error for frame1 yet"); + sawFrame2Error = true; + } else { + ok(false, "Saw error from unknown window"); + } + + is(sawFrame1Error, true, "Saw error for frame1"); + is(sawFrame2Error, true, "Saw error for frame2"); + + // This will generate two MessageEvents, one for each window. + sawFrame1Error = false; + sawFrame2Error = false; + + data = { + command: "error" + }; + frame1.postMessage(data, "*"); + + // First event. + event = yield undefined; + + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.data.type, "error", "Got an error message"); + is(event.data.message, errorMessage, "Got correct error message"); + is(event.data.filename, workerAbsoluteURL, "Got correct filename"); + is(event.data.lineno, errorLineno, "Got correct lineno"); + is(event.data.isErrorEvent, true, "Frame got an ErrorEvent"); + if (event.source == frame1) { + is(sawFrame1Error, false, "Haven't seen error for frame1 yet"); + sawFrame1Error = true; + } else if (event.source == frame2) { + is(sawFrame2Error, false, "Haven't seen error for frame1 yet"); + sawFrame2Error = true; + } else { + ok(false, "Saw error from unknown window"); + } + + // Second event + event = yield undefined; + + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.data.type, "error", "Got an error message"); + is(event.data.message, errorMessage, "Got correct error message"); + is(event.data.filename, workerAbsoluteURL, "Got correct filename"); + is(event.data.lineno, errorLineno, "Got correct lineno"); + is(event.data.isErrorEvent, true, "Frame got an ErrorEvent"); + if (event.source == frame1) { + is(sawFrame1Error, false, "Haven't seen error for frame1 yet"); + sawFrame1Error = true; + } else if (event.source == frame2) { + is(sawFrame2Error, false, "Haven't seen error for frame1 yet"); + sawFrame2Error = true; + } else { + ok(false, "Saw error from unknown window"); + } + + is(sawFrame1Error, true, "Saw error for frame1"); + is(sawFrame2Error, true, "Saw error for frame2"); + + // Try a shared worker in a different origin. + frame1 = document.getElementById("frame1"); + frame1.src = "http://example.org" + basePath + frameRelativeURL; + frame1.onload = sendToGenerator; + yield undefined; + + frame1 = frame1.contentWindow; + + data = { + command: "retrieve" + }; + frame1.postMessage(data, "*"); + + event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame1, "First window got the event"); + is(event.data.type, "result", "Got a result message"); + is(event.data.data, undefined, "No data stored yet"); + + frame2.postMessage(data, "*"); + + event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame2, "First window got the event"); + is(event.data.type, "result", "Got a result message"); + is(event.data.data, storedData, "Got stored data"); + + window.removeEventListener("message", sendToGenerator); + + SimpleTest.finish(); + yield undefined; + })(); + + let sendToGenerator = testGenerator.send.bind(testGenerator); + + </script> + </head> + <body onload="testGenerator.next();"> + <iframe id="frame1"></iframe> + <iframe id="frame2"></iframe> + </body> +</html> diff --git a/dom/workers/test/test_multi_sharedWorker_lifetimes.html b/dom/workers/test/test_multi_sharedWorker_lifetimes.html new file mode 100644 index 000000000..a3f4dc9b5 --- /dev/null +++ b/dom/workers/test/test_multi_sharedWorker_lifetimes.html @@ -0,0 +1,156 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for SharedWorker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + <script class="testbody" type="text/javascript;version=1.7"> + "use strict"; + + const scrollbarPref = "layout.testing.overlay-scrollbars.always-visible"; + const bfCacheEnabledPref = "browser.sessionhistory.cache_subframes"; + const bfCacheDepthPref = "browser.sessionhistory.max_total_viewers"; + const bfCacheDepth = 10; + + const frameRelativeURL = "multi_sharedWorker_frame.html"; + const storedData = "0123456789abcdefghijklmnopqrstuvwxyz"; + + let testGenerator = (function() { + SimpleTest.waitForExplicitFinish(); + + // Force scrollbar to always be shown. The scrollbar setting is + // necessary to avoid the fade-in/fade-out from evicting our document + // from the BF cache below. If bug 1049277 is fixed, then we can + // stop setting the scrollbar pref here. + SpecialPowers.pushPrefEnv({ set: [[scrollbarPref, true]] }, + sendToGenerator); + yield undefined; + + window.addEventListener("message", function(event) { + if (typeof(event.data) == "string") { + info(event.data); + } else { + sendToGenerator(event); + } + }); + + let frame = document.getElementById("frame"); + frame.src = frameRelativeURL; + frame.onload = sendToGenerator; + + yield undefined; + + frame = frame.contentWindow; + frame.postMessage({ command: "retrieve" }, "*"); + + let event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame, "Correct window got the event"); + is(event.data.type, "result", "Got a result message"); + is(event.data.data, undefined, "No data stored yet"); + + frame.postMessage({ command: "store", data: storedData }, "*"); + frame.postMessage({ command: "retrieve" }, "*"); + + event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame, "Correct window got the event"); + is(event.data.type, "result", "Got a result message"); + is(event.data.data, storedData, "Got stored data"); + + // Navigate when the bfcache is disabled. + info("Navigating to about:blank"); + frame = document.getElementById("frame"); + frame.onload = sendToGenerator; + frame.src = "about:blank"; + frame.contentWindow.document.body.offsetTop; + + yield undefined; + + info("Navigating to " + frameRelativeURL); + frame.src = frameRelativeURL; + frame.contentWindow.document.body.offsetTop; + + yield undefined; + + frame = frame.contentWindow; + frame.postMessage({ command: "retrieve" }, "*"); + + event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame, "Correct window got the event"); + is(event.data.type, "result", "Got a result message"); + is(event.data.data, undefined, "No data stored"); + + frame.postMessage({ command: "store", data: storedData }, "*"); + frame.postMessage({ command: "retrieve" }, "*"); + + event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame, "Correct window got the event"); + is(event.data.type, "result", "Got a result message"); + is(event.data.data, storedData, "Got stored data"); + + info("Enabling '" + bfCacheEnabledPref + "' pref"); + SpecialPowers.pushPrefEnv({ set: [[bfCacheEnabledPref, true], + [bfCacheDepthPref, bfCacheDepth]] }, + sendToGenerator); + yield undefined; + + // Navigate when the bfcache is enabled. + frame = document.getElementById("frame"); + frame.onload = sendToGenerator; + + info("Navigating to about:blank"); + frame.src = "about:blank"; + frame.contentWindow.document.body.offsetTop; + + yield undefined; + + for (let i = 0; i < 3; i++) { + info("Running GC"); + SpecialPowers.exactGC(sendToGenerator); + yield undefined; + + info("Waiting the event queue to clear"); + SpecialPowers.executeSoon(sendToGenerator); + yield undefined; + } + + info("Navigating to " + frameRelativeURL); + frame.src = frameRelativeURL; + frame.contentWindow.document.body.offsetTop; + + yield undefined; + + frame = frame.contentWindow; + frame.postMessage({ command: "retrieve" }, "*"); + + event = yield undefined; + ok(event instanceof MessageEvent, "Got a MessageEvent"); + is(event.source, frame, "Correct window got the event"); + is(event.data.type, "result", "Got a result message"); + is(event.data.data, storedData, "Still have data stored"); + + info("Resetting '" + bfCacheEnabledPref + "' pref"); + SpecialPowers.popPrefEnv(sendToGenerator); + yield undefined; + + window.removeEventListener("message", sendToGenerator); + + SimpleTest.finish(); + yield undefined; + })(); + + let sendToGenerator = testGenerator.send.bind(testGenerator); + + </script> + </head> + <body onload="testGenerator.next();"> + <iframe id="frame"></iframe> + </body> +</html> diff --git a/dom/workers/test/test_navigator.html b/dom/workers/test/test_navigator.html new file mode 100644 index 000000000..49d320967 --- /dev/null +++ b/dom/workers/test/test_navigator.html @@ -0,0 +1,68 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Navigator +--> +<head> + <title>Test for DOM Worker Navigator</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + + var worker = new Worker("navigator_worker.js"); + + worker.onmessage = function(event) { + var args = JSON.parse(event.data); + + if (args.name == "testFinished") { + SimpleTest.finish(); + return; + } + + if (typeof navigator[args.name] == "undefined") { + ok(false, "Navigator has no '" + args.name + "' property!"); + return; + } + + if (args.name === "languages") { + is(navigator.languages.toString(), args.value.toString(), "languages matches"); + return; + } + + if (args.name === "storage") { + is(typeof navigator.storage, typeof args.value, "storage type matches"); + return; + } + + is(navigator[args.name], args.value, + "Mismatched navigator string for " + args.name + "!"); + }; + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + } + + var version = SpecialPowers.Cc["@mozilla.org/xre/app-info;1"].getService(SpecialPowers.Ci.nsIXULAppInfo).version; + var isNightly = version.endsWith("a1"); + var isRelease = !version.includes("a"); + + worker.postMessage({ isNightly, isRelease }); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_navigator_languages.html b/dom/workers/test/test_navigator_languages.html new file mode 100644 index 000000000..2111739d9 --- /dev/null +++ b/dom/workers/test/test_navigator_languages.html @@ -0,0 +1,53 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Navigator +--> +<head> + <title>Test for DOM Worker Navigator.languages</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + + var tests = [ 'en,it', 'it,en,fr', '', 'en' ]; + var expectedLanguages; + function runTests() { + if (!tests.length) { + worker.postMessage('finish'); + SimpleTest.finish(); + return; + } + + expectedLanguages = tests.shift(); + SpecialPowers.pushPrefEnv({"set": [["intl.accept_languages", expectedLanguages]]}, function() { + worker.postMessage(true); + }); + } + + SimpleTest.waitForExplicitFinish(); + + var worker = new Worker("navigator_languages_worker.js"); + + worker.onmessage = function(event) { + is(event.data.toString(), navigator.languages.toString(), "The languages mach."); + is(event.data.toString(), expectedLanguages, "This is the correct result."); + runTests(); + } + + runTests(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_navigator_workers_hardwareConcurrency.html b/dom/workers/test/test_navigator_workers_hardwareConcurrency.html new file mode 100644 index 000000000..6fbbc75a9 --- /dev/null +++ b/dom/workers/test/test_navigator_workers_hardwareConcurrency.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Navigator.hardwareConcurrency</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + SimpleTest.waitForExplicitFinish(); + var script = "postMessage(navigator.hardwareConcurrency)"; + var url = URL.createObjectURL(new Blob([script])); + var w = new Worker(url); + w.onmessage = function(e) { + var x = e.data; + is(typeof x, "number", "hardwareConcurrency should be a number."); + ok(x > 0, "hardwareConcurrency should be greater than 0."); + SimpleTest.finish(); + } + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_newError.html b/dom/workers/test/test_newError.html new file mode 100644 index 000000000..51aeae6b0 --- /dev/null +++ b/dom/workers/test/test_newError.html @@ -0,0 +1,34 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <title>Test for DOM Worker Threads</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("newError_worker.js"); + + worker.onmessage = function(event) { + ok(false, "Shouldn't get a message!"); + SimpleTest.finish(); + } + + worker.onerror = function(event) { + is(event.message, "Error: foo!", "Got wrong error message!"); + event.preventDefault(); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/test_notification.html b/dom/workers/test/test_notification.html new file mode 100644 index 000000000..409d9dfd1 --- /dev/null +++ b/dom/workers/test/test_notification.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=916893 +--> +<head> + <title>Bug 916893</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=916893">Bug 916893</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events."); + + function runTest() { + MockServices.register(); + var w = new Worker("notification_worker.js"); + w.onmessage = function(e) { + if (e.data.type === 'finish') { + MockServices.unregister(); + SimpleTest.finish(); + } else if (e.data.type === 'ok') { + ok(e.data.test, e.data.message); + } else if (e.data.type === 'is') { + is(e.data.test1, e.data.test2, e.data.message); + } + } + + SimpleTest.waitForExplicitFinish(); + // turn on testing pref (used by notification.cpp, and mock the alerts + SpecialPowers.setBoolPref("notification.prompt.testing", true); + w.postMessage('start') + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv( + {"set": [["dom.webnotifications.workers.enabled", true]]}, + runTest + ); +</script> +</body> +</html> diff --git a/dom/workers/test/test_notification_child.html b/dom/workers/test/test_notification_child.html new file mode 100644 index 000000000..74a1e8e09 --- /dev/null +++ b/dom/workers/test/test_notification_child.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=916893 +--> +<head> + <title>Bug 916893 - Test Notifications in child workers.</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=916893">Bug 916893</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show event."); + function runTest() { + MockServices.register(); + var w = new Worker("notification_worker_child-parent.js"); + w.onmessage = function(e) { + if (e.data.type === 'finish') { + MockServices.unregister(); + SimpleTest.finish(); + } else if (e.data.type === 'ok') { + ok(e.data.test, e.data.message); + } else if (e.data.type === 'is') { + is(e.data.test1, e.data.test2, e.data.message); + } + } + + SimpleTest.waitForExplicitFinish(); + // turn on testing pref (used by notification.cpp, and mock the alerts + SpecialPowers.setBoolPref("notification.prompt.testing", true); + w.postMessage('start') + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv( + {"set": [["dom.webnotifications.workers.enabled", true]]}, + runTest + ); +</script> +</body> +</html> diff --git a/dom/workers/test/test_notification_permission.html b/dom/workers/test/test_notification_permission.html new file mode 100644 index 000000000..e51e060d3 --- /dev/null +++ b/dom/workers/test/test_notification_permission.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=916893 +--> +<head> + <title>Bug 916893 - Make sure error is fired on Notification if permission is denied.</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/tests/mochitest/notification/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=916893">Bug 916893</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show event."); + function runTest() { + MockServices.register(); + var w = new Worker("notification_permission_worker.js"); + w.onmessage = function(e) { + if (e.data.type === 'finish') { + SpecialPowers.setBoolPref("notification.prompt.testing.allow", true); + MockServices.unregister(); + SimpleTest.finish(); + } else if (e.data.type === 'ok') { + ok(e.data.test, e.data.message); + } else if (e.data.type === 'is') { + is(e.data.test1, e.data.test2, e.data.message); + } + } + + SimpleTest.waitForExplicitFinish(); + // turn on testing pref (used by notification.cpp, and mock the alerts + SpecialPowers.setBoolPref("notification.prompt.testing", true); + SpecialPowers.setBoolPref("notification.prompt.testing.allow", false); + w.postMessage('start') + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv( + {"set": [["dom.webnotifications.workers.enabled", true]]}, + runTest + ); +</script> +</body> +</html> diff --git a/dom/workers/test/test_onLine.html b/dom/workers/test/test_onLine.html new file mode 100644 index 000000000..660835f82 --- /dev/null +++ b/dom/workers/test/test_onLine.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 925437: online/offline events tests. + +Any copyright is dedicated to the Public Domain. +http://creativecommons.org/licenses/publicdomain/ +--> +<head> + <title>Test for Bug 925437 (worker online/offline events)</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=925437">Mozilla Bug 925437</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="text/javascript"> + +addLoadEvent(function() { + var w = new Worker("onLine_worker.js"); + + w.onmessage = function(e) { + if (e.data.type === 'ready') { + doTest(); + } else if (e.data.type === 'ok') { + ok(e.data.test, e.data.message); + } else if (e.data.type === 'finished') { + SimpleTest.finish(); + } + } + + function doTest() { + var iosvc = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService2); + iosvc.manageOfflineStatus = false; + + info("setting iosvc.offline = true"); + iosvc.offline = true; + + info("setting iosvc.offline = false"); + iosvc.offline = false; + + info("setting iosvc.offline = true"); + iosvc.offline = true; + + for (var i = 0; i < 10; ++i) { + iosvc.offline = !iosvc.offline; + } + + info("setting iosvc.offline = false"); + w.postMessage('lastTest'); + iosvc.offline = false; + } +}); + +SimpleTest.waitForExplicitFinish(); +</script> +</body> +</html> diff --git a/dom/workers/test/test_promise.html b/dom/workers/test/test_promise.html new file mode 100644 index 000000000..63f359f9d --- /dev/null +++ b/dom/workers/test/test_promise.html @@ -0,0 +1,44 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Promise object in workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + var worker = new Worker("promise_worker.js"); + + worker.onmessage = function(event) { + + if (event.data.type == 'finish') { + SimpleTest.finish(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } + } + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + }; + + worker.postMessage(true); + } + + SimpleTest.waitForExplicitFinish(); + runTest(); +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/test_promise_resolved_with_string.html b/dom/workers/test/test_promise_resolved_with_string.html new file mode 100644 index 000000000..28caaaaa8 --- /dev/null +++ b/dom/workers/test/test_promise_resolved_with_string.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1027221 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1027221</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1027221 **/ + // Set up a permanent atom + SimpleTest.waitForExplicitFinish(); + var x = "x"; + // Trigger some incremental gc + SpecialPowers.Cu.getJSTestingFunctions().gcslice(1); + + // Kick off a worker that uses this same atom + var w = new Worker("data:text/plain,Promise.resolve('x').then(function() { postMessage(1); });"); + // Maybe trigger some more incremental gc + SpecialPowers.Cu.getJSTestingFunctions().gcslice(1); + + w.onmessage = function() { + ok(true, "Got here"); + SimpleTest.finish(); + }; + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1027221">Mozilla Bug 1027221</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_recursion.html b/dom/workers/test/test_recursion.html new file mode 100644 index 000000000..888a607c4 --- /dev/null +++ b/dom/workers/test/test_recursion.html @@ -0,0 +1,69 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads +--> +<head> + <title>Test for DOM Worker Threads Recursion</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + // Intermittently triggers one assertion on Mac (bug 848098). + if (navigator.platform.indexOf("Mac") == 0) { + SimpleTest.expectAssertions(0, 1); + } + + const testCount = 2; + var errorCount = 0; + + var worker = new Worker("recursion_worker.js"); + + function done() { + worker.terminate(); + SimpleTest.finish(); + } + + worker.onmessage = function(event) { + if (event.data == "Done") { + ok(true, "correct message"); + } + else { + ok(false, "Bad message: " + event.data); + } + done(); + } + + worker.onerror = function(event) { + event.preventDefault(); + if (event.message == "too much recursion") { + ok(true, "got correct error message"); + ++errorCount; + } + else { + ok(false, "got bad error message: " + event.message); + done(); + } + } + + for (var i = 0; i < testCount; i++) { + worker.postMessage(""); + } + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_recursiveOnerror.html b/dom/workers/test/test_recursiveOnerror.html new file mode 100644 index 000000000..06471d978 --- /dev/null +++ b/dom/workers/test/test_recursiveOnerror.html @@ -0,0 +1,44 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <script type="text/javascript"> + const filename = "http://mochi.test:8888/tests/dom/workers/test/" + + "recursiveOnerror_worker.js"; + const errors = [ + { message: "Error: 2", lineno: 6 }, + { message: "Error: 1", lineno: 10 } + ] + + var errorCount = 0; + + var worker = new Worker("recursiveOnerror_worker.js"); + worker.postMessage("go"); + + worker.onerror = function(event) { + event.preventDefault(); + + ok(errorCount < errors.length, "Correct number of error events"); + const error = errors[errorCount++]; + + is(event.message, error.message, "Correct message"); + is(event.filename, filename, "Correct filename"); + is(event.lineno, error.lineno, "Correct lineno"); + + if (errorCount == errors.length) { + SimpleTest.finish(); + } + } + + SimpleTest.waitForExplicitFinish(); + </script> + </body> +</html> diff --git a/dom/workers/test/test_referrer.html b/dom/workers/test/test_referrer.html new file mode 100644 index 000000000..c3afec78e --- /dev/null +++ b/dom/workers/test/test_referrer.html @@ -0,0 +1,58 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the referrer of workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function test_mainScript() { + var worker = new Worker("referrer.sjs?worker"); + worker.onmessage = function() { + var xhr = new XMLHttpRequest(); + xhr.open('GET', 'referrer.sjs?result', true); + xhr.onload = function() { + is(xhr.responseText, location.href, "The referrer has been sent."); + next(); + } + xhr.send(); + } + worker.postMessage(42); + } + + function test_importScript() { + var worker = new Worker("worker_referrer.js"); + worker.onmessage = function(e) { + is(e.data, location.href.replace("test_referrer.html", "worker_referrer.js"), "The referrer has been sent."); + next(); + } + worker.postMessage(42); + } + + var tests = [ test_mainScript, test_importScript ]; + function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); + } + + SimpleTest.waitForExplicitFinish(); + next(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_referrer_header_worker.html b/dom/workers/test/test_referrer_header_worker.html new file mode 100644 index 000000000..d04c8b591 --- /dev/null +++ b/dom/workers/test/test_referrer_header_worker.html @@ -0,0 +1,39 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test the referrer of workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <script class="testbody" type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv( + {"set": [ + ['security.mixed_content.block_display_content', false], + ['security.mixed_content.block_active_content', false] + ]}, + function() { + SpecialPowers.pushPermissions([{'type': 'systemXHR', 'allow': true, 'context': document}], test); + }); + + function test() { + function messageListener(event) { + eval(event.data); + } + window.addEventListener("message", messageListener, false); + + var ifr = document.createElement('iframe'); + ifr.setAttribute('src', 'https://example.com/tests/dom/workers/test/referrer_worker.html'); + document.body.appendChild(ifr); + } + </script> +</body> +</html> + diff --git a/dom/workers/test/test_resolveWorker-assignment.html b/dom/workers/test/test_resolveWorker-assignment.html new file mode 100644 index 000000000..b6733e010 --- /dev/null +++ b/dom/workers/test/test_resolveWorker-assignment.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE html> +<html> + <head> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <script type="application/javascript"> + window.Worker = 17; // resolve through assignment + + var desc = Object.getOwnPropertyDescriptor(window, "Worker"); + ok(typeof desc === "object" && desc !== null, "Worker property must exist"); + + is(desc.value, 17, "Overwrite didn't work correctly"); + is(desc.enumerable, false, + "Initial descriptor was non-enumerable, and [[Put]] changes the " + + "property value but not its enumerability"); + is(desc.configurable, true, + "Initial descriptor was configurable, and [[Put]] changes the " + + "property value but not its configurability"); + is(desc.writable, true, + "Initial descriptor was writable, and [[Put]] changes the " + + "property value but not its writability"); + </script> + </body> +</html> diff --git a/dom/workers/test/test_resolveWorker.html b/dom/workers/test/test_resolveWorker.html new file mode 100644 index 000000000..8e2ea5445 --- /dev/null +++ b/dom/workers/test/test_resolveWorker.html @@ -0,0 +1,31 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE html> +<html> + <head> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <script type="application/javascript"> + window.Worker; // resolve not through assignment + Worker = 17; + + var desc = Object.getOwnPropertyDescriptor(window, "Worker"); + ok(typeof desc === "object" && desc !== null, "Worker property must exist"); + + is(desc.value, 17, "Overwrite didn't work correctly"); + is(desc.enumerable, false, + "Initial descriptor was non-enumerable, and [[Put]] changes the " + + "property value but not its enumerability"); + is(desc.configurable, true, + "Initial descriptor was configurable, and [[Put]] changes the " + + "property value but not its configurability"); + is(desc.writable, true, + "Initial descriptor was writable, and [[Put]] changes the " + + "property value but not its writability"); + </script> + </body> +</html> diff --git a/dom/workers/test/test_rvals.html b/dom/workers/test/test_rvals.html new file mode 100644 index 000000000..eba858928 --- /dev/null +++ b/dom/workers/test/test_rvals.html @@ -0,0 +1,37 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 911085</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("rvals_worker.js"); + + worker.onmessage = function(event) { + if (event.data == 'ignore') return; + + if (event.data == 'finished') { + is(worker.terminate(), undefined, "Terminate() returns 'undefined'"); + SimpleTest.finish(); + return; + } + + ok(event.data, "something good returns 'undefined' in workers"); + }; + + is(worker.postMessage(42), undefined, "PostMessage() returns 'undefined' on main thread"); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> + + diff --git a/dom/workers/test/test_setTimeoutWith0.html b/dom/workers/test/test_setTimeoutWith0.html new file mode 100644 index 000000000..4c0dacafb --- /dev/null +++ b/dom/workers/test/test_setTimeoutWith0.html @@ -0,0 +1,20 @@ +<html> +<head> + <title>Test for DOM Worker setTimeout and strings containing 0</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script> + +var a = new Worker('worker_setTimeoutWith0.js'); +a.onmessage = function(e) { + is(e.data, 2, "We want to see 2 here"); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +</script> +</body> +</html> + diff --git a/dom/workers/test/test_sharedWorker.html b/dom/workers/test/test_sharedWorker.html new file mode 100644 index 000000000..3d3d4e2c6 --- /dev/null +++ b/dom/workers/test/test_sharedWorker.html @@ -0,0 +1,71 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for SharedWorker</title> + <script src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + </head> + <body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + <script class="testbody"> + "use strict"; + + const href = window.location.href; + const filename = "sharedWorker_sharedWorker.js"; + const sentMessage = "ping"; + const errorFilename = href.substring(0, href.lastIndexOf("/") + 1) + + filename; + const errorLine = 91; + const errorColumn = 0; + + var worker = new SharedWorker(filename); + + ok(worker instanceof SharedWorker, "Got SharedWorker instance"); + ok(!("postMessage" in worker), "SharedWorker has no 'postMessage'"); + ok(worker.port instanceof MessagePort, + "Shared worker has MessagePort"); + + var receivedMessage; + var receivedError; + + worker.port.onmessage = function(event) { + ok(event instanceof MessageEvent, "Got a MessageEvent"); + ok(event.target === worker.port, + "MessageEvent has correct 'target' property"); + is(event.data, sentMessage, "Got correct message"); + ok(receivedMessage === undefined, "Haven't gotten message yet"); + receivedMessage = event.data; + if (receivedError) { + SimpleTest.finish(); + } + }; + + worker.onerror = function(event) { + ok(event instanceof ErrorEvent, "Got an ErrorEvent"); + is(event.message, "Error: " + sentMessage, "Got correct error"); + is(event.filename, errorFilename, "Got correct filename"); + is(event.lineno, errorLine, "Got correct lineno"); + is(event.colno, errorColumn, "Got correct column"); + ok(receivedError === undefined, "Haven't gotten error yet"); + receivedError = event.message; + event.preventDefault(); + if (receivedMessage) { + SimpleTest.finish(); + } + }; + + worker.port.postMessage(sentMessage); + + SimpleTest.waitForExplicitFinish(); + + </script> + </pre> + </body> +</html> diff --git a/dom/workers/test/test_sharedWorker_lifetime.html b/dom/workers/test/test_sharedWorker_lifetime.html new file mode 100644 index 000000000..169746892 --- /dev/null +++ b/dom/workers/test/test_sharedWorker_lifetime.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for MessagePort and SharedWorkers</title> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + </head> + <body> + <script class="testbody" type="text/javascript"> + +var gced = false; + +var sw = new SharedWorker('sharedWorker_lifetime.js'); +sw.port.onmessage = function(event) { + ok(gced, "The SW is still alive also after GC"); + SimpleTest.finish(); +} + +sw = null; +SpecialPowers.forceGC(); +gced = true; + +SimpleTest.waitForExplicitFinish(); + </script> + </body> +</html> diff --git a/dom/workers/test/test_sharedWorker_ports.html b/dom/workers/test/test_sharedWorker_ports.html new file mode 100644 index 000000000..32698ab52 --- /dev/null +++ b/dom/workers/test/test_sharedWorker_ports.html @@ -0,0 +1,42 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <title>Test for MessagePort and SharedWorkers</title> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + </head> + <body> + <script class="testbody" type="text/javascript"> + +var sw1 = new SharedWorker('sharedWorker_ports.js'); +sw1.port.onmessage = function(event) { + if (event.data.type == "connected") { + ok(true, "The SharedWorker is alive."); + + var sw2 = new SharedWorker('sharedWorker_ports.js'); + sw1.port.postMessage("Port from the main-thread!", [sw2.port]); + return; + } + + if (event.data.type == "status") { + ok(event.data.test, event.data.msg); + return; + } + + if (event.data.type == "finish") { + info("Finished!"); + ok(sw1.port, "The port still exists"); + sw1.port.foo = sw1; // Just a test to see if we leak. + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); + </script> + </body> +</html> + diff --git a/dom/workers/test/test_sharedWorker_privateBrowsing.html b/dom/workers/test/test_sharedWorker_privateBrowsing.html new file mode 100644 index 000000000..b0eac5831 --- /dev/null +++ b/dom/workers/test/test_sharedWorker_privateBrowsing.html @@ -0,0 +1,101 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SharedWorker - Private Browsing</title> + <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +</head> +<body> + +<script type="application/javascript"> + +const Ci = Components.interfaces; +var mainWindow; + +var contentPage = "http://mochi.test:8888/chrome/dom/workers/test/empty.html"; + +function testOnWindow(aIsPrivate, aCallback) { + var win = mainWindow.OpenBrowserWindow({private: aIsPrivate}); + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad, false); + win.addEventListener("DOMContentLoaded", function onInnerLoad() { + if (win.content.location.href != contentPage) { + win.gBrowser.loadURI(contentPage); + return; + } + + win.removeEventListener("DOMContentLoaded", onInnerLoad, true); + SimpleTest.executeSoon(function() { aCallback(win); }); + }, true); + + if (!aIsPrivate) { + win.gBrowser.loadURI(contentPage); + } + }, true); +} + +function setupWindow() { + mainWindow = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShellTreeItem) + .rootTreeItem + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + runTest(); +} + +var wN; +var wP; + +function doTests() { + testOnWindow(false, function(aWin) { + wN = aWin; + + testOnWindow(true, function(aWin) { + wP = aWin; + + var sharedWorker1 = new wP.content.SharedWorker('sharedWorker_privateBrowsing.js'); + sharedWorker1.port.onmessage = function(event) { + is(event.data, 1, "Only 1 sharedworker expected in the private window"); + + var sharedWorker2 = new wN.content.SharedWorker('sharedWorker_privateBrowsing.js'); + sharedWorker2.port.onmessage = function(event) { + is(event.data, 1, "Only 1 sharedworker expected in the normal window"); + + var sharedWorker3 = new wP.content.SharedWorker('sharedWorker_privateBrowsing.js'); + sharedWorker3.port.onmessage = function(event) { + is(event.data, 2, "Only 2 sharedworker expected in the private window"); + runTest(); + } + } + } + }); + }); +} + +var steps = [ + setupWindow, + doTests +]; + +function runTest() { + if (!steps.length) { + wN.close(); + wP.close(); + + SimpleTest.finish(); + return; + } + + var step = steps.shift(); + step(); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [ + ["browser.startup.page", 0], + ["browser.startup.homepage_override.mstone", "ignore"], +]}, runTest); + +</script> +</body> +</html> diff --git a/dom/workers/test/test_simpleThread.html b/dom/workers/test/test_simpleThread.html new file mode 100644 index 000000000..0dad90312 --- /dev/null +++ b/dom/workers/test/test_simpleThread.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads (Bug 437152) +--> +<head> + <title>Test for DOM Worker Threads (Bug 437152)</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=437152">DOM Worker Threads Bug 437152</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("simpleThread_worker.js"); + + worker.addEventListener("message",function(event) { + is(event.target, worker); + switch (event.data) { + case "no-op": + break; + case "started": + is(gotErrors, true); + worker.postMessage("no-op"); + worker.postMessage("stop"); + break; + case "stopped": + worker.postMessage("no-op"); + SimpleTest.finish(); + break; + default: + ok(false, "Unexpected message:" + event.data); + SimpleTest.finish(); + } + }, false); + + var gotErrors = false; + worker.onerror = function(event) { + event.preventDefault(); + is(event.target, worker); + is(event.message, "uncaught exception: Bad message: asdf"); + + worker.onerror = function(otherEvent) { + otherEvent.preventDefault(); + is(otherEvent.target, worker); + is(otherEvent.message, "ReferenceError: Components is not defined"); + gotErrors = true; + + worker.onerror = function(oneMoreEvent) { + ok(false, "Worker had an error:" + oneMoreEvent.message); + SimpleTest.finish(); + }; + }; + }; + + worker.postMessage("asdf"); + worker.postMessage("components"); + worker.postMessage("start"); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/test_subworkers_suspended.html b/dom/workers/test/test_subworkers_suspended.html new file mode 100644 index 000000000..063ccaa64 --- /dev/null +++ b/dom/workers/test/test_subworkers_suspended.html @@ -0,0 +1,135 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for sub workers+bfcache behavior</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + + const WORKER_URL = "worker_suspended.js"; + const SUB_WORKERS = 3 + + var testUrl1 = "window_suspended.html?page1Shown"; + var testUrl2 = "window_suspended.html?page2Shown"; + + var testWin; + var counter = 0; + + function cacheData() { + return caches.open("test") + .then(function(cache) { + return cache.match("http://mochi.test:888/foo"); + }) + .then(function(response) { + return response.text(); + }); + } + + function page1Shown(e) { + info("Page1Shown: " + testWin.location.href); + + // First time this page is shown. + if (counter == 0) { + ok(!e.persisted, "test page should have been persisted initially"); + + info("Create a worker and subworkers..."); + let worker = new e.target.defaultView.Worker(WORKER_URL); + + var promise = new Promise((resolve, reject) => { + info("Waiting until workers are ready..."); + worker.addEventListener("message", function onmessage(e) { + is(e.data, "ready", "We want to receive: -ready-"); + worker.removeEventListener("message", onmessage); + resolve(); + }); + worker.postMessage({ type: "page1", count: SUB_WORKERS }); + }); + + promise.then(function() { + info("Retrieving data from cache..."); + return cacheData(); + }) + + .then(function(content) { + is(content.indexOf("page1-"), 0, "We have data from the worker"); + }) + + .then(function() { + info("New location: " + testUrl2); + testWin.location.href = testUrl2; + }); + } else { + is(e.persisted, true, "test page should have been persisted in pageshow"); + + var promise = new Promise((resolve, reject) => { + info("Waiting a few seconds..."); + setTimeout(resolve, 5000); + }); + + promise.then(function() { + info("Retrieving data from cache..."); + return cacheData(); + }) + + .then(function(content) { + is(content.indexOf("page1-"), 0, "We have data from the worker"); + }) + + .then(function() { + testWin.close(); + SimpleTest.finish(); + }); + } + + counter++; + } + + function page2Shown(e) { + info("Page2Shown: " + testWin.location.href); + + info("Create a worker..."); + let worker = new e.target.defaultView.Worker(WORKER_URL); + + var promise = new Promise((resolve, reject) => { + info("Waiting until workers are ready..."); + worker.addEventListener("message", function onmessage(e) { + is(e.data, "ready", "We want to receive: -ready-"); + worker.removeEventListener("message", onmessage); + resolve(); + }); + worker.postMessage({ type: "page2" }); + }); + + promise.then(function() { + info("Retrieving data from cache..."); + return cacheData(); + }) + + .then(function(content) { + is(content, "page2-0", "We have data from the second worker"); + }) + + .then(function() { + info("Going back"); + testWin.history.back(); + }); + } + + SpecialPowers.pushPrefEnv({ set: [ + ["dom.caches.enabled", true], + ["dom.caches.testing.enabled", true], + ] }, + function() { + testWin = window.open(testUrl1); + }); + + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("untriaged"); + + </script> +</body> +</html> + diff --git a/dom/workers/test/test_suspend.html b/dom/workers/test/test_suspend.html new file mode 100644 index 000000000..806b97f6c --- /dev/null +++ b/dom/workers/test/test_suspend.html @@ -0,0 +1,138 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for DOM Worker Threads</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<iframe id="workerFrame" src="suspend_iframe.html" onload="subframeLoaded();"> +</iframe> +<script class="testbody" type="text/javascript"> + + SimpleTest.waitForExplicitFinish(); + + var iframe; + var lastCount; + + var suspended = false; + var resumed = false; + var finished = false; + + var interval; + var oldMessageCount; + var waitCount = 0; + + function finishTest() { + if (finished) { + return; + } + finished = true; + SpecialPowers.flushPrefEnv(function () { + iframe.terminateWorker(); + SimpleTest.finish(); + }); + } + + function waitInterval() { + if (finished) { + return; + } + is(String(iframe.location), "about:blank", "Wrong url!"); + is(suspended, true, "Not suspended?"); + is(resumed, false, "Already resumed?!"); + is(lastCount, oldMessageCount, "Received a message while suspended!"); + if (++waitCount == 5) { + clearInterval(interval); + resumed = true; + iframe.history.back(); + } + } + + function badOnloadCallback() { + if (finished) { + return; + } + ok(false, "We don't want suspend_iframe.html to fire a new load event, we want it to come out of the bfcache!"); + finishTest(); + } + + function suspendCallback() { + if (finished) { + return; + } + is(String(iframe.location), "about:blank", "Wrong url!"); + is(suspended, false, "Already suspended?"); + is(resumed, false, "Already resumed?"); + SpecialPowers.popPrefEnv(function () { + suspended = true; + var iframeElement = document.getElementById("workerFrame"); + iframeElement.onload = badOnloadCallback; + oldMessageCount = lastCount; + interval = setInterval(waitInterval, 1000); + }); + } + + function messageCallback(data) { + if (finished) { + return; + } + + if (!suspended) { + ok(lastCount === undefined || lastCount == data - 1, + "Got good data, lastCount = " + lastCount + ", data = " + data); + lastCount = data; + if (lastCount == 25) { + SpecialPowers.pushPrefEnv({"set": [["browser.sessionhistory.cache_subframes", true]]}, function () { + iframe.location = "about:blank"; + // We want suspend_iframe.html to go into bfcache, so we need to flush + // out all pending notifications. Otherwise, if they're flushed too + // late, they could kick us out of the bfcache again. + iframe.document.body.offsetTop; + }); + } + return; + } + + var newLocation = + window.location.toString().replace("test_suspend.html", + "suspend_iframe.html"); + is(newLocation.indexOf(iframe.location.toString()), 0, "Wrong url!"); + is(resumed, true, "Got message before resumed!"); + is(lastCount, data - 1, "Missed a message, suspend failed!"); + finishTest(); + } + + function errorCallback(data) { + if (finished) { + return; + } + ok(false, "Iframe had an error: '" + data + "'"); + finishTest(); + } + + function subframeLoaded() { + if (finished) { + return; + } + var iframeElement = document.getElementById("workerFrame"); + iframeElement.onload = suspendCallback; + + iframe = iframeElement.contentWindow; + ok(iframe, "No iframe?!"); + + iframe.startWorker(messageCallback, errorCallback); + } + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_terminate.html b/dom/workers/test/test_terminate.html new file mode 100644 index 000000000..5d31bd165 --- /dev/null +++ b/dom/workers/test/test_terminate.html @@ -0,0 +1,100 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker terminate feature +--> +<head> + <title>Test for DOM Worker Navigator</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + + + var messageCount = 0; + var intervalCount = 0; + + var interval; + + var worker; + + function messageListener(event) { + is(event.data, "Still alive!", "Correct message!"); + if (++messageCount == 20) { + ok(worker.onmessage === messageListener, + "Correct listener before terminate"); + + worker.terminate(); + + var exception = false; + try { + worker.addEventListener("message", messageListener, false); + } + catch (e) { + exception = true; + } + is(exception, false, "addEventListener didn't throw after terminate"); + + exception = false; + try { + worker.removeEventListener("message", messageListener, false); + } + catch (e) { + exception = true; + } + is(exception, false, "removeEventListener didn't throw after terminate"); + + exception = false; + try { + worker.postMessage("foo"); + } + catch (e) { + exception = true; + } + is(exception, false, "postMessage didn't throw after terminate"); + + exception = false; + try { + worker.terminate(); + } + catch (e) { + exception = true; + } + is(exception, false, "terminate didn't throw after terminate"); + + ok(worker.onmessage === messageListener, + "Correct listener after terminate"); + + worker.onmessage = function(event) { } + + interval = setInterval(testCount, 1000); + } + } + + function testCount() { + is(messageCount, 20, "Received another message after terminated!"); + if (intervalCount++ == 5) { + clearInterval(interval); + SimpleTest.finish(); + } + } + + worker = new Worker("terminate_worker.js"); + worker.onmessage = messageListener; + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_threadErrors.html b/dom/workers/test/test_threadErrors.html new file mode 100644 index 000000000..034a443e7 --- /dev/null +++ b/dom/workers/test/test_threadErrors.html @@ -0,0 +1,64 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads (Bug 437152) +--> +<head> + <title>Test for DOM Worker Threads (Bug 437152)</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=437152">DOM Worker Threads Bug 437152</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + const expectedErrorCount = 4; + + function messageListener(event) { + ok(false, "Unexpected message: " + event.data); + SimpleTest.finish(); + }; + + var actualErrorCount = 0; + var failedWorkers = []; + + function errorListener(event) { + event.preventDefault(); + + if (failedWorkers.indexOf(event.target) != -1) { + ok(false, "Seen an extra error from this worker"); + SimpleTest.finish(); + return; + } + + failedWorkers.push(event.target); + actualErrorCount++; + + if (actualErrorCount == expectedErrorCount) { + ok(true, "all errors correctly detected"); + SimpleTest.finish(); + } + }; + + for (var i = 1; i <= expectedErrorCount; i++) { + var worker = new Worker("threadErrors_worker" + i + ".js"); + worker.onmessage = messageListener; + worker.onerror = errorListener; + worker.postMessage("Hi"); + } + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_threadTimeouts.html b/dom/workers/test/test_threadTimeouts.html new file mode 100644 index 000000000..0fa86935a --- /dev/null +++ b/dom/workers/test/test_threadTimeouts.html @@ -0,0 +1,61 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads (Bug 437152) +--> +<head> + <title>Test for DOM Worker Threads (Bug 437152)</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=437152">DOM Worker Threads Bug 437152</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("threadTimeouts_worker.js"); + + worker.onmessage = function(event) { + is(event.target, worker); + switch (event.data) { + case "timeoutFinished": + event.target.postMessage("startInterval"); + break; + case "intervalFinished": + event.target.postMessage("cancelInterval"); + break; + case "intervalCanceled": + worker.postMessage("startExpression"); + break; + case "expressionFinished": + SimpleTest.finish(); + break; + default: + ok(false, "Unexpected message"); + SimpleTest.finish(); + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + ok(false, "Worker had an error: " + event.message); + SimpleTest.finish(); + }; + + worker.postMessage("startTimeout"); + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/test_throwingOnerror.html b/dom/workers/test/test_throwingOnerror.html new file mode 100644 index 000000000..7676541f7 --- /dev/null +++ b/dom/workers/test/test_throwingOnerror.html @@ -0,0 +1,54 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads +--> +<head> + <title>Test for DOM Worker Threads Recursion</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("throwingOnerror_worker.js"); + + var errors = ["foo", "bar"]; + + worker.onerror = function(event) { + event.preventDefault(); + var found = false; + for (var index in errors) { + if (event.message == "uncaught exception: " + errors[index]) { + errors.splice(index, 1); + found = true; + break; + } + } + is(found, true, "Unexpected error!"); + }; + + worker.onmessage = function(event) { + is(errors.length, 0, "Didn't see expected errors!"); + SimpleTest.finish(); + }; + + for (var i = 0; i < 2; i++) { + worker.postMessage(""); + } + + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_timeoutTracing.html b/dom/workers/test/test_timeoutTracing.html new file mode 100644 index 000000000..6770d36a1 --- /dev/null +++ b/dom/workers/test/test_timeoutTracing.html @@ -0,0 +1,48 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker Threads +--> +<head> + <title>Test for DOM Worker Threads</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("timeoutTracing_worker.js"); + + worker.onmessage = function(event) { + // begin + worker.onmessage = null; + + // 1 second should be enough to crash. + window.setTimeout(function(event) { + ok(true, "Didn't crash!"); + SimpleTest.finish(); + }, 1000); + + var os = SpecialPowers.Cc["@mozilla.org/observer-service;1"] + .getService(SpecialPowers.Ci.nsIObserverService); + os.notifyObservers(null, "memory-pressure", "heap-minimize"); + } + + worker.onerror = function(event) { + ok(false, "I was expecting a crash, not an error"); + SimpleTest.finish(); + }; + + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("untriaged"); + +</script> +</pre> +</body> +</html> + diff --git a/dom/workers/test/test_transferable.html b/dom/workers/test/test_transferable.html new file mode 100644 index 000000000..a3deec12a --- /dev/null +++ b/dom/workers/test/test_transferable.html @@ -0,0 +1,123 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<!-- +Tests of DOM Worker transferable objects +--> +<head> + <title>Test for DOM Worker transferable objects</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + + function test1(sizes) { + if (!sizes.length) { + runTests(); + return; + } + + var size = sizes.pop(); + + var worker = new Worker("transferable_worker.js"); + worker.onmessage = function(event) { + ok(event.data.status, event.data.event); + if (!event.data.status) { + runTests(); + return; + } + + if ("notEmpty" in event.data && "byteLength" in event.data.notEmpty) { + ok(event.data.notEmpty.byteLength != 0, + "P: NotEmpty object received: " + event.data.notEmpty.byteLength); + } + + if (!event.data.last) + return; + + test1(sizes); + } + worker.onerror = function(event) { + ok(false, "No errors!"); + } + + try { + worker.postMessage(42, true); + ok(false, "P: PostMessage - Exception for wrong type"); + } catch(e) { + ok(true, "P: PostMessage - Exception for wrong type"); + } + + try { + ab = new ArrayBuffer(size); + worker.postMessage(42,[ab, ab]); + ok(false, "P: PostMessage - Exception for duplicate"); + } catch(e) { + ok(true, "P: PostMessage - Exception for duplicate"); + } + + var ab = new ArrayBuffer(size); + ok(ab.byteLength == size, "P: The size is: " + size + " == " + ab.byteLength); + worker.postMessage({ data: 0, timeout: 0, ab: ab, cb: ab, size: size }, [ab]); + ok(ab.byteLength == 0, "P: PostMessage - The size is: 0 == " + ab.byteLength) + } + + function test2() { + var worker = new Worker("transferable_worker.js"); + worker.onmessage = function(event) { + ok(event.data.status, event.data.event); + if (!event.data.status) { + runTests(); + return; + } + + if ("notEmpty" in event.data && "byteLength" in event.data.notEmpty) { + ok(event.data.notEmpty.byteLength != 0, + "P: NotEmpty object received: " + event.data.notEmpty.byteLength); + } + + if (event.data.last) { + runTests(); + } + } + worker.onerror = function(event) { + ok(false, "No errors!"); + } + + var f = new Float32Array([0,1,2,3]); + ok(f.byteLength != 0, "P: The size is: " + f.byteLength + " is not 0"); + worker.postMessage({ event: "P: postMessage with Float32Array", status: true, + size: 4, notEmpty: f, bc: [ f, f, { dd: f } ] }, [f.buffer]); + ok(f.byteLength == 0, "P: The size is: " + f.byteLength + " is 0"); + } + + var tests = [ + function() { test1([1024 * 1024 * 32, 128, 4]); }, + test2 + ]; + function runTests() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); + } + + SimpleTest.waitForExplicitFinish(); + runTests(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_webSocket_sharedWorker.html b/dom/workers/test/test_webSocket_sharedWorker.html new file mode 100644 index 000000000..7609a164b --- /dev/null +++ b/dom/workers/test/test_webSocket_sharedWorker.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 1090183</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<script class="testbody" type="text/javascript"> + +var sw = new SharedWorker('webSocket_sharedWorker.js'); +sw.port.onmessage = function(event) { + if (event.data.type == 'finish') { + SimpleTest.finish(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_websocket1.html b/dom/workers/test/test_websocket1.html new file mode 100644 index 000000000..6c74af03c --- /dev/null +++ b/dom/workers/test/test_websocket1.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebSocket object in workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<pre id="feedback"></pre> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("websocket_worker1.js"); + + worker.onmessage = function(event) { + is(event.target, worker, "event.target should be a worker!"); + + if (event.data.type == 'finish') { + info("All done!"); + SimpleTest.finish(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } else if (event.data.type == 'feedback') { + info(event.data.msg); + document.getElementById('feedback').innerHTML += event.data.msg + "\n"; + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + info("error!"); + ok(false, "Worker had an error: " + event.data); + SimpleTest.finish(); + }; + + worker.postMessage('foobar'); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_websocket2.html b/dom/workers/test/test_websocket2.html new file mode 100644 index 000000000..d774be5a2 --- /dev/null +++ b/dom/workers/test/test_websocket2.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebSocket object in workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<pre id="feedback"></pre> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("websocket_worker2.js"); + + worker.onmessage = function(event) { + is(event.target, worker, "event.target should be a worker!"); + + if (event.data.type == 'finish') { + info("All done!"); + SimpleTest.finish(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } else if (event.data.type == 'feedback') { + info(event.data.msg); + document.getElementById('feedback').innerHTML += event.data.msg + "\n"; + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + info("error!"); + ok(false, "Worker had an error: " + event.data); + SimpleTest.finish(); + }; + + worker.postMessage('foobar'); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_websocket3.html b/dom/workers/test/test_websocket3.html new file mode 100644 index 000000000..0882cf313 --- /dev/null +++ b/dom/workers/test/test_websocket3.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebSocket object in workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<pre id="feedback"></pre> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("websocket_worker3.js"); + + worker.onmessage = function(event) { + is(event.target, worker, "event.target should be a worker!"); + + if (event.data.type == 'finish') { + info("All done!"); + SimpleTest.finish(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } else if (event.data.type == 'feedback') { + info(event.data.msg); + document.getElementById('feedback').innerHTML += event.data.msg + "\n"; + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + info("error!"); + ok(false, "Worker had an error: " + event.data); + SimpleTest.finish(); + }; + + worker.postMessage('foobar'); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_websocket4.html b/dom/workers/test/test_websocket4.html new file mode 100644 index 000000000..8c6bef506 --- /dev/null +++ b/dom/workers/test/test_websocket4.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebSocket object in workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<pre id="feedback"></pre> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("websocket_worker4.js"); + + worker.onmessage = function(event) { + is(event.target, worker, "event.target should be a worker!"); + + if (event.data.type == 'finish') { + info("All done!"); + SimpleTest.finish(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } else if (event.data.type == 'feedback') { + info(event.data.msg); + document.getElementById('feedback').innerHTML += event.data.msg + "\n"; + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + info("error!"); + ok(false, "Worker had an error: " + event.data); + SimpleTest.finish(); + }; + + worker.postMessage('foobar'); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_websocket5.html b/dom/workers/test/test_websocket5.html new file mode 100644 index 000000000..cadae3782 --- /dev/null +++ b/dom/workers/test/test_websocket5.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebSocket object in workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<pre id="feedback"></pre> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("websocket_worker5.js"); + + worker.onmessage = function(event) { + is(event.target, worker, "event.target should be a worker!"); + + if (event.data.type == 'finish') { + info("All done!"); + SimpleTest.finish(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } else if (event.data.type == 'feedback') { + info(event.data.msg); + document.getElementById('feedback').innerHTML += event.data.msg + "\n"; + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + info("error!"); + ok(false, "Worker had an error: " + event.data); + SimpleTest.finish(); + }; + + worker.postMessage('foobar'); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_websocket_basic.html b/dom/workers/test/test_websocket_basic.html new file mode 100644 index 000000000..5c9f8bf10 --- /dev/null +++ b/dom/workers/test/test_websocket_basic.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebSocket object in workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("websocket_basic_worker.js"); + + worker.onmessage = function(event) { + is(event.target, worker); + + if (event.data.type == 'finish') { + runTest(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + ok(false, "Worker had an error: " + event.data); + SimpleTest.finish(); + }; + + var tests = [ + function() { worker.postMessage(0); }, + function() { worker.postMessage(1); } + ]; + + function runTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); + } + + runTest(); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_websocket_https.html b/dom/workers/test/test_websocket_https.html new file mode 100644 index 000000000..aa94141fd --- /dev/null +++ b/dom/workers/test/test_websocket_https.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that creating insecure websockets from https workers is not possible</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" language="javascript"> + + onmessage = function(event) { + is(event.data, "not created", "WebSocket object must not be created"); + SimpleTest.finish(); + }; + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +<iframe src="https://example.com/tests/dom/workers/test/websocket_https.html"></iframe> +</body> +</html> diff --git a/dom/workers/test/test_websocket_loadgroup.html b/dom/workers/test/test_websocket_loadgroup.html new file mode 100644 index 000000000..3c65e262b --- /dev/null +++ b/dom/workers/test/test_websocket_loadgroup.html @@ -0,0 +1,61 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for WebSocket object in workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var worker = new Worker("websocket_loadgroup_worker.js"); + + var stopped = false; + worker.onmessage = function(e) { + if (e.data == 'opened') { + stopped = true; + window.stop(); + } else if (e.data == 'closed') { + ok(stopped, "Good!"); + stopped = false; + runTest(); + } else { + ok(false, "An error has been received"); + } + }; + + worker.onerror = function(event) { + is(event.target, worker); + ok(false, "Worker had an error: " + event.data); + SimpleTest.finish(); + }; + + var tests = [ + function() { worker.postMessage(0); }, + function() { worker.postMessage(1); } + ]; + + function runTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + test(); + } + + runTest(); + SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/workers/test/test_worker_interfaces.html b/dom/workers/test/test_worker_interfaces.html new file mode 100644 index 000000000..26af63e51 --- /dev/null +++ b/dom/workers/test/test_worker_interfaces.html @@ -0,0 +1,16 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Validate Interfaces Exposed to Workers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="worker_driver.js"></script> +</head> +<body> +<script class="testbody" type="text/javascript"> +workerTestExec("test_worker_interfaces.js"); +</script> +</body> +</html> diff --git a/dom/workers/test/test_worker_interfaces.js b/dom/workers/test/test_worker_interfaces.js new file mode 100644 index 000000000..e0647682c --- /dev/null +++ b/dom/workers/test/test_worker_interfaces.js @@ -0,0 +1,291 @@ +// This is a list of all interfaces that are exposed to workers. +// Please only add things to this list with great care and proper review +// from the associated module peers. + +// This file lists global interfaces we want exposed and verifies they +// are what we intend. Each entry in the arrays below can either be a +// simple string with the interface name, or an object with a 'name' +// property giving the interface name as a string, and additional +// properties which qualify the exposure of that interface. For example: +// +// [ +// "AGlobalInterface", +// { name: "ExperimentalThing", release: false }, +// { name: "ReallyExperimentalThing", nightly: true }, +// { name: "DesktopOnlyThing", desktop: true }, +// { name: "FancyControl", xbl: true }, +// { name: "DisabledEverywhere", disabled: true }, +// ]; +// +// See createInterfaceMap() below for a complete list of properties. + +// IMPORTANT: Do not change this list without review from +// a JavaScript Engine peer! +var ecmaGlobals = + [ + "Array", + "ArrayBuffer", + "Boolean", + "DataView", + "Date", + "Error", + "EvalError", + "Float32Array", + "Float64Array", + "Function", + "Infinity", + "Int16Array", + "Int32Array", + "Int8Array", + "InternalError", + {name: "Intl", android: false}, + "Iterator", + "JSON", + "Map", + "Math", + "NaN", + "Number", + "Object", + "Promise", + "Proxy", + "RangeError", + "ReferenceError", + "Reflect", + "RegExp", + "Set", + {name: "SharedArrayBuffer", release: false}, + {name: "SIMD", nightly: true}, + {name: "Atomics", release: false}, + "StopIteration", + "String", + "Symbol", + "SyntaxError", + {name: "TypedObject", nightly: true}, + "TypeError", + "Uint16Array", + "Uint32Array", + "Uint8Array", + "Uint8ClampedArray", + "URIError", + "WeakMap", + "WeakSet", + ]; +// IMPORTANT: Do not change the list above without review from +// a JavaScript Engine peer! + +// IMPORTANT: Do not change the list below without review from a DOM peer! +var interfaceNamesInGlobalScope = + [ +// IMPORTANT: Do not change this list without review from a DOM peer! + "Blob", +// IMPORTANT: Do not change this list without review from a DOM peer! + "BroadcastChannel", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Cache", +// IMPORTANT: Do not change this list without review from a DOM peer! + "CacheStorage", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Crypto", +// IMPORTANT: Do not change this list without review from a DOM peer! + "CustomEvent", +// IMPORTANT: Do not change this list without review from a DOM peer! + "DedicatedWorkerGlobalScope", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Directory", +// IMPORTANT: Do not change this list without review from a DOM peer! + "DOMCursor", +// IMPORTANT: Do not change this list without review from a DOM peer! + "DOMError", +// IMPORTANT: Do not change this list without review from a DOM peer! + "DOMException", +// IMPORTANT: Do not change this list without review from a DOM peer! + "DOMRequest", +// IMPORTANT: Do not change this list without review from a DOM peer! + "DOMStringList", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Event", +// IMPORTANT: Do not change this list without review from a DOM peer! + "EventTarget", +// IMPORTANT: Do not change this list without review from a DOM peer! + "File", +// IMPORTANT: Do not change this list without review from a DOM peer! + "FileReader", +// IMPORTANT: Do not change this list without review from a DOM peer! + "FileReaderSync", +// IMPORTANT: Do not change this list without review from a DOM peer! + "FormData", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Headers", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBCursor", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBCursorWithValue", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBDatabase", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBFactory", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBIndex", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBKeyRange", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBObjectStore", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBOpenDBRequest", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBRequest", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBTransaction", +// IMPORTANT: Do not change this list without review from a DOM peer! + "IDBVersionChangeEvent", +// IMPORTANT: Do not change this list without review from a DOM peer! + "ImageBitmap", +// IMPORTANT: Do not change this list without review from a DOM peer! + "ImageBitmapRenderingContext", +// IMPORTANT: Do not change this list without review from a DOM peer! + "ImageData", +// IMPORTANT: Do not change this list without review from a DOM peer! + "MessageChannel", +// IMPORTANT: Do not change this list without review from a DOM peer! + "MessageEvent", +// IMPORTANT: Do not change this list without review from a DOM peer! + "MessagePort", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Notification", +// IMPORTANT: Do not change this list without review from a DOM peer! + { name: "OffscreenCanvas", disabled: true }, +// IMPORTANT: Do not change this list without review from a DOM peer! + "Performance", +// IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceEntry", +// IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceMark", +// IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceMeasure", +// IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PerformanceObserver", nightly: true }, +// IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PerformanceObserverEntryList", nightly: true }, +// IMPORTANT: Do not change this list without review from a DOM peer! + "Request", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Response", +// IMPORTANT: Do not change this list without review from a DOM peer! + {name: "StorageManager", nightly: true}, +// IMPORTANT: Do not change this list without review from a DOM peer! + "SubtleCrypto", +// IMPORTANT: Do not change this list without review from a DOM peer! + "TextDecoder", +// IMPORTANT: Do not change this list without review from a DOM peer! + "TextEncoder", +// IMPORTANT: Do not change this list without review from a DOM peer! + "XMLHttpRequest", +// IMPORTANT: Do not change this list without review from a DOM peer! + "XMLHttpRequestEventTarget", +// IMPORTANT: Do not change this list without review from a DOM peer! + "XMLHttpRequestUpload", +// IMPORTANT: Do not change this list without review from a DOM peer! + "URL", +// IMPORTANT: Do not change this list without review from a DOM peer! + "URLSearchParams", +// IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLActiveInfo", disabled: true }, +// IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLBuffer", disabled: true }, +// IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLContextEvent", disabled: true }, +// IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLFramebuffer", disabled: true }, +// IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLProgram", disabled: true }, +// IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLRenderbuffer", disabled: true }, +// IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLRenderingContext", disabled: true }, +// IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLShader", disabled: true }, +// IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLShaderPrecisionFormat", disabled: true }, +// IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLTexture", disabled: true }, +// IMPORTANT: Do not change this list without review from a DOM peer! + { name: "WebGLUniformLocation", disabled: true }, +// IMPORTANT: Do not change this list without review from a DOM peer! + "WebSocket", +// IMPORTANT: Do not change this list without review from a DOM peer! + "Worker", +// IMPORTANT: Do not change this list without review from a DOM peer! + "WorkerGlobalScope", +// IMPORTANT: Do not change this list without review from a DOM peer! + "WorkerLocation", +// IMPORTANT: Do not change this list without review from a DOM peer! + "WorkerNavigator", +// IMPORTANT: Do not change this list without review from a DOM peer! + ]; +// IMPORTANT: Do not change the list above without review from a DOM peer! + +function createInterfaceMap(version, userAgent) { + var isNightly = version.endsWith("a1"); + var isRelease = !version.includes("a"); + var isDesktop = !/Mobile|Tablet/.test(userAgent); + var isAndroid = !!navigator.userAgent.includes("Android"); + + var interfaceMap = {}; + + function addInterfaces(interfaces) + { + for (var entry of interfaces) { + if (typeof(entry) === "string") { + interfaceMap[entry] = true; + } else { + ok(!("pref" in entry), "Bogus pref annotation for " + entry.name); + if ((entry.nightly === !isNightly) || + (entry.nightlyAndroid === !(isAndroid && isNightly) && isAndroid) || + (entry.desktop === !isDesktop) || + (entry.android === !isAndroid && !entry.nightlyAndroid) || + (entry.release === !isRelease) || + entry.disabled) { + interfaceMap[entry.name] = false; + } else { + interfaceMap[entry.name] = true; + } + } + } + } + + addInterfaces(ecmaGlobals); + addInterfaces(interfaceNamesInGlobalScope); + + return interfaceMap; +} + +function runTest(version, userAgent) { + var interfaceMap = createInterfaceMap(version, userAgent); + for (var name of Object.getOwnPropertyNames(self)) { + // An interface name should start with an upper case character. + if (!/^[A-Z]/.test(name)) { + continue; + } + ok(interfaceMap[name], + "If this is failing: DANGER, are you sure you want to expose the new interface " + name + + " to all webpages as a property on the worker? Do not make a change to this file without a " + + " review from a DOM peer for that specific change!!! (or a JS peer for changes to ecmaGlobals)"); + delete interfaceMap[name]; + } + for (var name of Object.keys(interfaceMap)) { + ok(name in self === interfaceMap[name], + name + " should " + (interfaceMap[name] ? "" : " NOT") + " be defined on the global scope"); + if (!interfaceMap[name]) { + delete interfaceMap[name]; + } + } + is(Object.keys(interfaceMap).length, 0, + "The following interface(s) are not enumerated: " + Object.keys(interfaceMap).join(", ")); +} + +workerTestGetVersion(function(version) { + workerTestGetUserAgent(function(userAgent) { + runTest(version, userAgent); + workerTestDone(); + }); +}); diff --git a/dom/workers/test/test_workersDisabled.html b/dom/workers/test/test_workersDisabled.html new file mode 100644 index 000000000..39a7fcc5b --- /dev/null +++ b/dom/workers/test/test_workersDisabled.html @@ -0,0 +1,54 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> + <head> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"> + </script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + + const enabledPref = "dom.workers.enabled"; + + is(SpecialPowers.getBoolPref(enabledPref), true, + "Workers should be enabled."); + + SpecialPowers.pushPrefEnv({"set": [[enabledPref, false]]}, test1); + + function test1() { + ok(!("Worker" in window), "Worker constructor should not be available."); + + var exception; + try { + var worker = new Worker("workersDisabled_worker.js"); + } + catch(e) { + exception = e; + } + + ok(exception, "Shouldn't be able to make a worker."); + + SpecialPowers.pushPrefEnv({"set": [[enabledPref, true]]}, test2); + } + + function test2() { + ok(("Worker" in window), "Worker constructor should be available."); + + const message = "Hi"; + + var worker = new Worker("workersDisabled_worker.js"); + worker.onmessage = function(event) { + is(event.data, message, "Good message."); + SimpleTest.finish(); + } + worker.postMessage(message); + } + </script> + </body> +</html> + diff --git a/dom/workers/test/test_workersDisabled.xul b/dom/workers/test/test_workersDisabled.xul new file mode 100644 index 000000000..1674f819f --- /dev/null +++ b/dom/workers/test/test_workersDisabled.xul @@ -0,0 +1,49 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="DOM Worker Threads Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="dom_worker_helper.js"/> + + <script type="application/javascript"> + <![CDATA[ + function test() + { + const enabledPref = "dom.workers.enabled"; + const message = "Hi"; + + var prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); + is(prefs.getBoolPref(enabledPref), true, "Workers should be enabled."); + + prefs.setBoolPref(enabledPref, false); + + ok("Worker" in window, "Worker constructor should be available."); + ok("ChromeWorker" in window, + "ChromeWorker constructor should be available."); + + var worker = new ChromeWorker("workersDisabled_worker.js"); + worker.onmessage = function(event) { + is(event.data, message, "Good message."); + prefs.clearUserPref(enabledPref); + finish(); + } + worker.postMessage(message); + + waitForWorkerFinish(); + } + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + </body> + <label id="test-result"/> +</window> diff --git a/dom/workers/test/threadErrors_worker1.js b/dom/workers/test/threadErrors_worker1.js new file mode 100644 index 000000000..c0ddade82 --- /dev/null +++ b/dom/workers/test/threadErrors_worker1.js @@ -0,0 +1,8 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// Syntax error +onmessage = function(event) { + for (var i = 0; i < 10) { } +} diff --git a/dom/workers/test/threadErrors_worker2.js b/dom/workers/test/threadErrors_worker2.js new file mode 100644 index 000000000..0462d9668 --- /dev/null +++ b/dom/workers/test/threadErrors_worker2.js @@ -0,0 +1,8 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// Bad function error +onmessage = function(event) { + foopy(); +} diff --git a/dom/workers/test/threadErrors_worker3.js b/dom/workers/test/threadErrors_worker3.js new file mode 100644 index 000000000..151ffbe5e --- /dev/null +++ b/dom/workers/test/threadErrors_worker3.js @@ -0,0 +1,9 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// Unhandled exception in body +onmessage = function(event) { +}; + +throw new Error("Bah!"); diff --git a/dom/workers/test/threadErrors_worker4.js b/dom/workers/test/threadErrors_worker4.js new file mode 100644 index 000000000..4518ce017 --- /dev/null +++ b/dom/workers/test/threadErrors_worker4.js @@ -0,0 +1,8 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +// Throwing message listener +onmessage = function(event) { + throw new Error("Bah!"); +}; diff --git a/dom/workers/test/threadTimeouts_worker.js b/dom/workers/test/threadTimeouts_worker.js new file mode 100644 index 000000000..7aaac03d2 --- /dev/null +++ b/dom/workers/test/threadTimeouts_worker.js @@ -0,0 +1,44 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +var gTimeoutId; +var gTimeoutCount = 0; +var gIntervalCount = 0; + +function timeoutFunc() { + if (++gTimeoutCount > 1) { + throw new Error("Timeout called more than once!"); + } + postMessage("timeoutFinished"); +} + +function intervalFunc() { + if (++gIntervalCount == 2) { + postMessage("intervalFinished"); + } +} + +function messageListener(event) { + switch (event.data) { + case "startTimeout": + gTimeoutId = setTimeout(timeoutFunc, 2000); + clearTimeout(gTimeoutId); + gTimeoutId = setTimeout(timeoutFunc, 2000); + break; + case "startInterval": + gTimeoutId = setInterval(intervalFunc, 2000); + break; + case "cancelInterval": + clearInterval(gTimeoutId); + postMessage("intervalCanceled"); + break; + case "startExpression": + setTimeout("this.postMessage('expressionFinished');", 2000); + break; + default: + throw "Bad message: " + event.data; + } +} + +addEventListener("message", messageListener, false); diff --git a/dom/workers/test/throwingOnerror_worker.js b/dom/workers/test/throwingOnerror_worker.js new file mode 100644 index 000000000..07e01787b --- /dev/null +++ b/dom/workers/test/throwingOnerror_worker.js @@ -0,0 +1,15 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onerror = function(event) { + throw "bar"; +}; + +var count = 0; +onmessage = function(event) { + if (!count++) { + throw "foo"; + } + postMessage(""); +}; diff --git a/dom/workers/test/timeoutTracing_worker.js b/dom/workers/test/timeoutTracing_worker.js new file mode 100644 index 000000000..d62cc50c5 --- /dev/null +++ b/dom/workers/test/timeoutTracing_worker.js @@ -0,0 +1,13 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +onmessage = function(event) { + throw "No messages should reach me!"; +} + +setInterval(function() { postMessage("Still alive!"); }, 20); +setInterval(";", 20); + +postMessage("Begin!"); diff --git a/dom/workers/test/transferable_worker.js b/dom/workers/test/transferable_worker.js new file mode 100644 index 000000000..3caf6121b --- /dev/null +++ b/dom/workers/test/transferable_worker.js @@ -0,0 +1,23 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +onmessage = function(event) { + if ("notEmpty" in event.data && "byteLength" in event.data.notEmpty) { + postMessage({ event: "W: NotEmpty object received: " + event.data.notEmpty.byteLength, + status: event.data.notEmpty.byteLength != 0, last: false }); + } + + var ab = new ArrayBuffer(event.data.size); + postMessage({ event: "W: The size is: " + event.data.size + " == " + ab.byteLength, + status: ab.byteLength == event.data.size, last: false }); + + postMessage({ event: "W: postMessage with arrayBuffer", status: true, + notEmpty: ab, ab: ab, bc: [ ab, ab, { dd: ab } ] }, [ab]); + + postMessage({ event: "W: The size is: 0 == " + ab.byteLength, + status: ab.byteLength == 0, last: false }); + + postMessage({ event: "W: last one!", status: true, last: true }); +} diff --git a/dom/workers/test/webSocket_sharedWorker.js b/dom/workers/test/webSocket_sharedWorker.js new file mode 100644 index 000000000..7c8fa0e12 --- /dev/null +++ b/dom/workers/test/webSocket_sharedWorker.js @@ -0,0 +1,20 @@ +onconnect = function(evt) { + var ws = new WebSocket("ws://mochi.test:8888/tests/dom/base/test/file_websocket_hello"); + + ws.onopen = function(e) { + evt.ports[0].postMessage({type: 'status', status: true, msg: 'OnOpen called' }); + ws.send("data"); + } + + ws.onclose = function(e) {} + + ws.onerror = function(e) { + evt.ports[0].postMessage({type: 'status', status: false, msg: 'onerror called!'}); + } + + ws.onmessage = function(e) { + evt.ports[0].postMessage({type: 'status', status: e.data == 'Hello world!', msg: 'Wrong data'}); + ws.close(); + evt.ports[0].postMessage({type: 'finish' }); + } +} diff --git a/dom/workers/test/websocket_basic_worker.js b/dom/workers/test/websocket_basic_worker.js new file mode 100644 index 000000000..256af569a --- /dev/null +++ b/dom/workers/test/websocket_basic_worker.js @@ -0,0 +1,39 @@ +onmessage = function(event) { + if (event.data != 0) { + var worker = new Worker('websocket_basic_worker.js'); + worker.onmessage = function(event) { + postMessage(event.data); + } + + worker.postMessage(event.data - 1); + return; + } + + status = false; + try { + if ((WebSocket instanceof Object)) { + status = true; + } + } catch(e) { + } + + postMessage({type: 'status', status: status, msg: 'WebSocket object:' + WebSocket}); + + var ws = new WebSocket("ws://mochi.test:8888/tests/dom/base/test/file_websocket_hello"); + ws.onopen = function(e) { + postMessage({type: 'status', status: true, msg: 'OnOpen called' }); + ws.send("data"); + } + + ws.onclose = function(e) {} + + ws.onerror = function(e) { + postMessage({type: 'status', status: false, msg: 'onerror called!'}); + } + + ws.onmessage = function(e) { + postMessage({type: 'status', status: e.data == 'Hello world!', msg: 'Wrong data'}); + ws.close(); + postMessage({type: 'finish' }); + } +} diff --git a/dom/workers/test/websocket_helpers.js b/dom/workers/test/websocket_helpers.js new file mode 100644 index 000000000..c2dc3f969 --- /dev/null +++ b/dom/workers/test/websocket_helpers.js @@ -0,0 +1,19 @@ +function feedback() { + postMessage({type: 'feedback', msg: "executing test: " + (current_test+1) + " of " + tests.length + " tests." }); +} + +function ok(status, msg) { + postMessage({type: 'status', status: !!status, msg: msg}); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function isnot(a, b, msg) { + ok(a != b, msg); +} + +function finish() { + postMessage({type: 'finish'}); +} diff --git a/dom/workers/test/websocket_https.html b/dom/workers/test/websocket_https.html new file mode 100644 index 000000000..549147a33 --- /dev/null +++ b/dom/workers/test/websocket_https.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + var worker = new Worker("https://example.com/tests/dom/workers/test/websocket_https_worker.js"); + + worker.onmessage = function(event) { + parent.postMessage(event.data, "*"); + }; + + worker.onerror = function(event) { + parent.postMessage("error", "*"); + }; + + worker.postMessage("start"); +</script> diff --git a/dom/workers/test/websocket_https_worker.js b/dom/workers/test/websocket_https_worker.js new file mode 100644 index 000000000..2592ed6d0 --- /dev/null +++ b/dom/workers/test/websocket_https_worker.js @@ -0,0 +1,9 @@ +onmessage = function() { + var wsCreated = true; + try { + new WebSocket("ws://mochi.test:8888/tests/dom/base/test/file_websocket_hello"); + } catch(e) { + wsCreated = false; + } + postMessage(wsCreated ? "created" : "not created"); +}; diff --git a/dom/workers/test/websocket_loadgroup_worker.js b/dom/workers/test/websocket_loadgroup_worker.js new file mode 100644 index 000000000..21cf248bc --- /dev/null +++ b/dom/workers/test/websocket_loadgroup_worker.js @@ -0,0 +1,24 @@ +onmessage = function(event) { + if (event.data != 0) { + var worker = new Worker('websocket_loadgroup_worker.js'); + worker.onmessage = function(event) { + postMessage(event.data); + } + + worker.postMessage(event.data - 1); + return; + } + + var ws = new WebSocket("ws://mochi.test:8888/tests/dom/base/test/file_websocket_hello"); + ws.onopen = function(e) { + postMessage('opened'); + } + + ws.onclose = function(e) { + postMessage('closed'); + } + + ws.onerror = function(e) { + postMessage('error'); + } +} diff --git a/dom/workers/test/websocket_worker1.js b/dom/workers/test/websocket_worker1.js new file mode 100644 index 000000000..cc2b6e825 --- /dev/null +++ b/dom/workers/test/websocket_worker1.js @@ -0,0 +1,19 @@ +importScripts('../../../dom/base/test/websocket_helpers.js'); +importScripts('../../../dom/base/test/websocket_tests.js'); +importScripts('websocket_helpers.js'); + +var tests = [ + test1, // client tries to connect to a http scheme location; + test2, // assure serialization of the connections; + test3, // client tries to connect to an non-existent ws server; + test4, // client tries to connect using a relative url; + test5, // client uses an invalid protocol value; + test6, // counter and encoding check; + test7, // onmessage event origin property check + test8, // client calls close() and the server sends the close frame (with no + // code or reason) in acknowledgement; + test9, // client closes the connection before the ws connection is established; + test10, // client sends a message before the ws connection is established; +]; + +doTest(); diff --git a/dom/workers/test/websocket_worker2.js b/dom/workers/test/websocket_worker2.js new file mode 100644 index 000000000..9dd4ae685 --- /dev/null +++ b/dom/workers/test/websocket_worker2.js @@ -0,0 +1,19 @@ +importScripts('../../../dom/base/test/websocket_helpers.js'); +importScripts('../../../dom/base/test/websocket_tests.js'); +importScripts('websocket_helpers.js'); + +var tests = [ + test11, // a simple hello echo; + test12, // client sends a message containing unpaired surrogates + test13, //server sends an invalid message; + test14, // server sends the close frame, it doesn't close the tcp connection + // and it keeps sending normal ws messages; + test15, // server closes the tcp connection, but it doesn't send the close + // frame; + test16, // client calls close() and tries to send a message; + test18, // client tries to connect to an http resource; + test19, // server closes the tcp connection before establishing the ws + // connection; +]; + +doTest(); diff --git a/dom/workers/test/websocket_worker3.js b/dom/workers/test/websocket_worker3.js new file mode 100644 index 000000000..d2811294f --- /dev/null +++ b/dom/workers/test/websocket_worker3.js @@ -0,0 +1,17 @@ +importScripts('../../../dom/base/test/websocket_helpers.js'); +importScripts('../../../dom/base/test/websocket_tests.js'); +importScripts('websocket_helpers.js'); + +var tests = [ + test24, // server rejects sub-protocol string + test25, // ctor with valid empty sub-protocol array + test26, // ctor with invalid sub-protocol array containing 1 empty element + test27, // ctor with invalid sub-protocol array containing an empty element in + // list + test28, // ctor using valid 1 element sub-protocol array + test29, // ctor using all valid 5 element sub-protocol array + test30, // ctor using valid 1 element sub-protocol array with element server + // will reject +]; + +doTest(); diff --git a/dom/workers/test/websocket_worker4.js b/dom/workers/test/websocket_worker4.js new file mode 100644 index 000000000..030ee67f5 --- /dev/null +++ b/dom/workers/test/websocket_worker4.js @@ -0,0 +1,19 @@ +importScripts('../../../dom/base/test/websocket_helpers.js'); +importScripts('../../../dom/base/test/websocket_tests.js'); +importScripts('websocket_helpers.js'); + +var tests = [ + test31, // ctor using valid 2 element sub-protocol array with 1 element server + // will reject and one server will accept + test32, // ctor using invalid sub-protocol array that contains duplicate items + test33, // test for sending/receiving custom close code (but no close reason) + test34, // test for receiving custom close code and reason + test35, // test for sending custom close code and reason + test36, // negative test for sending out of range close code + test37, // negative test for too long of a close reason + test38, // ensure extensions attribute is defined + test39, // a basic wss:// connectivity test + test40, // negative test for wss:// with no cert +]; + +doTest(); diff --git a/dom/workers/test/websocket_worker5.js b/dom/workers/test/websocket_worker5.js new file mode 100644 index 000000000..41d6e9be0 --- /dev/null +++ b/dom/workers/test/websocket_worker5.js @@ -0,0 +1,13 @@ +importScripts('../../../dom/base/test/websocket_helpers.js'); +importScripts('../../../dom/base/test/websocket_tests.js'); +importScripts('websocket_helpers.js'); + +var tests = [ + test42, // non-char utf-8 sequences + test43, // Test setting binaryType attribute + test44, // Test sending/receving binary ArrayBuffer + test46, // Test that we don't dispatch incoming msgs once in CLOSING state + test47, // Make sure onerror/onclose aren't called during close() +]; + +doTest(); diff --git a/dom/workers/test/window_suspended.html b/dom/workers/test/window_suspended.html new file mode 100644 index 000000000..b482cbe09 --- /dev/null +++ b/dom/workers/test/window_suspended.html @@ -0,0 +1,5 @@ +<script> +onpageshow = function(e) { + opener[location.search.split('?')[1]](e); +} +</script> diff --git a/dom/workers/test/worker_bug1278777.js b/dom/workers/test/worker_bug1278777.js new file mode 100644 index 000000000..c056d0254 --- /dev/null +++ b/dom/workers/test/worker_bug1278777.js @@ -0,0 +1,9 @@ +var xhr = new XMLHttpRequest(); +xhr.responseType = 'blob'; +xhr.open('GET', 'worker_bug1278777.js'); + +xhr.onload = function() { + postMessage(xhr.response instanceof Blob); +} + +xhr.send(); diff --git a/dom/workers/test/worker_bug1301094.js b/dom/workers/test/worker_bug1301094.js new file mode 100644 index 000000000..69cc25d23 --- /dev/null +++ b/dom/workers/test/worker_bug1301094.js @@ -0,0 +1,11 @@ +onmessage = function(e) { + var xhr = new XMLHttpRequest(); + xhr.open("POST", 'worker_bug1301094.js', false); + xhr.onload = function() { + self.postMessage("OK"); + }; + + var fd = new FormData(); + fd.append('file', e.data); + xhr.send(fd); +} diff --git a/dom/workers/test/worker_consoleAndBlobs.js b/dom/workers/test/worker_consoleAndBlobs.js new file mode 100644 index 000000000..41e8317ac --- /dev/null +++ b/dom/workers/test/worker_consoleAndBlobs.js @@ -0,0 +1,8 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +"use strict"; + +var b = new Blob(['123'], { type: 'foo/bar'}); +console.log({ msg: 'consoleAndBlobs', blob: b }); diff --git a/dom/workers/test/worker_driver.js b/dom/workers/test/worker_driver.js new file mode 100644 index 000000000..35eb17985 --- /dev/null +++ b/dom/workers/test/worker_driver.js @@ -0,0 +1,64 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +// +// Utility script for writing worker tests. In your main document do: +// +// <script type="text/javascript" src="worker_driver.js"></script> +// <script type="text/javascript"> +// workerTestExec('myWorkerTestCase.js') +// </script> +// +// This will then spawn a worker, define some utility functions, and then +// execute the code in myWorkerTestCase.js. You can then use these +// functions in your worker-side test: +// +// ok() - like the SimpleTest assert +// is() - like the SimpleTest assert +// workerTestDone() - like SimpleTest.finish() indicating the test is complete +// +// There are also some functions for requesting information that requires +// SpecialPowers or other main-thread-only resources: +// +// workerTestGetVersion() - request the current version string from the MT +// workerTestGetUserAgent() - request the user agent string from the MT +// workerTestGetOSCPU() - request the navigator.oscpu string from the MT +// +// For an example see test_worker_interfaces.html and test_worker_interfaces.js. + +function workerTestExec(script) { + SimpleTest.waitForExplicitFinish(); + var worker = new Worker('worker_wrapper.js'); + worker.onmessage = function(event) { + if (event.data.type == 'finish') { + SimpleTest.finish(); + + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + + } else if (event.data.type == 'getVersion') { + var result = SpecialPowers.Cc['@mozilla.org/xre/app-info;1'].getService(SpecialPowers.Ci.nsIXULAppInfo).version; + worker.postMessage({ + type: 'returnVersion', + result: result + }); + + } else if (event.data.type == 'getUserAgent') { + worker.postMessage({ + type: 'returnUserAgent', + result: navigator.userAgent + }); + } else if (event.data.type == 'getOSCPU') { + worker.postMessage({ + type: 'returnOSCPU', + result: navigator.oscpu + }); + } + } + + worker.onerror = function(event) { + ok(false, 'Worker had an error: ' + event.data); + SimpleTest.finish(); + }; + + worker.postMessage({ script: script }); +} diff --git a/dom/workers/test/worker_fileReader.js b/dom/workers/test/worker_fileReader.js new file mode 100644 index 000000000..f4e91b325 --- /dev/null +++ b/dom/workers/test/worker_fileReader.js @@ -0,0 +1,417 @@ +var testRanCounter = 0; +var expectedTestCount = 0; +var testSetupFinished = false; + +function ok(a, msg) { + postMessage({type: 'check', status: !!a, msg: msg }); +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function finish() { + postMessage({type: 'finish'}); +} + +function convertToUTF16(s) { + res = ""; + for (var i = 0; i < s.length; ++i) { + c = s.charCodeAt(i); + res += String.fromCharCode(c & 255, c >>> 8); + } + return res; +} + +function convertToUTF8(s) { + return unescape(encodeURIComponent(s)); +} + +function convertToDataURL(s) { + return "data:application/octet-stream;base64," + btoa(s); +} + +onmessage = function(message) { + is(FileReader.EMPTY, 0, "correct EMPTY value"); + is(FileReader.LOADING, 1, "correct LOADING value"); + is(FileReader.DONE, 2, "correct DONE value"); + + // List of blobs. + var asciiFile = message.data.blobs.shift(); + var binaryFile = message.data.blobs.shift(); + var nonExistingFile = message.data.blobs.shift(); + var utf8TextFile = message.data.blobs.shift(); + var utf16TextFile = message.data.blobs.shift(); + var emptyFile = message.data.blobs.shift(); + var dataUrlFile0 = message.data.blobs.shift(); + var dataUrlFile1 = message.data.blobs.shift(); + var dataUrlFile2 = message.data.blobs.shift(); + + // List of buffers for testing. + var testTextData = message.data.testTextData; + var testASCIIData = message.data.testASCIIData; + var testBinaryData = message.data.testBinaryData; + var dataurldata0 = message.data.dataurldata0; + var dataurldata1 = message.data.dataurldata1; + var dataurldata2 = message.data.dataurldata2; + + // Test that plain reading works and fires events as expected, both + // for text and binary reading + + var onloadHasRunText = false; + var onloadStartHasRunText = false; + r = new FileReader(); + is(r.readyState, FileReader.EMPTY, "correct initial text readyState"); + r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "plain reading"); + r.addEventListener("load", function() { onloadHasRunText = true }, false); + r.addEventListener("loadstart", function() { onloadStartHasRunText = true }, false); + r.readAsText(asciiFile); + is(r.readyState, FileReader.LOADING, "correct loading text readyState"); + is(onloadHasRunText, false, "text loading must be async"); + is(onloadStartHasRunText, true, "text loadstart should fire sync"); + expectedTestCount++; + + var onloadHasRunBinary = false; + var onloadStartHasRunBinary = false; + r = new FileReader(); + is(r.readyState, FileReader.EMPTY, "correct initial binary readyState"); + r.addEventListener("load", function() { onloadHasRunBinary = true }, false); + r.addEventListener("loadstart", function() { onloadStartHasRunBinary = true }, false); + r.readAsBinaryString(binaryFile); + r.onload = getLoadHandler(testBinaryData, testBinaryData.length, "binary reading"); + is(r.readyState, FileReader.LOADING, "correct loading binary readyState"); + is(onloadHasRunBinary, false, "binary loading must be async"); + is(onloadStartHasRunBinary, true, "binary loadstart should fire sync"); + expectedTestCount++; + + var onloadHasRunArrayBuffer = false; + var onloadStartHasRunArrayBuffer = false; + r = new FileReader(); + is(r.readyState, FileReader.EMPTY, "correct initial arrayBuffer readyState"); + r.addEventListener("load", function() { onloadHasRunArrayBuffer = true }, false); + r.addEventListener("loadstart", function() { onloadStartHasRunArrayBuffer = true }, false); + r.readAsArrayBuffer(binaryFile); + r.onload = getLoadHandlerForArrayBuffer(testBinaryData, testBinaryData.length, "array buffer reading"); + is(r.readyState, FileReader.LOADING, "correct loading arrayBuffer readyState"); + is(onloadHasRunArrayBuffer, false, "arrayBuffer loading must be async"); + is(onloadStartHasRunArrayBuffer, true, "arrayBuffer loadstart should fire sync"); + expectedTestCount++; + + // Test a variety of encodings, and make sure they work properly + r = new FileReader(); + r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "no encoding reading"); + r.readAsText(asciiFile, ""); + expectedTestCount++; + + r = new FileReader(); + r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "iso8859 reading"); + r.readAsText(asciiFile, "iso-8859-1"); + expectedTestCount++; + + r = new FileReader(); + r.onload = getLoadHandler(testTextData, + convertToUTF8(testTextData).length, + "utf8 reading"); + r.readAsText(utf8TextFile, "utf8"); + expectedTestCount++; + + r = new FileReader(); + r.readAsText(utf16TextFile, "utf-16"); + r.onload = getLoadHandler(testTextData, + convertToUTF16(testTextData).length, + "utf16 reading"); + expectedTestCount++; + + // Test get result without reading + r = new FileReader(); + is(r.readyState, FileReader.EMPTY, + "readyState in test reader get result without reading"); + is(r.error, null, + "no error in test reader get result without reading"); + is(r.result, null, + "result in test reader get result without reading"); + + // Test loading an empty file works (and doesn't crash!) + r = new FileReader(); + r.onload = getLoadHandler("", 0, "empty no encoding reading"); + r.readAsText(emptyFile, ""); + expectedTestCount++; + + r = new FileReader(); + r.onload = getLoadHandler("", 0, "empty utf8 reading"); + r.readAsText(emptyFile, "utf8"); + expectedTestCount++; + + r = new FileReader(); + r.onload = getLoadHandler("", 0, "empty utf16 reading"); + r.readAsText(emptyFile, "utf-16"); + expectedTestCount++; + + r = new FileReader(); + r.onload = getLoadHandler("", 0, "empty binary string reading"); + r.readAsBinaryString(emptyFile); + expectedTestCount++; + + r = new FileReader(); + r.onload = getLoadHandlerForArrayBuffer("", 0, "empty array buffer reading"); + r.readAsArrayBuffer(emptyFile); + expectedTestCount++; + + r = new FileReader(); + r.onload = getLoadHandler(convertToDataURL(""), 0, "empt binary string reading"); + r.readAsDataURL(emptyFile); + expectedTestCount++; + + // Test reusing a FileReader to read multiple times + r = new FileReader(); + r.onload = getLoadHandler(testASCIIData, + testASCIIData.length, + "to-be-reused reading text") + var makeAnotherReadListener = function(event) { + r = event.target; + r.removeEventListener("load", makeAnotherReadListener, false); + r.onload = getLoadHandler(testASCIIData, + testASCIIData.length, + "reused reading text"); + r.readAsText(asciiFile); + }; + r.addEventListener("load", makeAnotherReadListener, false); + r.readAsText(asciiFile); + expectedTestCount += 2; + + r = new FileReader(); + r.onload = getLoadHandler(testBinaryData, + testBinaryData.length, + "to-be-reused reading binary") + var makeAnotherReadListener2 = function(event) { + r = event.target; + r.removeEventListener("load", makeAnotherReadListener2, false); + r.onload = getLoadHandler(testBinaryData, + testBinaryData.length, + "reused reading binary"); + r.readAsBinaryString(binaryFile); + }; + r.addEventListener("load", makeAnotherReadListener2, false); + r.readAsBinaryString(binaryFile); + expectedTestCount += 2; + + r = new FileReader(); + r.onload = getLoadHandler(convertToDataURL(testBinaryData), + testBinaryData.length, + "to-be-reused reading data url") + var makeAnotherReadListener3 = function(event) { + r = event.target; + r.removeEventListener("load", makeAnotherReadListener3, false); + r.onload = getLoadHandler(convertToDataURL(testBinaryData), + testBinaryData.length, + "reused reading data url"); + r.readAsDataURL(binaryFile); + }; + r.addEventListener("load", makeAnotherReadListener3, false); + r.readAsDataURL(binaryFile); + expectedTestCount += 2; + + r = new FileReader(); + r.onload = getLoadHandlerForArrayBuffer(testBinaryData, + testBinaryData.length, + "to-be-reused reading arrayBuffer") + var makeAnotherReadListener4 = function(event) { + r = event.target; + r.removeEventListener("load", makeAnotherReadListener4, false); + r.onload = getLoadHandlerForArrayBuffer(testBinaryData, + testBinaryData.length, + "reused reading arrayBuffer"); + r.readAsArrayBuffer(binaryFile); + }; + r.addEventListener("load", makeAnotherReadListener4, false); + r.readAsArrayBuffer(binaryFile); + expectedTestCount += 2; + + // Test first reading as ArrayBuffer then read as something else + // (BinaryString) and doesn't crash + r = new FileReader(); + r.onload = getLoadHandlerForArrayBuffer(testBinaryData, + testBinaryData.length, + "to-be-reused reading arrayBuffer") + var makeAnotherReadListener5 = function(event) { + r = event.target; + r.removeEventListener("load", makeAnotherReadListener5, false); + r.onload = getLoadHandler(testBinaryData, + testBinaryData.length, + "reused reading binary string"); + r.readAsBinaryString(binaryFile); + }; + r.addEventListener("load", makeAnotherReadListener5, false); + r.readAsArrayBuffer(binaryFile); + expectedTestCount += 2; + + //Test data-URI encoding on differing file sizes + is(dataurldata0.length % 3, 0, "Want to test data with length % 3 == 0"); + r = new FileReader(); + r.onload = getLoadHandler(convertToDataURL(dataurldata0), + dataurldata0.length, + "dataurl reading, %3 = 0"); + r.readAsDataURL(dataUrlFile0); + expectedTestCount++; + + is(dataurldata1.length % 3, 1, "Want to test data with length % 3 == 1"); + r = new FileReader(); + r.onload = getLoadHandler(convertToDataURL(dataurldata1), + dataurldata1.length, + "dataurl reading, %3 = 1"); + r.readAsDataURL(dataUrlFile1); + expectedTestCount++; + + is(dataurldata2.length % 3, 2, "Want to test data with length % 3 == 2"); + r = new FileReader(); + r.onload = getLoadHandler(convertToDataURL(dataurldata2), + dataurldata2.length, + "dataurl reading, %3 = 2"); + r.readAsDataURL(dataUrlFile2), + expectedTestCount++; + + + // Test abort() + var abortHasRun = false; + var loadEndHasRun = false; + r = new FileReader(); + r.onabort = function (event) { + is(abortHasRun, false, "abort should only fire once"); + is(loadEndHasRun, false, "loadend shouldn't have fired yet"); + abortHasRun = true; + is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort"); + is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads"); + is(event.target.result, null, "file data should be null on aborted reads"); + } + r.onloadend = function (event) { + is(abortHasRun, true, "abort should fire before loadend"); + is(loadEndHasRun, false, "loadend should only fire once"); + loadEndHasRun = true; + is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort"); + is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads"); + is(event.target.result, null, "file data should be null on aborted reads"); + } + r.onload = function() { ok(false, "load should not fire for aborted reads") }; + r.onerror = function() { ok(false, "error should not fire for aborted reads") }; + r.onprogress = function() { ok(false, "progress should not fire for aborted reads") }; + var abortThrew = false; + try { + r.abort(); + } catch(e) { + abortThrew = true; + } + is(abortThrew, true, "abort() must throw if not loading"); + is(abortHasRun, false, "abort() is a no-op unless loading"); + r.readAsText(asciiFile); + r.abort(); + is(abortHasRun, true, "abort should fire sync"); + is(loadEndHasRun, true, "loadend should fire sync"); + + // Test calling readAsX to cause abort() + var reuseAbortHasRun = false; + r = new FileReader(); + r.onabort = function (event) { + is(reuseAbortHasRun, false, "abort should only fire once"); + reuseAbortHasRun = true; + is(event.target.readyState, FileReader.DONE, "should be DONE while firing onabort"); + is(event.target.error.name, "AbortError", "error set to AbortError for aborted reads"); + is(event.target.result, null, "file data should be null on aborted reads"); + } + r.onload = function() { ok(false, "load should not fire for aborted reads") }; + var abortThrew = false; + try { + r.abort(); + } catch(e) { + abortThrew = true; + } + is(abortThrew, true, "abort() must throw if not loading"); + is(reuseAbortHasRun, false, "abort() is a no-op unless loading"); + r.readAsText(asciiFile); + r.readAsText(asciiFile); + is(reuseAbortHasRun, true, "abort should fire sync"); + r.onload = getLoadHandler(testASCIIData, testASCIIData.length, "reuse-as-abort reading"); + expectedTestCount++; + + + // Test reading from nonexistent files + r = new FileReader(); + var didThrow = false; + r.onerror = function (event) { + is(event.target.readyState, FileReader.DONE, "should be DONE while firing onerror"); + is(event.target.error.name, "NotFoundError", "error set to NotFoundError for nonexistent files"); + is(event.target.result, null, "file data should be null on aborted reads"); + testHasRun(); + }; + r.onload = function (event) { + is(false, "nonexistent file shouldn't load! (FIXME: bug 1122788)"); + testHasRun(); + }; + try { + r.readAsDataURL(nonExistingFile); + expectedTestCount++; + } catch(ex) { + didThrow = true; + } + // Once this test passes, we should test that onerror gets called and + // that the FileReader object is in the right state during that call. + is(didThrow, false, "shouldn't throw when opening nonexistent file, should fire error instead"); + + + function getLoadHandler(expectedResult, expectedLength, testName) { + return function (event) { + is(event.target.readyState, FileReader.DONE, + "readyState in test " + testName); + is(event.target.error, null, + "no error in test " + testName); + is(event.target.result, expectedResult, + "result in test " + testName); + is(event.lengthComputable, true, + "lengthComputable in test " + testName); + is(event.loaded, expectedLength, + "loaded in test " + testName); + is(event.total, expectedLength, + "total in test " + testName); + testHasRun(); + } + } + + function getLoadHandlerForArrayBuffer(expectedResult, expectedLength, testName) { + return function (event) { + is(event.target.readyState, FileReader.DONE, + "readyState in test " + testName); + is(event.target.error, null, + "no error in test " + testName); + is(event.lengthComputable, true, + "lengthComputable in test " + testName); + is(event.loaded, expectedLength, + "loaded in test " + testName); + is(event.total, expectedLength, + "total in test " + testName); + is(event.target.result.byteLength, expectedLength, + "array buffer size in test " + testName); + var u8v = new Uint8Array(event.target.result); + is(String.fromCharCode.apply(String, u8v), expectedResult, + "array buffer contents in test " + testName); + u8v = null; + is(event.target.result.byteLength, expectedLength, + "array buffer size after gc in test " + testName); + u8v = new Uint8Array(event.target.result); + is(String.fromCharCode.apply(String, u8v), expectedResult, + "array buffer contents after gc in test " + testName); + testHasRun(); + } + } + + function testHasRun() { + //alert(testRanCounter); + ++testRanCounter; + if (testRanCounter == expectedTestCount) { + is(testSetupFinished, true, "test setup should have finished; check for exceptions"); + is(onloadHasRunText, true, "onload text should have fired by now"); + is(onloadHasRunBinary, true, "onload binary should have fired by now"); + finish(); + } + } + + testSetupFinished = true; +} diff --git a/dom/workers/test/worker_referrer.js b/dom/workers/test/worker_referrer.js new file mode 100644 index 000000000..227a77caf --- /dev/null +++ b/dom/workers/test/worker_referrer.js @@ -0,0 +1,9 @@ +onmessage = function() { + importScripts(['referrer.sjs?import']); + var xhr = new XMLHttpRequest(); + xhr.open('GET', 'referrer.sjs?result', true); + xhr.onload = function() { + postMessage(xhr.responseText); + } + xhr.send(); +} diff --git a/dom/workers/test/worker_setTimeoutWith0.js b/dom/workers/test/worker_setTimeoutWith0.js new file mode 100644 index 000000000..2d8e5e6c2 --- /dev/null +++ b/dom/workers/test/worker_setTimeoutWith0.js @@ -0,0 +1,3 @@ +var x = 0; +setTimeout("x++; '\x00'; x++;"); +setTimeout("postMessage(x);"); diff --git a/dom/workers/test/worker_suspended.js b/dom/workers/test/worker_suspended.js new file mode 100644 index 000000000..3b53fcea2 --- /dev/null +++ b/dom/workers/test/worker_suspended.js @@ -0,0 +1,31 @@ +var count = 0; + +function do_magic(data) { + caches.open("test") + .then(function(cache) { + return cache.put("http://mochi.test:888/foo", new Response(data.type + "-" + count++)); + }) + .then(function() { + if (count == 1) { + postMessage("ready"); + } + + if (data.loop) { + setTimeout(function() {do_magic(data); }, 500); + } + }); +} + +onmessage = function(e) { + if (e.data.type == 'page1') { + if (e.data.count > 0) { + var a = new Worker("worker_suspended.js"); + a.postMessage({ type: "page1", count: e.data - 1 }); + a.onmessage = function() { postMessage("ready"); } + } else { + do_magic({ type: e.data.type, loop: true }); + } + } else if (e.data.type == 'page2') { + do_magic({ type: e.data.type, loop: false }); + } +} diff --git a/dom/workers/test/worker_wrapper.js b/dom/workers/test/worker_wrapper.js new file mode 100644 index 000000000..c385803c4 --- /dev/null +++ b/dom/workers/test/worker_wrapper.js @@ -0,0 +1,99 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +// +// Worker-side wrapper script for the worker_driver.js helper code. See +// the comments at the top of worker_driver.js for more information. + +function ok(a, msg) { + dump("OK: " + !!a + " => " + a + ": " + msg + "\n"); + postMessage({type: 'status', status: !!a, msg: a + ": " + msg }); +} + +function is(a, b, msg) { + dump("IS: " + (a===b) + " => " + a + " | " + b + ": " + msg + "\n"); + postMessage({type: 'status', status: a === b, msg: a + " === " + b + ": " + msg }); +} + +function workerTestArrayEquals(a, b) { + if (!Array.isArray(a) || !Array.isArray(b) || a.length != b.length) { + return false; + } + for (var i = 0, n = a.length; i < n; ++i) { + if (a[i] !== b[i]) { + return false; + } + } + return true; +} + +function workerTestDone() { + postMessage({ type: 'finish' }); +} + +function workerTestGetPermissions(permissions, cb) { + addEventListener('message', function workerTestGetPermissionsCB(e) { + if (e.data.type != 'returnPermissions' || + !workerTestArrayEquals(permissions, e.data.permissions)) { + return; + } + removeEventListener('message', workerTestGetPermissionsCB); + cb(e.data.result); + }); + postMessage({ + type: 'getPermissions', + permissions: permissions + }); +} + +function workerTestGetVersion(cb) { + addEventListener('message', function workerTestGetVersionCB(e) { + if (e.data.type !== 'returnVersion') { + return; + } + removeEventListener('message', workerTestGetVersionCB); + cb(e.data.result); + }); + postMessage({ + type: 'getVersion' + }); +} + +function workerTestGetUserAgent(cb) { + addEventListener('message', function workerTestGetUserAgentCB(e) { + if (e.data.type !== 'returnUserAgent') { + return; + } + removeEventListener('message', workerTestGetUserAgentCB); + cb(e.data.result); + }); + postMessage({ + type: 'getUserAgent' + }); +} + +function workerTestGetOSCPU(cb) { + addEventListener('message', function workerTestGetOSCPUCB(e) { + if (e.data.type !== 'returnOSCPU') { + return; + } + removeEventListener('message', workerTestGetOSCPUCB); + cb(e.data.result); + }); + postMessage({ + type: 'getOSCPU' + }); +} + +addEventListener('message', function workerWrapperOnMessage(e) { + removeEventListener('message', workerWrapperOnMessage); + var data = e.data; + try { + importScripts(data.script); + } catch(e) { + postMessage({ + type: 'status', + status: false, + msg: 'worker failed to import ' + data.script + "; error: " + e.message + }); + } +}); diff --git a/dom/workers/test/workersDisabled_worker.js b/dom/workers/test/workersDisabled_worker.js new file mode 100644 index 000000000..7346fc142 --- /dev/null +++ b/dom/workers/test/workersDisabled_worker.js @@ -0,0 +1,7 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ +onmessage = function(event) { + postMessage(event.data); +} diff --git a/dom/workers/test/xpcshell/data/chrome.manifest b/dom/workers/test/xpcshell/data/chrome.manifest new file mode 100644 index 000000000..611e81fd4 --- /dev/null +++ b/dom/workers/test/xpcshell/data/chrome.manifest @@ -0,0 +1 @@ +content workers ./ diff --git a/dom/workers/test/xpcshell/data/worker.js b/dom/workers/test/xpcshell/data/worker.js new file mode 100644 index 000000000..9a83bb128 --- /dev/null +++ b/dom/workers/test/xpcshell/data/worker.js @@ -0,0 +1,7 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + + +self.onmessage = function(msg) { + self.postMessage("OK"); +}; diff --git a/dom/workers/test/xpcshell/data/worker_fileReader.js b/dom/workers/test/xpcshell/data/worker_fileReader.js new file mode 100644 index 000000000..b27366d17 --- /dev/null +++ b/dom/workers/test/xpcshell/data/worker_fileReader.js @@ -0,0 +1,8 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + + +self.onmessage = function(msg) { + var fr = new FileReader(); + self.postMessage("OK"); +}; diff --git a/dom/workers/test/xpcshell/test_fileReader.js b/dom/workers/test/xpcshell/test_fileReader.js new file mode 100644 index 000000000..0e283fbe0 --- /dev/null +++ b/dom/workers/test/xpcshell/test_fileReader.js @@ -0,0 +1,40 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +Components.utils.import("resource://gre/modules/Promise.jsm"); + +// Worker must be loaded from a chrome:// uri, not a file:// +// uri, so we first need to load it. +var WORKER_SOURCE_URI = "chrome://workers/content/worker_fileReader.js"; +do_load_manifest("data/chrome.manifest"); + +function run_test() { + run_next_test(); +} + +function talk_with_worker(worker) { + let deferred = Promise.defer(); + worker.onmessage = function(event) { + let success = true; + if (event.data == "OK") { + deferred.resolve(); + } else { + success = false; + deferred.reject(event); + } + do_check_true(success); + worker.terminate(); + }; + worker.onerror = function(event) { + let error = new Error(event.message, event.filename, event.lineno); + worker.terminate(); + deferred.reject(error); + }; + worker.postMessage("START"); + return deferred.promise; +} + + +add_task(function test_chrome_worker() { + return talk_with_worker(new ChromeWorker(WORKER_SOURCE_URI)); +}); diff --git a/dom/workers/test/xpcshell/test_workers.js b/dom/workers/test/xpcshell/test_workers.js new file mode 100644 index 000000000..c06e62af3 --- /dev/null +++ b/dom/workers/test/xpcshell/test_workers.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +Components.utils.import("resource://gre/modules/Promise.jsm"); + +// Worker must be loaded from a chrome:// uri, not a file:// +// uri, so we first need to load it. +var WORKER_SOURCE_URI = "chrome://workers/content/worker.js"; +do_load_manifest("data/chrome.manifest"); + +function run_test() { + run_next_test(); +} + +function talk_with_worker(worker) { + let deferred = Promise.defer(); + worker.onmessage = function(event) { + let success = true; + if (event.data == "OK") { + deferred.resolve(); + } else { + success = false; + deferred.reject(event); + } + do_check_true(success); + worker.terminate(); + }; + worker.onerror = function(event) { + let error = new Error(event.message, event.filename, event.lineno); + worker.terminate(); + deferred.reject(error); + }; + worker.postMessage("START"); + return deferred.promise; +} + + +add_task(function test_chrome_worker() { + return talk_with_worker(new ChromeWorker(WORKER_SOURCE_URI)); +}); + +add_task(function test_worker() { + return talk_with_worker(new Worker(WORKER_SOURCE_URI)); +}); diff --git a/dom/workers/test/xpcshell/xpcshell.ini b/dom/workers/test/xpcshell/xpcshell.ini new file mode 100644 index 000000000..917b842f5 --- /dev/null +++ b/dom/workers/test/xpcshell/xpcshell.ini @@ -0,0 +1,11 @@ +[DEFAULT] +head = +tail = +skip-if = toolkit == 'android' +support-files = + data/worker.js + data/worker_fileReader.js + data/chrome.manifest + +[test_workers.js] +[test_fileReader.js] |