diff --git a/dom/bindings/Bindings.conf b/dom/bindings/Bindings.conf index 7bc9f5adcf..c749c7c4b2 100644 --- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -200,6 +200,7 @@ DOMInterfaces = { }, 'Cache': { + 'implicitJSContext': [ 'add', 'addAll' ], 'nativeType': 'mozilla::dom::cache::Cache', }, diff --git a/dom/cache/AutoUtils.cpp b/dom/cache/AutoUtils.cpp index bdad6da2bf..f5b5a4011d 100644 --- a/dom/cache/AutoUtils.cpp +++ b/dom/cache/AutoUtils.cpp @@ -7,6 +7,8 @@ #include "mozilla/dom/cache/AutoUtils.h" #include "mozilla/unused.h" +#include "mozilla/dom/InternalHeaders.h" +#include "mozilla/dom/InternalRequest.h" #include "mozilla/dom/cache/CacheParent.h" #include "mozilla/dom/cache/CachePushStreamChild.h" #include "mozilla/dom/cache/CacheStreamControlParent.h" @@ -17,6 +19,8 @@ #include "mozilla/ipc/FileDescriptorSetChild.h" #include "mozilla/ipc/FileDescriptorSetParent.h" #include "mozilla/ipc/PBackgroundParent.h" +#include "nsCRT.h" +#include "nsHttp.h" namespace { @@ -168,15 +172,6 @@ AutoChildOpArgs::~AutoChildOpArgs() CleanupChild(args.requestOrVoid().get_CacheRequest().body(), action); break; } - case CacheOpArgs::TCacheAddAllArgs: - { - CacheAddAllArgs& args = mOpArgs.get_CacheAddAllArgs(); - auto& list = args.requestList(); - for (uint32_t i = 0; i < list.Length(); ++i) { - CleanupChild(list[i].body(), action); - } - break; - } case CacheOpArgs::TCachePutAllArgs: { CachePutAllArgs& args = mOpArgs.get_CachePutAllArgs(); @@ -216,8 +211,7 @@ AutoChildOpArgs::~AutoChildOpArgs() void AutoChildOpArgs::Add(InternalRequest* aRequest, BodyAction aBodyAction, - ReferrerAction aReferrerAction, SchemeAction aSchemeAction, - ErrorResult& aRv) + SchemeAction aSchemeAction, ErrorResult& aRv) { MOZ_ASSERT(!mSent); @@ -226,7 +220,7 @@ AutoChildOpArgs::Add(InternalRequest* aRequest, BodyAction aBodyAction, { CacheMatchArgs& args = mOpArgs.get_CacheMatchArgs(); mTypeUtils->ToCacheRequest(args.request(), aRequest, aBodyAction, - aReferrerAction, aSchemeAction, aRv); + aSchemeAction, aRv); break; } case CacheOpArgs::TCacheMatchAllArgs: @@ -235,35 +229,14 @@ AutoChildOpArgs::Add(InternalRequest* aRequest, BodyAction aBodyAction, MOZ_ASSERT(args.requestOrVoid().type() == CacheRequestOrVoid::Tvoid_t); args.requestOrVoid() = CacheRequest(); mTypeUtils->ToCacheRequest(args.requestOrVoid().get_CacheRequest(), - aRequest, aBodyAction, aReferrerAction, - aSchemeAction, aRv); - break; - } - case CacheOpArgs::TCacheAddAllArgs: - { - CacheAddAllArgs& args = mOpArgs.get_CacheAddAllArgs(); - - // The FileDescriptorSetChild asserts in its destructor that all fds have - // been removed. The copy constructor, however, simply duplicates the - // fds without removing any. This means each temporary and copy must be - // explicitly cleaned up. - // - // Avoid a lot of this hassle by making sure we only create one here. On - // error we remove it. - CacheRequest& request = *args.requestList().AppendElement(); - - mTypeUtils->ToCacheRequest(request, aRequest, aBodyAction, - aReferrerAction, aSchemeAction, aRv); - if (aRv.Failed()) { - args.requestList().RemoveElementAt(args.requestList().Length() - 1); - } + aRequest, aBodyAction, aSchemeAction, aRv); break; } case CacheOpArgs::TCacheDeleteArgs: { CacheDeleteArgs& args = mOpArgs.get_CacheDeleteArgs(); mTypeUtils->ToCacheRequest(args.request(), aRequest, aBodyAction, - aReferrerAction, aSchemeAction, aRv); + aSchemeAction, aRv); break; } case CacheOpArgs::TCacheKeysArgs: @@ -272,15 +245,14 @@ AutoChildOpArgs::Add(InternalRequest* aRequest, BodyAction aBodyAction, MOZ_ASSERT(args.requestOrVoid().type() == CacheRequestOrVoid::Tvoid_t); args.requestOrVoid() = CacheRequest(); mTypeUtils->ToCacheRequest(args.requestOrVoid().get_CacheRequest(), - aRequest, aBodyAction, aReferrerAction, - aSchemeAction, aRv); + aRequest, aBodyAction, aSchemeAction, aRv); break; } case CacheOpArgs::TStorageMatchArgs: { StorageMatchArgs& args = mOpArgs.get_StorageMatchArgs(); mTypeUtils->ToCacheRequest(args.request(), aRequest, aBodyAction, - aReferrerAction, aSchemeAction, aRv); + aSchemeAction, aRv); break; } default: @@ -288,10 +260,112 @@ AutoChildOpArgs::Add(InternalRequest* aRequest, BodyAction aBodyAction, } } +namespace { + +bool +MatchInPutList(InternalRequest* aRequest, + const nsTArray& aPutList) +{ + MOZ_ASSERT(aRequest); + + // This method implements the SW spec QueryCache algorithm against an + // in memory array of Request/Response objects. This essentially the + // same algorithm that is implemented in DBSchema.cpp. Unfortunately + // we cannot unify them because when operating against the real database + // we don't want to load all request/response objects into memory. + + // Note, we can skip the check for a invalid request method because + // Cache should only call into here with a GET or HEAD. +#ifdef DEBUG + nsAutoCString method; + aRequest->GetMethod(method); + MOZ_ASSERT(method.LowerCaseEqualsLiteral("get") || + method.LowerCaseEqualsLiteral("head")); +#endif + + nsRefPtr requestHeaders = aRequest->Headers(); + + for (uint32_t i = 0; i < aPutList.Length(); ++i) { + const CacheRequest& cachedRequest = aPutList[i].request(); + const CacheResponse& cachedResponse = aPutList[i].response(); + + nsAutoCString url; + aRequest->GetURL(url); + + // If the URLs don't match, then just skip to the next entry. + if (NS_ConvertUTF8toUTF16(url) != cachedRequest.url()) { + continue; + } + + nsRefPtr cachedRequestHeaders = + TypeUtils::ToInternalHeaders(cachedRequest.headers()); + + nsRefPtr cachedResponseHeaders = + TypeUtils::ToInternalHeaders(cachedResponse.headers()); + + nsAutoTArray varyHeaders; + ErrorResult rv; + cachedResponseHeaders->GetAll(NS_LITERAL_CSTRING("vary"), varyHeaders, rv); + MOZ_ALWAYS_TRUE(!rv.Failed()); + + // Assume the vary headers match until we find a conflict + bool varyHeadersMatch = true; + + for (uint32_t j = 0; j < varyHeaders.Length(); ++j) { + // Extract the header names inside the Vary header value. + nsAutoCString varyValue(varyHeaders[j]); + char* rawBuffer = varyValue.BeginWriting(); + char* token = nsCRT::strtok(rawBuffer, NS_HTTP_HEADER_SEPS, &rawBuffer); + bool bailOut = false; + for (; token; + token = nsCRT::strtok(rawBuffer, NS_HTTP_HEADER_SEPS, &rawBuffer)) { + nsDependentCString header(token); + MOZ_ASSERT(!header.EqualsLiteral("*"), + "We should have already caught this in " + "TypeUtils::ToPCacheResponseWithoutBody()"); + + ErrorResult headerRv; + nsAutoCString value; + requestHeaders->Get(header, value, headerRv); + if (NS_WARN_IF(headerRv.Failed())) { + headerRv.SuppressException(); + MOZ_ASSERT(value.IsEmpty()); + } + + nsAutoCString cachedValue; + cachedRequestHeaders->Get(header, value, headerRv); + if (NS_WARN_IF(headerRv.Failed())) { + headerRv.SuppressException(); + MOZ_ASSERT(cachedValue.IsEmpty()); + } + + if (value != cachedValue) { + varyHeadersMatch = false; + bailOut = true; + break; + } + } + + if (bailOut) { + break; + } + } + + // URL was equal and all vary headers match! + if (varyHeadersMatch) { + return true; + } + } + + return false; +} + +} // namespace + void AutoChildOpArgs::Add(InternalRequest* aRequest, BodyAction aBodyAction, - ReferrerAction aReferrerAction, SchemeAction aSchemeAction, - Response& aResponse, ErrorResult& aRv) + SchemeAction aSchemeAction, Response& aResponse, + ErrorResult& aRv) { MOZ_ASSERT(!mSent); @@ -300,6 +374,14 @@ AutoChildOpArgs::Add(InternalRequest* aRequest, BodyAction aBodyAction, { CachePutAllArgs& args = mOpArgs.get_CachePutAllArgs(); + // Throw an error if a request/response pair would mask another + // request/response pair in the same PutAll operation. This is + // step 2.3.2.3 from the "Batch Cache Operations" spec algorithm. + if (MatchInPutList(aRequest, args.requestResponseList())) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + // The FileDescriptorSetChild asserts in its destructor that all fds have // been removed. The copy constructor, however, simply duplicates the // fds without removing any. This means each temporary and copy must be @@ -312,7 +394,7 @@ AutoChildOpArgs::Add(InternalRequest* aRequest, BodyAction aBodyAction, pair.response().body() = void_t(); mTypeUtils->ToCacheRequest(pair.request(), aRequest, aBodyAction, - aReferrerAction, aSchemeAction, aRv); + aSchemeAction, aRv); if (!aRv.Failed()) { mTypeUtils->ToCacheResponse(pair.response(), aResponse, aRv); } diff --git a/dom/cache/AutoUtils.h b/dom/cache/AutoUtils.h index 7d92e42d24..afffb6268b 100644 --- a/dom/cache/AutoUtils.h +++ b/dom/cache/AutoUtils.h @@ -47,18 +47,15 @@ class MOZ_STACK_CLASS AutoChildOpArgs final { public: typedef TypeUtils::BodyAction BodyAction; - typedef TypeUtils::ReferrerAction ReferrerAction; typedef TypeUtils::SchemeAction SchemeAction; AutoChildOpArgs(TypeUtils* aTypeUtils, const CacheOpArgs& aOpArgs); ~AutoChildOpArgs(); void Add(InternalRequest* aRequest, BodyAction aBodyAction, - ReferrerAction aReferrerAction, SchemeAction aSchemeAction, - ErrorResult& aRv); + SchemeAction aSchemeAction, ErrorResult& aRv); void Add(InternalRequest* aRequest, BodyAction aBodyAction, - ReferrerAction aReferrerAction, SchemeAction aSchemeAction, - Response& aResponse, ErrorResult& aRv); + SchemeAction aSchemeAction, Response& aResponse, ErrorResult& aRv); const CacheOpArgs& SendAsOpArgs(); diff --git a/dom/cache/Cache.cpp b/dom/cache/Cache.cpp index 864f02dee7..55de07763d 100644 --- a/dom/cache/Cache.cpp +++ b/dom/cache/Cache.cpp @@ -9,12 +9,14 @@ #include "mozilla/dom/Headers.h" #include "mozilla/dom/InternalResponse.h" #include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseNativeHandler.h" #include "mozilla/dom/Response.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/dom/CacheBinding.h" #include "mozilla/dom/cache/AutoUtils.h" #include "mozilla/dom/cache/CacheChild.h" #include "mozilla/dom/cache/CachePushStreamChild.h" +#include "mozilla/dom/cache/Feature.h" #include "mozilla/dom/cache/ReadStream.h" #include "mozilla/ErrorResult.h" #include "mozilla/Preferences.h" @@ -22,30 +24,53 @@ #include "nsIGlobalObject.h" #include "nsNetUtil.h" +namespace mozilla { +namespace dom { +namespace cache { + +using mozilla::dom::workers::GetCurrentThreadWorkerPrivate; +using mozilla::dom::workers::WorkerPrivate; + namespace { -using mozilla::ErrorResult; -using mozilla::dom::MSG_INVALID_REQUEST_METHOD; -using mozilla::dom::OwningRequestOrUSVString; -using mozilla::dom::Request; -using mozilla::dom::RequestOrUSVString; +bool +IsValidPutRequestURL(const nsAString& aUrl, ErrorResult& aRv) +{ + bool validScheme = false; + + // make a copy because ProcessURL strips the fragmet + nsAutoString url(aUrl); + + TypeUtils::ProcessURL(url, &validScheme, nullptr, aRv); + if (aRv.Failed()) { + return false; + } + + if (!validScheme) { + NS_NAMED_LITERAL_STRING(label, "Request"); + aRv.ThrowTypeError(MSG_INVALID_URL_SCHEME, &label, &url); + return false; + } + + return true; +} static bool IsValidPutRequestMethod(const Request& aRequest, ErrorResult& aRv) { nsAutoCString method; aRequest.GetMethod(method); - bool valid = method.LowerCaseEqualsLiteral("get"); - if (!valid) { + if (!method.LowerCaseEqualsLiteral("get")) { NS_ConvertASCIItoUTF16 label(method); aRv.ThrowTypeError(MSG_INVALID_REQUEST_METHOD, &label); + return false; } - return valid; + + return true; } static bool -IsValidPutRequestMethod(const RequestOrUSVString& aRequest, - ErrorResult& aRv) +IsValidPutRequestMethod(const RequestOrUSVString& aRequest, ErrorResult& aRv) { // If the provided request is a string URL, then it will default to // a valid http method automatically. @@ -55,26 +80,132 @@ IsValidPutRequestMethod(const RequestOrUSVString& aRequest, return IsValidPutRequestMethod(aRequest.GetAsRequest(), aRv); } -static bool -IsValidPutRequestMethod(const OwningRequestOrUSVString& aRequest, - ErrorResult& aRv) -{ - if (!aRequest.IsRequest()) { - return true; - } - return IsValidPutRequestMethod(*aRequest.GetAsRequest().get(), aRv); -} - } // namespace -namespace mozilla { -namespace dom { -namespace cache { +// Helper class to wait for Add()/AddAll() fetch requests to complete and +// then perform a PutAll() with the responses. This class holds a Feature +// to keep the Worker thread alive. This is mainly to ensure that Add/AddAll +// act the same as other Cache operations that directly create a CacheOpChild +// actor. +class Cache::FetchHandler final : public PromiseNativeHandler +{ +public: + FetchHandler(Feature* aFeature, Cache* aCache, + nsTArray>&& aRequestList, Promise* aPromise) + : mFeature(aFeature) + , mCache(aCache) + , mRequestList(Move(aRequestList)) + , mPromise(aPromise) + { + MOZ_ASSERT_IF(!NS_IsMainThread(), mFeature); + MOZ_ASSERT(mCache); + MOZ_ASSERT(mPromise); + } -using mozilla::ErrorResult; -using mozilla::unused; -using mozilla::dom::workers::GetCurrentThreadWorkerPrivate; -using mozilla::dom::workers::WorkerPrivate; + virtual void + ResolvedCallback(JSContext* aCx, JS::Handle aValue) override + { + NS_ASSERT_OWNINGTHREAD(FetchHandler); + + // Stop holding the worker alive when we leave this method. + nsRefPtr feature; + feature.swap(mFeature); + + // Promise::All() passed an array of fetch() Promises should give us + // an Array of Response objects. The following code unwraps these + // JS values back to an nsTArray>. + + nsAutoTArray, 256> responseList; + responseList.SetCapacity(mRequestList.Length()); + + if (NS_WARN_IF(!JS_IsArrayObject(aCx, aValue))) { + Fail(); + return; + } + + JS::Rooted obj(aCx, &aValue.toObject()); + + uint32_t length; + if (NS_WARN_IF(!JS_GetArrayLength(aCx, obj, &length))) { + Fail(); + return; + } + + for (uint32_t i = 0; i < length; ++i) { + JS::Rooted value(aCx); + + if (NS_WARN_IF(!JS_GetElement(aCx, obj, i, &value))) { + Fail(); + return; + } + + if (NS_WARN_IF(!value.isObject())) { + Fail(); + return; + } + + JS::Rooted responseObj(aCx, &value.toObject()); + + nsRefPtr response; + nsresult rv = UNWRAP_OBJECT(Response, responseObj, response); + if (NS_WARN_IF(NS_FAILED(rv))) { + Fail(); + return; + } + + if (NS_WARN_IF(response->Type() == ResponseType::Error)) { + Fail(); + return; + } + + responseList.AppendElement(Move(response)); + } + + MOZ_ASSERT(mRequestList.Length() == responseList.Length()); + + // Now store the unwrapped Response list in the Cache. + ErrorResult result; + nsRefPtr put = mCache->PutAll(mRequestList, responseList, result); + if (NS_WARN_IF(result.Failed())) { + // TODO: abort the fetch requests we have running (bug 1157434) + mPromise->MaybeReject(result); + return; + } + + // Chain the Cache::Put() promise to the original promise returned to + // the content script. + mPromise->MaybeResolve(put); + } + + virtual void + RejectedCallback(JSContext* aCx, JS::Handle aValue) override + { + NS_ASSERT_OWNINGTHREAD(FetchHandler); + Fail(); + } + +private: + ~FetchHandler() + { + } + + void + Fail() + { + ErrorResult rv; + rv.ThrowTypeError(MSG_FETCH_FAILED); + mPromise->MaybeReject(rv); + } + + nsRefPtr mFeature; + nsRefPtr mCache; + nsTArray> mRequestList; + nsRefPtr mPromise; + + NS_DECL_ISUPPORTS +}; + +NS_IMPL_ISUPPORTS0(Cache::FetchHandler) NS_IMPL_CYCLE_COLLECTING_ADDREF(mozilla::dom::cache::Cache); NS_IMPL_CYCLE_COLLECTING_RELEASE(mozilla::dom::cache::Cache); @@ -110,7 +241,7 @@ Cache::Match(const RequestOrUSVString& aRequest, AutoChildOpArgs args(this, CacheMatchArgs(CacheRequest(), params)); - args.Add(ir, IgnoreBody, PassThroughReferrer, IgnoreInvalidScheme, aRv); + args.Add(ir, IgnoreBody, IgnoreInvalidScheme, aRv); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } @@ -136,7 +267,7 @@ Cache::MatchAll(const Optional& aRequest, return nullptr; } - args.Add(ir, IgnoreBody, PassThroughReferrer, IgnoreInvalidScheme, aRv); + args.Add(ir, IgnoreBody, IgnoreInvalidScheme, aRv); if (aRv.Failed()) { return nullptr; } @@ -146,66 +277,72 @@ Cache::MatchAll(const Optional& aRequest, } already_AddRefed -Cache::Add(const RequestOrUSVString& aRequest, ErrorResult& aRv) +Cache::Add(JSContext* aContext, const RequestOrUSVString& aRequest, + ErrorResult& aRv) { - MOZ_ASSERT(mActor); - if (!IsValidPutRequestMethod(aRequest, aRv)) { return nullptr; } - nsRefPtr ir = ToInternalRequest(aRequest, ReadBody, aRv); + GlobalObject global(aContext, mGlobal->GetGlobalJSObject()); + MOZ_ASSERT(!global.Failed()); + + nsTArray> requestList(1); + nsRefPtr request = Request::Constructor(global, aRequest, + RequestInit(), aRv); if (aRv.Failed()) { return nullptr; } - AutoChildOpArgs args(this, CacheAddAllArgs()); - - args.Add(ir, ReadBody, ExpandReferrer, NetworkErrorOnInvalidScheme, aRv); - if (aRv.Failed()) { + nsAutoString url; + request->GetUrl(url); + if (!IsValidPutRequestURL(url, aRv)) { return nullptr; } - return ExecuteOp(args, aRv); + requestList.AppendElement(Move(request)); + return AddAll(global, Move(requestList), aRv); } already_AddRefed -Cache::AddAll(const Sequence& aRequests, +Cache::AddAll(JSContext* aContext, + const Sequence& aRequestList, ErrorResult& aRv) { - MOZ_ASSERT(mActor); + GlobalObject global(aContext, mGlobal->GetGlobalJSObject()); + MOZ_ASSERT(!global.Failed()); - // If there is no work to do, then resolve immediately - if (aRequests.IsEmpty()) { - nsRefPtr promise = Promise::Create(mGlobal, aRv); - if (!promise) { - return nullptr; + nsTArray> requestList(aRequestList.Length()); + for (uint32_t i = 0; i < aRequestList.Length(); ++i) { + RequestOrUSVString requestOrString; + + if (aRequestList[i].IsRequest()) { + requestOrString.SetAsRequest() = aRequestList[i].GetAsRequest(); + if (!IsValidPutRequestMethod(requestOrString.GetAsRequest(), aRv)) { + return nullptr; + } + } else { + requestOrString.SetAsUSVString().Rebind( + aRequestList[i].GetAsUSVString().Data(), + aRequestList[i].GetAsUSVString().Length()); } - promise->MaybeResolve(JS::UndefinedHandleValue); - return promise.forget(); - } - - AutoChildOpArgs args(this, CacheAddAllArgs()); - - for (uint32_t i = 0; i < aRequests.Length(); ++i) { - if (!IsValidPutRequestMethod(aRequests[i], aRv)) { - return nullptr; - } - - nsRefPtr ir = ToInternalRequest(aRequests[i], ReadBody, - aRv); + nsRefPtr request = Request::Constructor(global, requestOrString, + RequestInit(), aRv); if (aRv.Failed()) { return nullptr; } - args.Add(ir, ReadBody, ExpandReferrer, NetworkErrorOnInvalidScheme, aRv); - if (aRv.Failed()) { + nsAutoString url; + request->GetUrl(url); + if (!IsValidPutRequestURL(url, aRv)) { return nullptr; } + + requestList.AppendElement(Move(request)); } - return ExecuteOp(args, aRv); + return AddAll(global, Move(requestList), aRv); } already_AddRefed @@ -225,7 +362,7 @@ Cache::Put(const RequestOrUSVString& aRequest, Response& aResponse, AutoChildOpArgs args(this, CachePutAllArgs()); - args.Add(ir, ReadBody, PassThroughReferrer, TypeErrorOnInvalidScheme, + args.Add(ir, ReadBody, TypeErrorOnInvalidScheme, aResponse, aRv); if (aRv.Failed()) { return nullptr; @@ -250,7 +387,7 @@ Cache::Delete(const RequestOrUSVString& aRequest, AutoChildOpArgs args(this, CacheDeleteArgs(CacheRequest(), params)); - args.Add(ir, IgnoreBody, PassThroughReferrer, IgnoreInvalidScheme, aRv); + args.Add(ir, IgnoreBody, IgnoreInvalidScheme, aRv); if (aRv.Failed()) { return nullptr; } @@ -276,7 +413,7 @@ Cache::Keys(const Optional& aRequest, return nullptr; } - args.Add(ir, IgnoreBody, PassThroughReferrer, IgnoreInvalidScheme, aRv); + args.Add(ir, IgnoreBody, IgnoreInvalidScheme, aRv); if (aRv.Failed()) { return nullptr; } @@ -375,6 +512,80 @@ Cache::ExecuteOp(AutoChildOpArgs& aOpArgs, ErrorResult& aRv) return promise.forget(); } +already_AddRefed +Cache::AddAll(const GlobalObject& aGlobal, + nsTArray>&& aRequestList, ErrorResult& aRv) +{ + MOZ_ASSERT(mActor); + + // If there is no work to do, then resolve immediately + if (aRequestList.IsEmpty()) { + nsRefPtr promise = Promise::Create(mGlobal, aRv); + if (!promise) { + return nullptr; + } + + promise->MaybeResolve(JS::UndefinedHandleValue); + return promise.forget(); + } + + nsAutoTArray, 256> fetchList; + fetchList.SetCapacity(aRequestList.Length()); + + // Begin fetching each request in parallel. For now, if an error occurs just + // abandon our previous fetch calls. In theory we could cancel them in the + // future once fetch supports it. + + for (uint32_t i = 0; i < aRequestList.Length(); ++i) { + RequestOrUSVString requestOrString; + requestOrString.SetAsRequest() = aRequestList[i]; + nsRefPtr fetch = FetchRequest(mGlobal, requestOrString, + RequestInit(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + fetchList.AppendElement(Move(fetch)); + } + + nsRefPtr promise = Promise::Create(mGlobal, aRv); + if (aRv.Failed()) { + return nullptr; + } + + nsRefPtr handler = new FetchHandler(mActor->GetFeature(), this, + Move(aRequestList), promise); + + nsRefPtr fetchPromise = Promise::All(aGlobal, fetchList, aRv); + if (aRv.Failed()) { + return nullptr; + } + fetchPromise->AppendNativeHandler(handler); + + return promise.forget(); +} + +already_AddRefed +Cache::PutAll(const nsTArray>& aRequestList, + const nsTArray>& aResponseList, + ErrorResult& aRv) +{ + MOZ_ASSERT(mActor); + MOZ_ASSERT(aRequestList.Length() == aResponseList.Length()); + + AutoChildOpArgs args(this, CachePutAllArgs()); + + for (uint32_t i = 0; i < aRequestList.Length(); ++i) { + nsRefPtr ir = aRequestList[i]->GetInternalRequest(); + args.Add(ir, ReadBody, TypeErrorOnInvalidScheme, *aResponseList[i], aRv); + if (aRv.Failed()) { + return nullptr; + } + } + + return ExecuteOp(args, aRv); +} + } // namespace cache } // namespace dom } // namespace mozilla diff --git a/dom/cache/Cache.h b/dom/cache/Cache.h index 7656c539af..f5275ac9a3 100644 --- a/dom/cache/Cache.h +++ b/dom/cache/Cache.h @@ -50,10 +50,11 @@ public: MatchAll(const Optional& aRequest, const CacheQueryOptions& aOptions, ErrorResult& aRv); already_AddRefed - Add(const RequestOrUSVString& aRequest, ErrorResult& aRv); + Add(JSContext* aContext, const RequestOrUSVString& aRequest, + ErrorResult& aRv); already_AddRefed - AddAll(const Sequence& aRequests, - ErrorResult& aRv); + AddAll(JSContext* aContext, + const Sequence& aRequests, ErrorResult& aRv); already_AddRefed Put(const RequestOrUSVString& aRequest, Response& aResponse, ErrorResult& aRv); @@ -85,6 +86,8 @@ public: CreatePushStream(nsIAsyncInputStream* aStream) override; private: + class FetchHandler; + ~Cache(); // Called when we're destroyed or CCed. @@ -93,6 +96,15 @@ private: already_AddRefed ExecuteOp(AutoChildOpArgs& aOpArgs, ErrorResult& aRv); + already_AddRefed + AddAll(const GlobalObject& aGlobal, nsTArray>&& aRequestList, + ErrorResult& aRv); + + already_AddRefed + PutAll(const nsTArray>& aRequestList, + const nsTArray>& aResponseList, + ErrorResult& aRv); + nsCOMPtr mGlobal; CacheChild* mActor; diff --git a/dom/cache/CacheOpChild.cpp b/dom/cache/CacheOpChild.cpp index dd13fa2565..30d33c2aec 100644 --- a/dom/cache/CacheOpChild.cpp +++ b/dom/cache/CacheOpChild.cpp @@ -116,7 +116,6 @@ CacheOpChild::Recv__delete__(const ErrorResult& aRv, HandleResponseList(aResult.get_CacheMatchAllResult().responseList()); break; } - case CacheOpResult::TCacheAddAllResult: case CacheOpResult::TCachePutAllResult: { mPromise->MaybeResolve(JS::UndefinedHandleValue); diff --git a/dom/cache/CacheOpParent.cpp b/dom/cache/CacheOpParent.cpp index 5e658302ca..c7e2edc910 100644 --- a/dom/cache/CacheOpParent.cpp +++ b/dom/cache/CacheOpParent.cpp @@ -72,30 +72,6 @@ CacheOpParent::Execute(Manager* aManager) mManager = aManager; - // Handle add/addAll op with a FetchPut object - if (mOpArgs.type() == CacheOpArgs::TCacheAddAllArgs) { - MOZ_ASSERT(mCacheId != INVALID_CACHE_ID); - - const CacheAddAllArgs& args = mOpArgs.get_CacheAddAllArgs(); - const nsTArray& list = args.requestList(); - - nsAutoTArray, 256> requestStreamList; - for (uint32_t i = 0; i < list.Length(); ++i) { - requestStreamList.AppendElement(DeserializeCacheStream(list[i].body())); - } - - nsRefPtr fetchPut; - nsresult rv = FetchPut::Create(this, mManager, mCacheId, list, - requestStreamList, getter_AddRefs(fetchPut)); - if (NS_WARN_IF(NS_FAILED(rv))) { - OnOpComplete(ErrorResult(rv), CacheAddAllResult()); - return; - } - - mFetchPutList.AppendElement(fetchPut.forget()); - return; - } - // Handle put op if (mOpArgs.type() == CacheOpArgs::TCachePutAllArgs) { MOZ_ASSERT(mCacheId != INVALID_CACHE_ID); @@ -151,11 +127,6 @@ CacheOpParent::ActorDestroy(ActorDestroyReason aReason) mVerifier = nullptr; } - for (uint32_t i = 0; i < mFetchPutList.Length(); ++i) { - mFetchPutList[i]->ClearListener(); - } - mFetchPutList.Clear(); - if (mManager) { mManager->RemoveListener(this); mManager = nullptr; @@ -222,18 +193,6 @@ CacheOpParent::OnOpComplete(ErrorResult&& aRv, const CacheOpResult& aResult, unused << Send__delete__(this, aRv, result.SendAsOpResult()); } -void -CacheOpParent::OnFetchPut(FetchPut* aFetchPut, ErrorResult&& aRv) -{ - NS_ASSERT_OWNINGTHREAD(CacheOpParent); - MOZ_ASSERT(aFetchPut); - - aFetchPut->ClearListener(); - MOZ_ALWAYS_TRUE(mFetchPutList.RemoveElement(aFetchPut)); - - OnOpComplete(Move(aRv), CacheAddAllResult()); -} - already_AddRefed CacheOpParent::DeserializeCacheStream(const CacheReadStreamOrVoid& aStreamOrVoid) { diff --git a/dom/cache/CacheOpParent.h b/dom/cache/CacheOpParent.h index 0e64e54bb2..ceea9eff5d 100644 --- a/dom/cache/CacheOpParent.h +++ b/dom/cache/CacheOpParent.h @@ -7,7 +7,6 @@ #ifndef mozilla_dom_cache_CacheOpParent_h #define mozilla_dom_cache_CacheOpParent_h -#include "mozilla/dom/cache/FetchPut.h" #include "mozilla/dom/cache/Manager.h" #include "mozilla/dom/cache/PCacheOpParent.h" #include "mozilla/dom/cache/PrincipalVerifier.h" @@ -23,7 +22,6 @@ namespace cache { class CacheOpParent final : public PCacheOpParent , public PrincipalVerifier::Listener , public Manager::Listener - , public FetchPut::Listener { // to allow use of convenience overrides using Manager::Listener::OnOpComplete; @@ -61,10 +59,6 @@ private: const nsTArray& aSavedRequestList, StreamList* aStreamList) override; - // FetchPut::Listener methods - virtual void - OnFetchPut(FetchPut* aFetchPut, ErrorResult&& aRv) override; - // utility methods already_AddRefed DeserializeCacheStream(const CacheReadStreamOrVoid& aStreamOrVoid); @@ -75,7 +69,6 @@ private: const CacheOpArgs mOpArgs; nsRefPtr mManager; nsRefPtr mVerifier; - nsTArray> mFetchPutList; NS_DECL_OWNINGTHREAD }; diff --git a/dom/cache/CacheParent.cpp b/dom/cache/CacheParent.cpp index 9523631127..82979b1889 100644 --- a/dom/cache/CacheParent.cpp +++ b/dom/cache/CacheParent.cpp @@ -49,7 +49,6 @@ CacheParent::AllocPCacheOpParent(const CacheOpArgs& aOpArgs) { if (aOpArgs.type() != CacheOpArgs::TCacheMatchArgs && aOpArgs.type() != CacheOpArgs::TCacheMatchAllArgs && - aOpArgs.type() != CacheOpArgs::TCacheAddAllArgs && aOpArgs.type() != CacheOpArgs::TCachePutAllArgs && aOpArgs.type() != CacheOpArgs::TCacheDeleteArgs && aOpArgs.type() != CacheOpArgs::TCacheKeysArgs) diff --git a/dom/cache/CacheStorage.cpp b/dom/cache/CacheStorage.cpp index a4ff49ed7a..cc93b82354 100644 --- a/dom/cache/CacheStorage.cpp +++ b/dom/cache/CacheStorage.cpp @@ -432,8 +432,7 @@ CacheStorage::MaybeRunPendingRequests() nsAutoPtr entry(mPendingRequests[i].forget()); AutoChildOpArgs args(this, entry->mArgs); if (entry->mRequest) { - args.Add(entry->mRequest, IgnoreBody, PassThroughReferrer, - IgnoreInvalidScheme, rv); + args.Add(entry->mRequest, IgnoreBody, IgnoreInvalidScheme, rv); } if (rv.Failed()) { entry->mPromise->MaybeReject(rv); diff --git a/dom/cache/CacheTypes.ipdlh b/dom/cache/CacheTypes.ipdlh index 750e7cbbc4..a98d94ca8c 100644 --- a/dom/cache/CacheTypes.ipdlh +++ b/dom/cache/CacheTypes.ipdlh @@ -6,6 +6,7 @@ include protocol PCache; include protocol PCachePushStream; include protocol PCacheStreamControl; include InputStreamParams; +include ChannelInfo; using HeadersGuardEnum from "mozilla/dom/cache/IPCUtils.h"; using RequestCredentials from "mozilla/dom/cache/IPCUtils.h"; @@ -25,7 +26,6 @@ struct CacheQueryParams bool ignoreSearch; bool ignoreMethod; bool ignoreVary; - bool prefixMatch; bool cacheNameSet; nsString cacheName; }; @@ -82,7 +82,7 @@ struct CacheResponse HeadersEntry[] headers; HeadersGuardEnum headersGuard; CacheReadStreamOrVoid body; - nsCString securityInfo; + IPCChannelInfo channelInfo; }; union CacheResponseOrVoid @@ -109,11 +109,6 @@ struct CacheMatchAllArgs CacheQueryParams params; }; -struct CacheAddAllArgs -{ - CacheRequest[] requestList; -}; - struct CachePutAllArgs { CacheRequestResponse[] requestResponseList; @@ -160,7 +155,6 @@ union CacheOpArgs { CacheMatchArgs; CacheMatchAllArgs; - CacheAddAllArgs; CachePutAllArgs; CacheDeleteArgs; CacheKeysArgs; @@ -181,10 +175,6 @@ struct CacheMatchAllResult CacheResponse[] responseList; }; -struct CacheAddAllResult -{ -}; - struct CachePutAllResult { }; @@ -229,7 +219,6 @@ union CacheOpResult void_t; CacheMatchResult; CacheMatchAllResult; - CacheAddAllResult; CachePutAllResult; CacheDeleteResult; CacheKeysResult; diff --git a/dom/cache/Connection.cpp b/dom/cache/Connection.cpp new file mode 100644 index 0000000000..91b2bb1882 --- /dev/null +++ b/dom/cache/Connection.cpp @@ -0,0 +1,277 @@ +/* -*- 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/cache/Connection.h" + +#include "mozilla/dom/cache/DBSchema.h" +#include "mozStorageHelper.h" + +namespace mozilla { +namespace dom { +namespace cache { + +NS_IMPL_ISUPPORTS(cache::Connection, mozIStorageAsyncConnection, + mozIStorageConnection); + +Connection::Connection(mozIStorageConnection* aBase) + : mBase(aBase) + , mClosed(false) +{ + MOZ_ASSERT(mBase); +} + +Connection::~Connection() +{ + NS_ASSERT_OWNINGTHREAD(Connection); + MOZ_ALWAYS_TRUE(NS_SUCCEEDED(Close())); +} + +NS_IMETHODIMP +Connection::Close() +{ + NS_ASSERT_OWNINGTHREAD(Connection); + + if (mClosed) { + return NS_OK; + } + mClosed = true; + + // If we are closing here, then Cache must not have a transaction + // open anywhere else. This should be guaranteed to succeed. + MOZ_ALWAYS_TRUE(NS_SUCCEEDED(db::IncrementalVacuum(this))); + + return mBase->Close(); +} + +// The following methods are all boilerplate that either forward to the +// base connection or block the method. All the async execution methods +// are blocked because Cache does not use them and they would require more +// work to wrap properly. + +// mozIStorageAsyncConnection methods + +NS_IMETHODIMP +Connection::AsyncClose(mozIStorageCompletionCallback*) +{ + // async methods are not supported + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Connection::AsyncClone(bool, mozIStorageCompletionCallback*) +{ + // async methods are not supported + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Connection::GetDatabaseFile(nsIFile** aFileOut) +{ + return mBase->GetDatabaseFile(aFileOut); +} + +NS_IMETHODIMP +Connection::CreateAsyncStatement(const nsACString&, mozIStorageAsyncStatement**) +{ + // async methods are not supported + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Connection::ExecuteAsync(mozIStorageBaseStatement**, uint32_t, + mozIStorageStatementCallback*, + mozIStoragePendingStatement**) +{ + // async methods are not supported + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Connection::ExecuteSimpleSQLAsync(const nsACString&, + mozIStorageStatementCallback*, + mozIStoragePendingStatement**) +{ + // async methods are not supported + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Connection::CreateFunction(const nsACString& aFunctionName, + int32_t aNumArguments, + mozIStorageFunction* aFunction) +{ + // async methods are not supported + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Connection::CreateAggregateFunction(const nsACString& aFunctionName, + int32_t aNumArguments, + mozIStorageAggregateFunction* aFunction) +{ + return mBase->CreateAggregateFunction(aFunctionName, aNumArguments, + aFunction); +} + +NS_IMETHODIMP +Connection::RemoveFunction(const nsACString& aFunctionName) +{ + return mBase->RemoveFunction(aFunctionName); +} + +NS_IMETHODIMP +Connection::SetProgressHandler(int32_t aGranularity, + mozIStorageProgressHandler* aHandler, + mozIStorageProgressHandler** aHandlerOut) +{ + return mBase->SetProgressHandler(aGranularity, aHandler, aHandlerOut); +} + +NS_IMETHODIMP +Connection::RemoveProgressHandler(mozIStorageProgressHandler** aHandlerOut) +{ + return mBase->RemoveProgressHandler(aHandlerOut); +} + +// mozIStorageConnection methods + +NS_IMETHODIMP +Connection::Clone(bool aReadOnly, mozIStorageConnection** aConnectionOut) +{ + nsCOMPtr conn; + nsresult rv = mBase->Clone(aReadOnly, getter_AddRefs(conn)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + nsCOMPtr wrapped = new Connection(conn); + wrapped.forget(aConnectionOut); + + return rv; +} + +NS_IMETHODIMP +Connection::GetDefaultPageSize(int32_t* aSizeOut) +{ + return mBase->GetDefaultPageSize(aSizeOut); +} + +NS_IMETHODIMP +Connection::GetConnectionReady(bool* aReadyOut) +{ + return mBase->GetConnectionReady(aReadyOut); +} + +NS_IMETHODIMP +Connection::GetLastInsertRowID(int64_t* aRowIdOut) +{ + return mBase->GetLastInsertRowID(aRowIdOut); +} + +NS_IMETHODIMP +Connection::GetAffectedRows(int32_t* aCountOut) +{ + return mBase->GetAffectedRows(aCountOut); +} + +NS_IMETHODIMP +Connection::GetLastError(int32_t* aErrorOut) +{ + return mBase->GetLastError(aErrorOut); +} + +NS_IMETHODIMP +Connection::GetLastErrorString(nsACString& aErrorOut) +{ + return mBase->GetLastErrorString(aErrorOut); +} + +NS_IMETHODIMP +Connection::GetSchemaVersion(int32_t* aVersionOut) +{ + return mBase->GetSchemaVersion(aVersionOut); +} + +NS_IMETHODIMP +Connection::SetSchemaVersion(int32_t aVersion) +{ + return mBase->SetSchemaVersion(aVersion); +} + +NS_IMETHODIMP +Connection::CreateStatement(const nsACString& aQuery, + mozIStorageStatement** aStatementOut) +{ + return mBase->CreateStatement(aQuery, aStatementOut); +} + +NS_IMETHODIMP +Connection::ExecuteSimpleSQL(const nsACString& aQuery) +{ + return mBase->ExecuteSimpleSQL(aQuery); +} + +NS_IMETHODIMP +Connection::TableExists(const nsACString& aTableName, bool* aExistsOut) +{ + return mBase->TableExists(aTableName, aExistsOut); +} + +NS_IMETHODIMP +Connection::IndexExists(const nsACString& aIndexName, bool* aExistsOut) +{ + return mBase->IndexExists(aIndexName, aExistsOut); +} + +NS_IMETHODIMP +Connection::GetTransactionInProgress(bool* aResultOut) +{ + return mBase->GetTransactionInProgress(aResultOut); +} + +NS_IMETHODIMP +Connection::BeginTransaction() +{ + return mBase->BeginTransaction(); +} + +NS_IMETHODIMP +Connection::BeginTransactionAs(int32_t aType) +{ + return mBase->BeginTransactionAs(aType); +} + +NS_IMETHODIMP +Connection::CommitTransaction() +{ + return mBase->CommitTransaction(); +} + +NS_IMETHODIMP +Connection::RollbackTransaction() +{ + return mBase->RollbackTransaction(); +} + +NS_IMETHODIMP +Connection::CreateTable(const char* aTable, const char* aSchema) +{ + return mBase->CreateTable(aTable, aSchema); +} + +NS_IMETHODIMP +Connection::SetGrowthIncrement(int32_t aIncrement, const nsACString& aDatabase) +{ + return mBase->SetGrowthIncrement(aIncrement, aDatabase); +} + +NS_IMETHODIMP +Connection::EnableModule(const nsACString& aModule) +{ + return mBase->EnableModule(aModule); +} + +} // namespace cache +} // namespace dom +} // namespace mozilla diff --git a/dom/cache/Connection.h b/dom/cache/Connection.h new file mode 100644 index 0000000000..93dc8177a0 --- /dev/null +++ b/dom/cache/Connection.h @@ -0,0 +1,36 @@ +/* -*- 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_cache_Connection_h +#define mozilla_dom_cache_Connection_h + +#include "mozIStorageConnection.h" + +namespace mozilla { +namespace dom { +namespace cache { + +class Connection final : public mozIStorageConnection +{ +public: + explicit Connection(mozIStorageConnection* aBase); + +private: + ~Connection(); + + nsCOMPtr mBase; + bool mClosed; + + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEASYNCCONNECTION + NS_DECL_MOZISTORAGECONNECTION +}; + +} // namespace cache +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_cache_Connection_h diff --git a/dom/cache/DBAction.cpp b/dom/cache/DBAction.cpp index d5a6c5b1dc..0aae9f2327 100644 --- a/dom/cache/DBAction.cpp +++ b/dom/cache/DBAction.cpp @@ -6,6 +6,9 @@ #include "mozilla/dom/cache/DBAction.h" +#include "mozilla/dom/cache/Connection.h" +#include "mozilla/dom/cache/DBSchema.h" +#include "mozilla/dom/cache/FileUtils.h" #include "mozilla/dom/quota/PersistenceType.h" #include "mozilla/net/nsFileProtocolHandler.h" #include "mozIStorageConnection.h" @@ -15,8 +18,6 @@ #include "nsIURI.h" #include "nsNetUtil.h" #include "nsThreadUtils.h" -#include "DBSchema.h" -#include "FileUtils.h" namespace mozilla { namespace dom { @@ -79,7 +80,12 @@ DBAction::RunOnTarget(Resolver* aResolver, const QuotaInfo& aQuotaInfo, // Save this connection in the shared Data object so later Actions can // use it. This avoids opening a new connection for every Action. if (aOptionalData) { - aOptionalData->SetConnection(conn); + // Since we know this connection will be around for as long as the + // Cache is open, use our special wrapped connection class. This + // will let us perform certain operations once the Cache origin + // is closed. + nsCOMPtr wrapped = new Connection(conn); + aOptionalData->SetConnection(wrapped); } } @@ -186,6 +192,9 @@ DBAction::WipeDatabase(nsIFile* aDBFile, nsIFile* aDBDir) nsresult rv = aDBFile->Remove(false); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + // Note, the -wal journal file will be automatically deleted by sqlite when + // the new database is created. No need to explicitly delete it here. + // Delete the morgue as well. rv = BodyDeleteDir(aDBDir); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } diff --git a/dom/cache/DBSchema.cpp b/dom/cache/DBSchema.cpp index 1956df9e8c..3baf7e2796 100644 --- a/dom/cache/DBSchema.cpp +++ b/dom/cache/DBSchema.cpp @@ -18,6 +18,7 @@ #include "nsTArray.h" #include "nsCRT.h" #include "nsHttp.h" +#include "nsICryptoHash.h" #include "mozilla/dom/HeadersBinding.h" #include "mozilla/dom/RequestBinding.h" #include "mozilla/dom/ResponseBinding.h" @@ -28,13 +29,30 @@ namespace dom { namespace cache { namespace db { -const int32_t kMaxWipeSchemaVersion = 6; +const int32_t kMaxWipeSchemaVersion = 10; namespace { -const int32_t kLatestSchemaVersion = 6; +const int32_t kLatestSchemaVersion = 10; const int32_t kMaxEntriesPerStatement = 255; +const uint32_t kPageSize = 4 * 1024; + +// Grow the database in chunks to reduce fragmentation +const uint32_t kGrowthSize = 32 * 1024; +const uint32_t kGrowthPages = kGrowthSize / kPageSize; +static_assert(kGrowthSize % kPageSize == 0, + "Growth size must be multiple of page size"); + +// Only release free pages when we have more than this limit +const int32_t kMaxFreePages = kGrowthPages; + +// Limit WAL journal to a reasonable size +const uint32_t kWalAutoCheckpointSize = 512 * 1024; +const uint32_t kWalAutoCheckpointPages = kWalAutoCheckpointSize / kPageSize; +static_assert(kWalAutoCheckpointSize % kPageSize == 0, + "WAL checkpoint size must be multiple of page size"); + } // namespace // If any of the static_asserts below fail, it means that you have changed @@ -151,6 +169,14 @@ namespace { typedef int32_t EntryId; +struct IdCount +{ + IdCount() : mId(-1), mCount(0) { } + explicit IdCount(int32_t aId) : mId(aId), mCount(1) { } + int32_t mId; + int32_t mCount; +}; + static nsresult QueryAll(mozIStorageConnection* aConn, CacheId aCacheId, nsTArray& aEntryIdListOut); static nsresult QueryCache(mozIStorageConnection* aConn, CacheId aCacheId, @@ -164,7 +190,14 @@ static nsresult MatchByVaryHeader(mozIStorageConnection* aConn, static nsresult DeleteEntries(mozIStorageConnection* aConn, const nsTArray& aEntryIdList, nsTArray& aDeletedBodyIdListOut, + nsTArray& aDeletedSecurityIdListOut, uint32_t aPos=0, int32_t aLen=-1); +static nsresult InsertSecurityInfo(mozIStorageConnection* aConn, + const nsACString& aData, int32_t *aIdOut); +static nsresult DeleteSecurityInfo(mozIStorageConnection* aConn, int32_t aId, + int32_t aCount); +static nsresult DeleteSecurityInfoList(mozIStorageConnection* aConn, + const nsTArray& aDeletedStorageIdList); static nsresult InsertEntry(mozIStorageConnection* aConn, CacheId aCacheId, const CacheRequest& aRequest, const nsID* aRequestBodyId, @@ -181,10 +214,14 @@ static void AppendListParamsToQuery(nsACString& aQuery, static nsresult BindListParamsToQuery(mozIStorageStatement* aState, const nsTArray& aEntryIdList, uint32_t aPos, int32_t aLen); -static nsresult BindId(mozIStorageStatement* aState, uint32_t aPos, +static nsresult BindId(mozIStorageStatement* aState, const nsACString& aName, const nsID* aId); static nsresult ExtractId(mozIStorageStatement* aState, uint32_t aPos, nsID* aIdOut); +static nsresult CreateAndBindKeyStatement(mozIStorageConnection* aConn, + const char* aQueryFormat, + const nsAString& aKey, + mozIStorageStatement** aStateOut); } // anonymous namespace nsresult @@ -193,26 +230,12 @@ CreateSchema(mozIStorageConnection* aConn) MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(aConn); - nsAutoCString pragmas( - // Enable auto-vaccum but in incremental mode in order to avoid doing a lot - // of work at the end of each transaction. - "PRAGMA auto_vacuum = INCREMENTAL; " - ); - - nsresult rv = aConn->ExecuteSimpleSQL(pragmas); - if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - int32_t schemaVersion; - rv = aConn->GetSchemaVersion(&schemaVersion); + nsresult rv = aConn->GetSchemaVersion(&schemaVersion); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (schemaVersion == kLatestSchemaVersion) { - // We already have the correct schema, so just do an incremental vaccum and - // get started. - rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( - "PRAGMA incremental_vacuum;")); - if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - + // We already have the correct schema, so just get started. return rv; } @@ -239,6 +262,24 @@ CreateSchema(mozIStorageConnection* aConn) )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + // Security blobs are quite large and duplicated for every Response from + // the same https origin. This table is used to de-duplicate this data. + rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE security_info (" + "id INTEGER NOT NULL PRIMARY KEY, " + "hash BLOB NOT NULL, " // first 8-bytes of the sha1 hash of data column + "data BLOB NOT NULL, " // full security info data, usually a few KB + "refcount INTEGER NOT NULL" + ");" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Index the smaller hash value instead of the large security data blob. + rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE INDEX security_info_hash_index ON security_info (hash);" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE entries (" "id INTEGER NOT NULL PRIMARY KEY, " @@ -259,7 +300,7 @@ CreateSchema(mozIStorageConnection* aConn) "response_status_text TEXT NOT NULL, " "response_headers_guard INTEGER NOT NULL, " "response_body_id TEXT NULL, " - "response_security_info BLOB NULL, " + "response_security_info_id INTEGER NULL REFERENCES security_info(id), " "cache_id INTEGER NOT NULL REFERENCES caches(id) ON DELETE CASCADE" ");" )); @@ -304,10 +345,13 @@ CreateSchema(mozIStorageConnection* aConn) )); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + // NOTE: key allows NULL below since that is how "" is represented + // in a BLOB column. We use BLOB to avoid encoding issues + // with storing DOMStrings. rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE storage (" "namespace INTEGER NOT NULL, " - "key TEXT NOT NULL, " + "key BLOB NULL, " "cache_id INTEGER NOT NULL REFERENCES caches(id), " "PRIMARY KEY(namespace, key) " ");" @@ -337,24 +381,69 @@ InitializeConnection(mozIStorageConnection* aConn) // This function needs to perform per-connection initialization tasks that // need to happen regardless of the schema. - nsAutoCString pragmas( -#if defined(MOZ_WIDGET_ANDROID) || defined(MOZ_WIDGET_GONK) - // Switch the journaling mode to TRUNCATE to avoid changing the directory - // structure at the conclusion of every transaction for devices with slower - // file systems. - "PRAGMA journal_mode = TRUNCATE; " -#endif - "PRAGMA foreign_keys = ON; " - - // Note, the default encoding of UTF-8 is preferred. mozStorage does all - // the work necessary to convert UTF-16 nsString values for us. We don't - // need ordering and the binary equality operations are correct. So, do - // NOT set PRAGMA encoding to UTF-16. + nsPrintfCString pragmas( + // Use a smaller page size to improve perf/footprint; default is too large + "PRAGMA page_size = %u; " + // Enable auto_vacuum; this must happen after page_size and before WAL + "PRAGMA auto_vacuum = INCREMENTAL; " + "PRAGMA foreign_keys = ON; ", + kPageSize ); + // Note, the default encoding of UTF-8 is preferred. mozStorage does all + // the work necessary to convert UTF-16 nsString values for us. We don't + // need ordering and the binary equality operations are correct. So, do + // NOT set PRAGMA encoding to UTF-16. + nsresult rv = aConn->ExecuteSimpleSQL(pragmas); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + // Limit fragmentation by growing the database by many pages at once. + rv = aConn->SetGrowthIncrement(kGrowthSize, EmptyCString()); + if (rv == NS_ERROR_FILE_TOO_BIG) { + NS_WARNING("Not enough disk space to set sqlite growth increment."); + rv = NS_OK; + } + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Enable WAL journaling. This must be performed in a separate transaction + // after changing the page_size and enabling auto_vacuum. + nsPrintfCString wal( + // WAL journal can grow to given number of *pages* + "PRAGMA wal_autocheckpoint = %u; " + // Always truncate the journal back to given number of *bytes* + "PRAGMA journal_size_limit = %u; " + // WAL must be enabled at the end to allow page size to be changed, etc. + "PRAGMA journal_mode = WAL; ", + kWalAutoCheckpointPages, + kWalAutoCheckpointSize + ); + + rv = aConn->ExecuteSimpleSQL(wal); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Verify that we successfully set the vacuum mode to incremental. It + // is very easy to put the database in a state where the auto_vacuum + // pragma above fails silently. +#ifdef DEBUG + nsCOMPtr state; + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA auto_vacuum;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + int32_t mode; + rv = state->GetInt32(0, &mode); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // integer value 2 is incremental mode + if (NS_WARN_IF(mode != 2)) { return NS_ERROR_UNEXPECTED; } +#endif + return NS_OK; } @@ -401,17 +490,22 @@ DeleteCacheId(mozIStorageConnection* aConn, CacheId aCacheId, nsresult rv = QueryAll(aConn, aCacheId, matches); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = DeleteEntries(aConn, matches, aDeletedBodyIdListOut); + nsAutoTArray deletedSecurityIdList; + rv = DeleteEntries(aConn, matches, aDeletedBodyIdListOut, + deletedSecurityIdList); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = DeleteSecurityInfoList(aConn, deletedSecurityIdList); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } // Delete the remainder of the cache using cascade semantics. nsCOMPtr state; rv = aConn->CreateStatement(NS_LITERAL_CSTRING( - "DELETE FROM caches WHERE id=?1;" + "DELETE FROM caches WHERE id=:id;" ), getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt64Parameter(0, aCacheId); + rv = state->BindInt64ByName(NS_LITERAL_CSTRING("id"), aCacheId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = state->Execute(); @@ -433,11 +527,11 @@ IsCacheOrphaned(mozIStorageConnection* aConn, CacheId aCacheId, nsCOMPtr state; nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( - "SELECT COUNT(*) FROM storage WHERE cache_id=?1;" + "SELECT COUNT(*) FROM storage WHERE cache_id=:cache_id;" ), getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt64Parameter(0, aCacheId); + rv = state->BindInt64ByName(NS_LITERAL_CSTRING("cache_id"), aCacheId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasMoreData = false; @@ -527,19 +621,26 @@ CachePut(mozIStorageConnection* aConn, CacheId aCacheId, MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(aConn); - CacheQueryParams params(false, false, false, false, false, + CacheQueryParams params(false, false, false, false, NS_LITERAL_STRING("")); nsAutoTArray matches; nsresult rv = QueryCache(aConn, aCacheId, aRequest, params, matches); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = DeleteEntries(aConn, matches, aDeletedBodyIdListOut); + nsAutoTArray deletedSecurityIdList; + rv = DeleteEntries(aConn, matches, aDeletedBodyIdListOut, + deletedSecurityIdList); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = InsertEntry(aConn, aCacheId, aRequest, aRequestBodyId, aResponse, aResponseBodyId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + // Delete the security values after doing the insert to avoid churning + // the security table when its not necessary. + rv = DeleteSecurityInfoList(aConn, deletedSecurityIdList); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + return rv; } @@ -563,7 +664,12 @@ CacheDelete(mozIStorageConnection* aConn, CacheId aCacheId, return rv; } - rv = DeleteEntries(aConn, matches, aDeletedBodyIdListOut); + nsAutoTArray deletedSecurityIdList; + rv = DeleteEntries(aConn, matches, aDeletedBodyIdListOut, + deletedSecurityIdList); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = DeleteSecurityInfoList(aConn, deletedSecurityIdList); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } *aSuccessOut = true; @@ -641,11 +747,11 @@ StorageMatch(mozIStorageConnection* aConn, nsCOMPtr state; rv = aConn->CreateStatement(NS_LITERAL_CSTRING( - "SELECT cache_id FROM storage WHERE namespace=?1 ORDER BY rowid;" + "SELECT cache_id FROM storage WHERE namespace=:namespace ORDER BY rowid;" ), getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(0, aNamespace); + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("namespace"), aNamespace); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsAutoTArray cacheIdList; @@ -685,16 +791,19 @@ StorageGetCacheId(mozIStorageConnection* aConn, Namespace aNamespace, *aFoundCacheOut = false; + // How we constrain the key column depends on the value of our key. Use + // a format string for the query and let CreateAndBindKeyStatement() fill + // it in for us. + const char* query = "SELECT cache_id FROM storage " + "WHERE namespace=:namespace AND %s " + "ORDER BY rowid;"; + nsCOMPtr state; - nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( - "SELECT cache_id FROM storage WHERE namespace=?1 AND key=?2 ORDER BY rowid;" - ), getter_AddRefs(state)); + nsresult rv = CreateAndBindKeyStatement(aConn, query, aKey, + getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(0, aNamespace); - if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - - rv = state->BindStringParameter(1, aKey); + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("namespace"), aNamespace); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasMoreData = false; @@ -721,17 +830,18 @@ StoragePutCache(mozIStorageConnection* aConn, Namespace aNamespace, nsCOMPtr state; nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( - "INSERT INTO storage (namespace, key, cache_id) VALUES(?1, ?2, ?3);" + "INSERT INTO storage (namespace, key, cache_id) " + "VALUES (:namespace, :key, :cache_id);" ), getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(0, aNamespace); + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("namespace"), aNamespace); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindStringParameter(1, aKey); + rv = state->BindStringAsBlobByName(NS_LITERAL_CSTRING("key"), aKey); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt64Parameter(2, aCacheId); + rv = state->BindInt64ByName(NS_LITERAL_CSTRING("cache_id"), aCacheId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = state->Execute(); @@ -747,16 +857,17 @@ StorageForgetCache(mozIStorageConnection* aConn, Namespace aNamespace, MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(aConn); + // How we constrain the key column depends on the value of our key. Use + // a format string for the query and let CreateAndBindKeyStatement() fill + // it in for us. + const char *query = "DELETE FROM storage WHERE namespace=:namespace AND %s;"; + nsCOMPtr state; - nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( - "DELETE FROM storage WHERE namespace=?1 AND key=?2;" - ), getter_AddRefs(state)); + nsresult rv = CreateAndBindKeyStatement(aConn, query, aKey, + getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(0, aNamespace); - if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - - rv = state->BindStringParameter(1, aKey); + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("namespace"), aNamespace); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = state->Execute(); @@ -774,18 +885,19 @@ StorageGetKeys(mozIStorageConnection* aConn, Namespace aNamespace, nsCOMPtr state; nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( - "SELECT key FROM storage WHERE namespace=?1 ORDER BY rowid;" + "SELECT key FROM storage WHERE namespace=:namespace ORDER BY rowid;" ), getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(0, aNamespace); + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("namespace"), aNamespace); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasMoreData = false; while (NS_SUCCEEDED(state->ExecuteStep(&hasMoreData)) && hasMoreData) { nsAutoString key; - rv = state->GetString(0, key); + rv = state->GetBlobAsString(0, key); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + aKeysOut.AppendElement(key); } @@ -803,11 +915,11 @@ QueryAll(mozIStorageConnection* aConn, CacheId aCacheId, nsCOMPtr state; nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( - "SELECT id FROM entries WHERE cache_id=?1 ORDER BY id;" + "SELECT id FROM entries WHERE cache_id=:cache_id ORDER BY id;" ), getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt64Parameter(0, aCacheId); + rv = state->BindInt64ByName(NS_LITERAL_CSTRING("cache_id"), aCacheId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasMoreData = false; @@ -843,7 +955,7 @@ QueryCache(mozIStorageConnection* aConn, CacheId aCacheId, "FROM entries " "LEFT OUTER JOIN response_headers ON entries.id=response_headers.entry_id " "AND response_headers.name='vary' " - "WHERE entries.cache_id=?1 " + "WHERE entries.cache_id=:cache_id " "AND entries." ); @@ -856,30 +968,16 @@ QueryCache(mozIStorageConnection* aConn, CacheId aCacheId, query.AppendLiteral("request_url"); } - if (aParams.prefixMatch()) { - query.AppendLiteral(" LIKE ?2 ESCAPE '\\'"); - } else { - query.AppendLiteral("=?2"); - } - - query.AppendLiteral(" GROUP BY entries.id ORDER BY entries.id;"); + query.AppendLiteral("=:url GROUP BY entries.id ORDER BY entries.id;"); nsCOMPtr state; nsresult rv = aConn->CreateStatement(query, getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt64Parameter(0, aCacheId); + rv = state->BindInt64ByName(NS_LITERAL_CSTRING("cache_id"), aCacheId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - if (aParams.prefixMatch()) { - nsAutoString escapedUrlToMatch; - rv = state->EscapeStringForLIKE(urlToMatch, '\\', escapedUrlToMatch); - if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - urlToMatch = escapedUrlToMatch; - urlToMatch.AppendLiteral("%"); - } - - rv = state->BindStringParameter(1, urlToMatch); + rv = state->BindStringByName(NS_LITERAL_CSTRING("url"), urlToMatch); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasMoreData = false; @@ -925,11 +1023,11 @@ MatchByVaryHeader(mozIStorageConnection* aConn, nsCOMPtr state; nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT value FROM response_headers " - "WHERE name='vary' AND entry_id=?1;" + "WHERE name='vary' AND entry_id=:entry_id;" ), getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(0, entryId); + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("entry_id"), entryId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsAutoTArray varyValues; @@ -949,11 +1047,11 @@ MatchByVaryHeader(mozIStorageConnection* aConn, state->Reset(); rv = aConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT name, value FROM request_headers " - "WHERE entry_id=?1;" + "WHERE entry_id=:entry_id;" ), getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(0, entryId); + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("entry_id"), entryId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsRefPtr cachedHeaders = @@ -1028,6 +1126,7 @@ nsresult DeleteEntries(mozIStorageConnection* aConn, const nsTArray& aEntryIdList, nsTArray& aDeletedBodyIdListOut, + nsTArray& aDeletedSecurityIdListOut, uint32_t aPos, int32_t aLen) { MOZ_ASSERT(!NS_IsMainThread()); @@ -1052,7 +1151,7 @@ DeleteEntries(mozIStorageConnection* aConn, int32_t max = kMaxEntriesPerStatement; int32_t curLen = std::min(max, remaining); nsresult rv = DeleteEntries(aConn, aEntryIdList, aDeletedBodyIdListOut, - curPos, curLen); + aDeletedSecurityIdListOut, curPos, curLen); if (NS_FAILED(rv)) { return rv; } curPos += curLen; @@ -1063,7 +1162,8 @@ DeleteEntries(mozIStorageConnection* aConn, nsCOMPtr state; nsAutoCString query( - "SELECT request_body_id, response_body_id FROM entries WHERE id IN (" + "SELECT request_body_id, response_body_id, response_security_info_id " + "FROM entries WHERE id IN (" ); AppendListParamsToQuery(query, aEntryIdList, aPos, aLen); query.AppendLiteral(")"); @@ -1090,6 +1190,33 @@ DeleteEntries(mozIStorageConnection* aConn, aDeletedBodyIdListOut.AppendElement(id); } } + + // and then a possible third entry for the security id + bool isNull = false; + rv = state->GetIsNull(2, &isNull); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (!isNull) { + int32_t securityId = -1; + rv = state->GetInt32(2, &securityId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // First try to increment the count for this ID if we're already + // seen it + bool found = false; + for (uint32_t i = 0; i < aDeletedSecurityIdListOut.Length(); ++i) { + if (aDeletedSecurityIdListOut[i].mId == securityId) { + found = true; + aDeletedSecurityIdListOut[i].mCount += 1; + break; + } + } + + // Otherwise add a new entry for this ID with a count of 1 + if (!found) { + aDeletedSecurityIdListOut.AppendElement(IdCount(securityId)); + } + } } // Dependent records removed via ON DELETE CASCADE @@ -1112,6 +1239,194 @@ DeleteEntries(mozIStorageConnection* aConn, return rv; } +nsresult +InsertSecurityInfo(mozIStorageConnection* aConn, const nsACString& aData, + int32_t *aIdOut) +{ + MOZ_ASSERT(aConn); + MOZ_ASSERT(aIdOut); + MOZ_ASSERT(!aData.IsEmpty()); + + // We want to use an index to find existing security blobs, but indexing + // the full blob would be quite expensive. Instead, we index a small + // hash value. Calculate this hash as the first 8 bytes of the SHA1 of + // the full data. + nsresult rv; + nsCOMPtr crypto = + do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = crypto->Init(nsICryptoHash::SHA1); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = crypto->Update(reinterpret_cast(aData.BeginReading()), + aData.Length()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + nsAutoCString fullHash; + rv = crypto->Finish(false /* based64 result */, fullHash); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + nsDependentCSubstring hash(fullHash, 0, 8); + + // Next, search for an existing entry for this blob by comparing the hash + // value first and then the full data. SQLite is smart enough to use + // the index on the hash to search the table before doing the expensive + // comparison of the large data column. (This was verified with EXPLAIN.) + nsCOMPtr state; + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + // Note that hash and data are blobs, but we can use = here since the + // columns are NOT NULL. + "SELECT id, refcount FROM security_info WHERE hash=:hash AND data=:data;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringAsBlobByName(NS_LITERAL_CSTRING("hash"), hash); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringAsBlobByName(NS_LITERAL_CSTRING("data"), aData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // This security info blob is already in the database + if (hasMoreData) { + // get the existing security blob id to return + rv = state->GetInt32(0, aIdOut); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + int32_t refcount = -1; + rv = state->GetInt32(1, &refcount); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // But first, update the refcount in the database. + refcount += 1; + + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE security_info SET refcount=:refcount WHERE id=:id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("refcount"), refcount); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("id"), *aIdOut); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return NS_OK; + } + + // This is a new security info blob. Create a new row in the security table + // with an initial refcount of 1. + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO security_info (hash, data, refcount) VALUES (:hash, :data, 1);" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringAsBlobByName(NS_LITERAL_CSTRING("hash"), hash); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringAsBlobByName(NS_LITERAL_CSTRING("data"), aData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT last_insert_rowid()" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->GetInt32(0, aIdOut); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return NS_OK; +} + +nsresult +DeleteSecurityInfo(mozIStorageConnection* aConn, int32_t aId, int32_t aCount) +{ + // First, we need to determine the current refcount for this security blob. + nsCOMPtr state; + nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT refcount FROM security_info WHERE id=:id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("id"), aId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + int32_t refcount = -1; + rv = state->GetInt32(0, &refcount); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + MOZ_ASSERT(refcount >= aCount); + + // Next, calculate the new refcount + int32_t newCount = refcount - aCount; + + // If the last reference to this security blob was removed we can + // just remove the entire row. + if (newCount == 0) { + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "DELETE FROM security_info WHERE id=:id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("id"), aId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return NS_OK; + } + + // Otherwise update the refcount in the table to reflect the reduced + // number of references to the security blob. + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE security_info SET refcount=:refcount WHERE id=:id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("refcount"), newCount); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("id"), aId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return NS_OK; +} + +nsresult +DeleteSecurityInfoList(mozIStorageConnection* aConn, + const nsTArray& aDeletedStorageIdList) +{ + for (uint32_t i = 0; i < aDeletedStorageIdList.Length(); ++i) { + nsresult rv = DeleteSecurityInfo(aConn, aDeletedStorageIdList[i].mId, + aDeletedStorageIdList[i].mCount); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + + return NS_OK; +} + nsresult InsertEntry(mozIStorageConnection* aConn, CacheId aCacheId, const CacheRequest& aRequest, @@ -1122,8 +1437,18 @@ InsertEntry(mozIStorageConnection* aConn, CacheId aCacheId, MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(aConn); + nsresult rv = NS_OK; + int32_t securityId = -1; + + if (!aResponse.channelInfo().securityInfo().IsEmpty()) { + rv = InsertSecurityInfo(aConn, + aResponse.channelInfo().securityInfo(), + &securityId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + nsCOMPtr state; - nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( "INSERT INTO entries (" "request_method, " "request_url, " @@ -1142,75 +1467,107 @@ InsertEntry(mozIStorageConnection* aConn, CacheId aCacheId, "response_status_text, " "response_headers_guard, " "response_body_id, " - "response_security_info, " + "response_security_info_id, " "cache_id " - ") VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19)" + ") VALUES (" + ":request_method, " + ":request_url, " + ":request_url_no_query, " + ":request_referrer, " + ":request_headers_guard, " + ":request_mode, " + ":request_credentials, " + ":request_contentpolicytype, " + ":request_context, " + ":request_cache, " + ":request_body_id, " + ":response_type, " + ":response_url, " + ":response_status, " + ":response_status_text, " + ":response_headers_guard, " + ":response_body_id, " + ":response_security_info_id, " + ":cache_id " + ");" ), getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindUTF8StringParameter(0, aRequest.method()); + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("request_method"), + aRequest.method()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindStringParameter(1, aRequest.url()); + rv = state->BindStringByName(NS_LITERAL_CSTRING("request_url"), + aRequest.url()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindStringParameter(2, aRequest.urlWithoutQuery()); + rv = state->BindStringByName(NS_LITERAL_CSTRING("request_url_no_query"), + aRequest.urlWithoutQuery()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindStringParameter(3, aRequest.referrer()); + rv = state->BindStringByName(NS_LITERAL_CSTRING("request_referrer"), + aRequest.referrer()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(4, + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("request_headers_guard"), static_cast(aRequest.headersGuard())); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(5, static_cast(aRequest.mode())); + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("request_mode"), + static_cast(aRequest.mode())); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(6, + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("request_credentials"), static_cast(aRequest.credentials())); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(7, + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("request_contentpolicytype"), static_cast(aRequest.contentPolicyType())); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(8, + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("request_context"), static_cast(aRequest.context())); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(9, + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("request_cache"), static_cast(aRequest.requestCache())); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = BindId(state, 10, aRequestBodyId); + rv = BindId(state, NS_LITERAL_CSTRING("request_body_id"), aRequestBodyId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(11, static_cast(aResponse.type())); + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("response_type"), + static_cast(aResponse.type())); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindStringParameter(12, aResponse.url()); + rv = state->BindStringByName(NS_LITERAL_CSTRING("response_url"), + aResponse.url()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(13, aResponse.status()); + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("response_status"), + aResponse.status()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindUTF8StringParameter(14, aResponse.statusText()); + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("response_status_text"), + aResponse.statusText()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(15, + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("response_headers_guard"), static_cast(aResponse.headersGuard())); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = BindId(state, 16, aResponseBodyId); + rv = BindId(state, NS_LITERAL_CSTRING("response_body_id"), aResponseBodyId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindBlobParameter(17, reinterpret_cast - (aResponse.securityInfo().get()), - aResponse.securityInfo().Length()); + if (aResponse.channelInfo().securityInfo().IsEmpty()) { + rv = state->BindNullByName(NS_LITERAL_CSTRING("response_security_info_id")); + } else { + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("response_security_info_id"), + securityId); + } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt64Parameter(18, aCacheId); + rv = state->BindInt64ByName(NS_LITERAL_CSTRING("cache_id"), aCacheId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = state->Execute(); @@ -1234,19 +1591,21 @@ InsertEntry(mozIStorageConnection* aConn, CacheId aCacheId, "name, " "value, " "entry_id " - ") VALUES (?1, ?2, ?3)" + ") VALUES (:name, :value, :entry_id)" ), getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } const nsTArray& requestHeaders = aRequest.headers(); for (uint32_t i = 0; i < requestHeaders.Length(); ++i) { - rv = state->BindUTF8StringParameter(0, requestHeaders[i].name()); + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("name"), + requestHeaders[i].name()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindUTF8StringParameter(1, requestHeaders[i].value()); + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("value"), + requestHeaders[i].value()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(2, entryId); + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("entry_id"), entryId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = state->Execute(); @@ -1258,19 +1617,21 @@ InsertEntry(mozIStorageConnection* aConn, CacheId aCacheId, "name, " "value, " "entry_id " - ") VALUES (?1, ?2, ?3)" + ") VALUES (:name, :value, :entry_id)" ), getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } const nsTArray& responseHeaders = aResponse.headers(); for (uint32_t i = 0; i < responseHeaders.Length(); ++i) { - rv = state->BindUTF8StringParameter(0, responseHeaders[i].name()); + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("name"), + responseHeaders[i].name()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindUTF8StringParameter(1, responseHeaders[i].value()); + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("value"), + responseHeaders[i].value()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(2, entryId); + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("entry_id"), entryId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = state->Execute(); @@ -1291,19 +1652,21 @@ ReadResponse(mozIStorageConnection* aConn, EntryId aEntryId, nsCOMPtr state; nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT " - "response_type, " - "response_url, " - "response_status, " - "response_status_text, " - "response_headers_guard, " - "response_body_id, " - "response_security_info " + "entries.response_type, " + "entries.response_url, " + "entries.response_status, " + "entries.response_status_text, " + "entries.response_headers_guard, " + "entries.response_body_id, " + "security_info.data " "FROM entries " - "WHERE id=?1;" + "LEFT OUTER JOIN security_info " + "ON entries.response_security_info_id=security_info.id " + "WHERE entries.id=:id;" ), getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(0, aEntryId); + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("id"), aEntryId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasMoreData = false; @@ -1342,23 +1705,19 @@ ReadResponse(mozIStorageConnection* aConn, EntryId aEntryId, if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } - uint8_t* data = nullptr; - uint32_t dataLength = 0; - rv = state->GetBlob(6, &dataLength, &data); + rv = state->GetBlobAsUTF8String(6, aSavedResponseOut->mValue.channelInfo().securityInfo()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - aSavedResponseOut->mValue.securityInfo().Adopt( - reinterpret_cast(data), dataLength); rv = aConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT " "name, " "value " "FROM response_headers " - "WHERE entry_id=?1;" + "WHERE entry_id=:entry_id;" ), getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(0, aEntryId); + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("entry_id"), aEntryId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } while (NS_SUCCEEDED(state->ExecuteStep(&hasMoreData)) && hasMoreData) { @@ -1399,11 +1758,11 @@ ReadRequest(mozIStorageConnection* aConn, EntryId aEntryId, "request_cache, " "request_body_id " "FROM entries " - "WHERE id=?1;" + "WHERE id=:id;" ), getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(0, aEntryId); + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("id"), aEntryId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } bool hasMoreData = false; @@ -1472,11 +1831,11 @@ ReadRequest(mozIStorageConnection* aConn, EntryId aEntryId, "name, " "value " "FROM request_headers " - "WHERE entry_id=?1;" + "WHERE entry_id=:entry_id;" ), getter_AddRefs(state)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - rv = state->BindInt32Parameter(0, aEntryId); + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("entry_id"), aEntryId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } while (NS_SUCCEEDED(state->ExecuteStep(&hasMoreData)) && hasMoreData) { @@ -1518,28 +1877,28 @@ BindListParamsToQuery(mozIStorageStatement* aState, MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT((aPos + aLen) <= aEntryIdList.Length()); for (int32_t i = aPos; i < aLen; ++i) { - nsresult rv = aState->BindInt32Parameter(i, aEntryIdList[i]); + nsresult rv = aState->BindInt32ByIndex(i, aEntryIdList[i]); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } nsresult -BindId(mozIStorageStatement* aState, uint32_t aPos, const nsID* aId) +BindId(mozIStorageStatement* aState, const nsACString& aName, const nsID* aId) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(aState); nsresult rv; if (!aId) { - rv = aState->BindNullParameter(aPos); + rv = aState->BindNullByName(aName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return rv; } char idBuf[NSID_LENGTH]; aId->ToProvidedString(idBuf); - rv = aState->BindUTF8StringParameter(aPos, nsAutoCString(idBuf)); + rv = aState->BindUTF8StringByName(aName, nsAutoCString(idBuf)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return rv; @@ -1562,8 +1921,111 @@ ExtractId(mozIStorageStatement* aState, uint32_t aPos, nsID* aIdOut) return rv; } +nsresult +CreateAndBindKeyStatement(mozIStorageConnection* aConn, + const char* aQueryFormat, + const nsAString& aKey, + mozIStorageStatement** aStateOut) +{ + MOZ_ASSERT(aConn); + MOZ_ASSERT(aQueryFormat); + MOZ_ASSERT(aStateOut); + + // The key is stored as a blob to avoid encoding issues. An empty string + // is mapped to NULL for blobs. Normally we would just write the query + // as "key IS :key" to do the proper NULL checking, but that prevents + // sqlite from using the key index. Therefore use "IS NULL" explicitly + // if the key is empty, otherwise use "=:key" so that sqlite uses the + // index. + const char* constraint = nullptr; + if (aKey.IsEmpty()) { + constraint = "key IS NULL"; + } else { + constraint = "key=:key"; + } + + nsPrintfCString query(aQueryFormat, constraint); + + nsCOMPtr state; + nsresult rv = aConn->CreateStatement(query, getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (!aKey.IsEmpty()) { + rv = state->BindStringAsBlobByName(NS_LITERAL_CSTRING("key"), aKey); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + + state.forget(aStateOut); + + return rv; +} + } // namespace +nsresult +IncrementalVacuum(mozIStorageConnection* aConn) +{ + // Determine how much free space is in the database. + nsCOMPtr state; + nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA freelist_count;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + int32_t freePages = 0; + rv = state->GetInt32(0, &freePages); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // We have a relatively small page size, so we want to be careful to avoid + // fragmentation. We already use a growth incremental which will cause + // sqlite to allocate and release multiple pages at the same time. We can + // further reduce fragmentation by making our allocated chunks a bit + // "sticky". This is done by creating some hysteresis where we allocate + // pages/chunks as soon as we need them, but we only release pages/chunks + // when we have a large amount of free space. This helps with the case + // where a page is adding and remove resources causing it to dip back and + // forth across a chunk boundary. + // + // So only proceed with releasing pages if we have more than our constant + // threshold. + if (freePages <= kMaxFreePages) { + return NS_OK; + } + + // Release the excess pages back to the sqlite VFS. This may also release + // chunks of multiple pages back to the OS. + int32_t pagesToRelease = freePages - kMaxFreePages; + + rv = aConn->ExecuteSimpleSQL(nsPrintfCString( + "PRAGMA incremental_vacuum(%d);", pagesToRelease + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Verify that our incremental vacuum actually did something +#ifdef DEBUG + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA freelist_count;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + freePages = 0; + rv = state->GetInt32(0, &freePages); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + MOZ_ASSERT(freePages <= kMaxFreePages); +#endif + + return NS_OK; +} + } // namespace db } // namespace cache } // namespace dom diff --git a/dom/cache/DBSchema.h b/dom/cache/DBSchema.h index fdd2d85d39..6f94deb362 100644 --- a/dom/cache/DBSchema.h +++ b/dom/cache/DBSchema.h @@ -32,6 +32,7 @@ namespace db { nsresult CreateSchema(mozIStorageConnection* aConn); +// Note, this cannot be executed within a transaction. nsresult InitializeConnection(mozIStorageConnection* aConn); @@ -104,6 +105,10 @@ nsresult StorageGetKeys(mozIStorageConnection* aConn, Namespace aNamespace, nsTArray& aKeysOut); +// Note, this works best when its NOT executed within a transaction. +nsresult +IncrementalVacuum(mozIStorageConnection* aConn); + // We will wipe out databases with a schema versions less than this. extern const int32_t kMaxWipeSchemaVersion; diff --git a/dom/cache/FetchPut.cpp b/dom/cache/FetchPut.cpp deleted file mode 100644 index 0e03589e9f..0000000000 --- a/dom/cache/FetchPut.cpp +++ /dev/null @@ -1,483 +0,0 @@ -/* -*- 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/cache/FetchPut.h" - -#include "mozilla/dom/Fetch.h" -#include "mozilla/dom/FetchDriver.h" -#include "mozilla/dom/Headers.h" -#include "mozilla/dom/Promise.h" -#include "mozilla/dom/PromiseNativeHandler.h" -#include "mozilla/dom/Request.h" -#include "mozilla/dom/Response.h" -#include "mozilla/dom/ResponseBinding.h" -#include "mozilla/dom/UnionTypes.h" -#include "mozilla/dom/cache/ManagerId.h" -#include "nsContentUtils.h" -#include "nsNetUtil.h" -#include "nsThreadUtils.h" -#include "nsCRT.h" -#include "nsHttp.h" - -namespace mozilla { -namespace dom { -namespace cache { - -class FetchPut::Runnable final : public nsRunnable -{ -public: - explicit Runnable(FetchPut* aFetchPut) - : mFetchPut(aFetchPut) - { - MOZ_ASSERT(mFetchPut); - } - - NS_IMETHOD Run() override - { - if (NS_IsMainThread()) - { - mFetchPut->DoFetchOnMainThread(); - return NS_OK; - } - - MOZ_ASSERT(mFetchPut->mInitiatingThread == NS_GetCurrentThread()); - - mFetchPut->DoPutOnWorkerThread(); - - // The FetchPut object must ultimately be freed on the worker thread, - // so make sure we release our reference here. The runnable may end - // up getting deleted on the main thread. - mFetchPut = nullptr; - - return NS_OK; - } - -private: - nsRefPtr mFetchPut; -}; - -class FetchPut::FetchObserver final : public FetchDriverObserver -{ -public: - explicit FetchObserver(FetchPut* aFetchPut) - : mFetchPut(aFetchPut) - { - } - - virtual void OnResponseAvailable(InternalResponse* aResponse) override - { - MOZ_ASSERT(!mInternalResponse); - mInternalResponse = aResponse; - } - - virtual void OnResponseEnd() override - { - mFetchPut->FetchComplete(this, mInternalResponse); - if (mFetchPut->mInitiatingThread == NS_GetCurrentThread()) { - mFetchPut = nullptr; - } else { - nsCOMPtr initiatingThread(mFetchPut->mInitiatingThread); - nsCOMPtr runnable = - NS_NewNonOwningRunnableMethod(mFetchPut.forget().take(), &FetchPut::Release); - MOZ_ALWAYS_TRUE(NS_SUCCEEDED( - initiatingThread->Dispatch(runnable, nsIThread::DISPATCH_NORMAL))); - } - } - -protected: - virtual ~FetchObserver() { } - -private: - nsRefPtr mFetchPut; - nsRefPtr mInternalResponse; -}; - -// static -nsresult -FetchPut::Create(Listener* aListener, Manager* aManager, CacheId aCacheId, - const nsTArray& aRequests, - const nsTArray>& aRequestStreams, - FetchPut** aFetchPutOut) -{ - MOZ_ASSERT(aRequests.Length() == aRequestStreams.Length()); - - // The FetchDriver requires that all requests have a referrer already set. -#ifdef DEBUG - for (uint32_t i = 0; i < aRequests.Length(); ++i) { - if (aRequests[i].referrer() == EmptyString()) { - return NS_ERROR_UNEXPECTED; - } - } -#endif - - nsRefPtr ref = new FetchPut(aListener, aManager, aCacheId, - aRequests, aRequestStreams); - - nsresult rv = ref->DispatchToMainThread(); - if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - - ref.forget(aFetchPutOut); - - return NS_OK; -} - -void -FetchPut::ClearListener() -{ - MOZ_ASSERT(mListener); - mListener = nullptr; -} - -FetchPut::FetchPut(Listener* aListener, Manager* aManager, CacheId aCacheId, - const nsTArray& aRequests, - const nsTArray>& aRequestStreams) - : mListener(aListener) - , mManager(aManager) - , mCacheId(aCacheId) - , mInitiatingThread(NS_GetCurrentThread()) - , mStateList(aRequests.Length()) - , mPendingCount(0) -{ - MOZ_ASSERT(mListener); - MOZ_ASSERT(mManager); - MOZ_ASSERT(aRequests.Length() == aRequestStreams.Length()); - - for (uint32_t i = 0; i < aRequests.Length(); ++i) { - State* s = mStateList.AppendElement(); - s->mCacheRequest = aRequests[i]; - s->mRequestStream = aRequestStreams[i]; - } - - mManager->AddRefCacheId(mCacheId); -} - -FetchPut::~FetchPut() -{ - MOZ_ASSERT(mInitiatingThread == NS_GetCurrentThread()); - MOZ_ASSERT(!mListener); - mManager->RemoveListener(this); - mManager->ReleaseCacheId(mCacheId); - mResult.SuppressException(); // XXXbz should we really be ending up here with - // a failed mResult we never reported to anyone? -} - -nsresult -FetchPut::DispatchToMainThread() -{ - MOZ_ASSERT(!mRunnable); - - nsCOMPtr runnable = new Runnable(this); - - nsresult rv = NS_DispatchToMainThread(runnable, nsIThread::DISPATCH_NORMAL); - if (NS_WARN_IF(NS_FAILED(rv))) { - return rv; - } - - MOZ_ASSERT(!mRunnable); - mRunnable = runnable.forget(); - - return NS_OK; -} - -void -FetchPut::DispatchToInitiatingThread() -{ - MOZ_ASSERT(mRunnable); - - nsresult rv = mInitiatingThread->Dispatch(mRunnable, - nsIThread::DISPATCH_NORMAL); - if (NS_FAILED(rv)) { - MOZ_CRASH("Failed to dispatch to worker thread after fetch completion."); - } - - mRunnable = nullptr; -} - -void -FetchPut::DoFetchOnMainThread() -{ - MOZ_ASSERT(NS_IsMainThread()); - - nsRefPtr managerId = mManager->GetManagerId(); - nsCOMPtr principal = managerId->Principal(); - mPendingCount = mStateList.Length(); - - nsCOMPtr loadGroup; - nsresult rv = NS_NewLoadGroup(getter_AddRefs(loadGroup), principal); - if (NS_WARN_IF(NS_FAILED(rv))) { - MaybeSetError(ErrorResult(rv)); - MaybeCompleteOnMainThread(); - return; - } - - for (uint32_t i = 0; i < mStateList.Length(); ++i) { - nsRefPtr internalRequest = - ToInternalRequest(mStateList[i].mCacheRequest); - - // If there is a stream we must clone it so that its still available - // to store in the cache later; - if (mStateList[i].mRequestStream) { - internalRequest->SetBody(mStateList[i].mRequestStream); - nsRefPtr clone = internalRequest->Clone(); - - // The copy construction clone above can change the source stream, - // so get it back out to use when we put this in the cache. - internalRequest->GetBody(getter_AddRefs(mStateList[i].mRequestStream)); - - internalRequest = clone; - } - - nsRefPtr fetchDriver = new FetchDriver(internalRequest, - principal, - loadGroup); - - mStateList[i].mFetchObserver = new FetchObserver(this); - rv = fetchDriver->Fetch(mStateList[i].mFetchObserver); - if (NS_WARN_IF(NS_FAILED(rv))) { - MaybeSetError(ErrorResult(rv)); - mStateList[i].mFetchObserver = nullptr; - mPendingCount -= 1; - continue; - } - } - - // If they all failed, then we might need to complete main thread immediately - MaybeCompleteOnMainThread(); -} - -void -FetchPut::FetchComplete(FetchObserver* aObserver, - InternalResponse* aInternalResponse) -{ - MOZ_ASSERT(NS_IsMainThread()); - - if (aInternalResponse->IsError() && !mResult.Failed()) { - MaybeSetError(ErrorResult(NS_ERROR_FAILURE)); - } - - for (uint32_t i = 0; i < mStateList.Length(); ++i) { - if (mStateList[i].mFetchObserver == aObserver) { - ErrorResult rv; - ToCacheResponseWithoutBody(mStateList[i].mCacheResponse, - *aInternalResponse, rv); - if (rv.Failed()) { - MaybeSetError(Move(rv)); - } else { - aInternalResponse->GetBody(getter_AddRefs(mStateList[i].mResponseStream)); - } - mStateList[i].mFetchObserver = nullptr; - MOZ_ASSERT(mPendingCount > 0); - mPendingCount -= 1; - MaybeCompleteOnMainThread(); - return; - } - } - - MOZ_ASSERT_UNREACHABLE("Should never get called by unknown fetch observer."); -} - -void -FetchPut::MaybeCompleteOnMainThread() -{ - MOZ_ASSERT(NS_IsMainThread()); - - if (mPendingCount > 0) { - return; - } - - DispatchToInitiatingThread(); -} - -void -FetchPut::DoPutOnWorkerThread() -{ - MOZ_ASSERT(mInitiatingThread == NS_GetCurrentThread()); - - if (mResult.Failed()) { - MaybeNotifyListener(); - return; - } - - // These allocate ~4.5k combined on the stack - nsAutoTArray putList; - nsAutoTArray, 16> requestStreamList; - nsAutoTArray, 16> responseStreamList; - - putList.SetCapacity(mStateList.Length()); - requestStreamList.SetCapacity(mStateList.Length()); - responseStreamList.SetCapacity(mStateList.Length()); - - for (uint32_t i = 0; i < mStateList.Length(); ++i) { - // The spec requires us to catch if content tries to insert a set of - // requests that would overwrite each other. - if (MatchInPutList(mStateList[i].mCacheRequest, putList)) { - MaybeSetError(ErrorResult(NS_ERROR_DOM_INVALID_STATE_ERR)); - MaybeNotifyListener(); - return; - } - - CacheRequestResponse* entry = putList.AppendElement(); - entry->request() = mStateList[i].mCacheRequest; - entry->response() = mStateList[i].mCacheResponse; - requestStreamList.AppendElement(mStateList[i].mRequestStream.forget()); - responseStreamList.AppendElement(mStateList[i].mResponseStream.forget()); - } - mStateList.Clear(); - - mManager->ExecutePutAll(this, mCacheId, putList, requestStreamList, - responseStreamList); -} - -// static -bool -FetchPut::MatchInPutList(const CacheRequest& aRequest, - const nsTArray& aPutList) -{ - // This method implements the SW spec QueryCache algorithm against an - // in memory array of Request/Response objects. This essentially the - // same algorithm that is implemented in DBSchema.cpp. Unfortunately - // we cannot unify them because when operating against the real database - // we don't want to load all request/response objects into memory. - - if (!aRequest.method().LowerCaseEqualsLiteral("get") && - !aRequest.method().LowerCaseEqualsLiteral("head")) { - return false; - } - - nsRefPtr requestHeaders = - ToInternalHeaders(aRequest.headers()); - - for (uint32_t i = 0; i < aPutList.Length(); ++i) { - const CacheRequest& cachedRequest = aPutList[i].request(); - const CacheResponse& cachedResponse = aPutList[i].response(); - - // If the URLs don't match, then just skip to the next entry. - if (aRequest.url() != cachedRequest.url()) { - continue; - } - - nsRefPtr cachedRequestHeaders = - ToInternalHeaders(cachedRequest.headers()); - - nsRefPtr cachedResponseHeaders = - ToInternalHeaders(cachedResponse.headers()); - - nsAutoTArray varyHeaders; - ErrorResult rv; - cachedResponseHeaders->GetAll(NS_LITERAL_CSTRING("vary"), varyHeaders, rv); - MOZ_ALWAYS_TRUE(!rv.Failed()); - - // Assume the vary headers match until we find a conflict - bool varyHeadersMatch = true; - - for (uint32_t j = 0; j < varyHeaders.Length(); ++j) { - // Extract the header names inside the Vary header value. - nsAutoCString varyValue(varyHeaders[j]); - char* rawBuffer = varyValue.BeginWriting(); - char* token = nsCRT::strtok(rawBuffer, NS_HTTP_HEADER_SEPS, &rawBuffer); - bool bailOut = false; - for (; token; - token = nsCRT::strtok(rawBuffer, NS_HTTP_HEADER_SEPS, &rawBuffer)) { - nsDependentCString header(token); - MOZ_ASSERT(!header.EqualsLiteral("*"), - "We should have already caught this in " - "TypeUtils::ToPCacheResponseWithoutBody()"); - - ErrorResult headerRv; - nsAutoCString value; - requestHeaders->Get(header, value, headerRv); - if (NS_WARN_IF(headerRv.Failed())) { - headerRv.SuppressException(); - MOZ_ASSERT(value.IsEmpty()); - } - - nsAutoCString cachedValue; - cachedRequestHeaders->Get(header, value, headerRv); - if (NS_WARN_IF(headerRv.Failed())) { - headerRv.SuppressException(); - MOZ_ASSERT(cachedValue.IsEmpty()); - } - - if (value != cachedValue) { - varyHeadersMatch = false; - bailOut = true; - break; - } - } - - if (bailOut) { - break; - } - } - - // URL was equal and all vary headers match! - if (varyHeadersMatch) { - return true; - } - } - - return false; -} - -void -FetchPut::OnOpComplete(ErrorResult&& aRv, const CacheOpResult& aResult, - CacheId aOpenedCacheId, - const nsTArray& aSavedResponseList, - const nsTArray& aSavedRequestList, - StreamList* aStreamList) -{ - MOZ_ASSERT(mInitiatingThread == NS_GetCurrentThread()); - MOZ_ASSERT(aResult.type() == CacheOpResult::TCachePutAllResult); - MaybeSetError(Move(aRv)); - MaybeNotifyListener(); -} - -void -FetchPut::MaybeSetError(ErrorResult&& aRv) -{ - if (mResult.Failed() || !aRv.Failed()) { - return; - } - mResult = Move(aRv); -} - -void -FetchPut::MaybeNotifyListener() -{ - MOZ_ASSERT(mInitiatingThread == NS_GetCurrentThread()); - if (!mListener) { - return; - } - // CacheParent::OnFetchPut can lead to the destruction of |this| when the - // object is removed from CacheParent::mFetchPutList, so make sure that - // doesn't happen until this method returns. - nsRefPtr kungFuDeathGrip(this); - mListener->OnFetchPut(this, Move(mResult)); -} - -nsIGlobalObject* -FetchPut::GetGlobalObject() const -{ - MOZ_CRASH("No global object in parent-size FetchPut operation!"); -} - -#ifdef DEBUG -void -FetchPut::AssertOwningThread() const -{ - MOZ_ASSERT(mInitiatingThread == NS_GetCurrentThread()); -} -#endif - -CachePushStreamChild* -FetchPut::CreatePushStream(nsIAsyncInputStream* aStream) -{ - MOZ_CRASH("FetchPut should never create a push stream!"); -} - -} // namespace cache -} // namespace dom -} // namespace mozilla diff --git a/dom/cache/FetchPut.h b/dom/cache/FetchPut.h deleted file mode 100644 index a1867482af..0000000000 --- a/dom/cache/FetchPut.h +++ /dev/null @@ -1,123 +0,0 @@ -/* -*- 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_cache_FetchPut_h -#define mozilla_dom_cache_FetchPut_h - -#include "mozilla/AlreadyAddRefed.h" -#include "mozilla/Attributes.h" -#include "mozilla/ErrorResult.h" -#include "mozilla/dom/cache/Manager.h" -#include "mozilla/dom/cache/CacheTypes.h" -#include "mozilla/dom/cache/Types.h" -#include "mozilla/dom/cache/TypeUtils.h" -#include "mozilla/nsRefPtr.h" -#include "nsTArray.h" -#include - -class nsIInputStream; -class nsIRunnable; -class nsIThread; - -namespace mozilla { -namespace dom { - -class Request; -class Response; - -namespace cache { - -class FetchPut final : public Manager::Listener - , public TypeUtils -{ -public: - typedef std::pair, nsRefPtr> PutPair; - - class Listener - { - public: - virtual void - OnFetchPut(FetchPut* aFetchPut, ErrorResult&& aRv) = 0; - }; - - static nsresult - Create(Listener* aListener, Manager* aManager, CacheId aCacheId, - const nsTArray& aRequests, - const nsTArray>& aRequestStreams, - FetchPut** aFetchPutOut); - - void ClearListener(); - -private: - class Runnable; - class FetchObserver; - friend class FetchObserver; - struct State - { - CacheRequest mCacheRequest; - nsCOMPtr mRequestStream; - nsRefPtr mFetchObserver; - CacheResponse mCacheResponse; - nsCOMPtr mResponseStream; - - nsRefPtr mRequest; - nsRefPtr mResponse; - }; - - FetchPut(Listener* aListener, Manager* aManager, CacheId aCacheId, - const nsTArray& aRequests, - const nsTArray>& aRequestStreams); - ~FetchPut(); - - nsresult DispatchToMainThread(); - void DispatchToInitiatingThread(); - - void DoFetchOnMainThread(); - void FetchComplete(FetchObserver* aObserver, - InternalResponse* aInternalResponse); - void MaybeCompleteOnMainThread(); - - void DoPutOnWorkerThread(); - static bool MatchInPutList(const CacheRequest& aRequest, - const nsTArray& aPutList); - - virtual void - OnOpComplete(ErrorResult&& aRv, const CacheOpResult& aResult, - CacheId aOpenedCacheId, - const nsTArray& aSavedResponseList, - const nsTArray& aSavedRequestList, - StreamList* aStreamList) override; - - void MaybeSetError(ErrorResult&& aRv); - void MaybeNotifyListener(); - - // TypeUtils methods - virtual nsIGlobalObject* GetGlobalObject() const override; -#ifdef DEBUG - virtual void AssertOwningThread() const override; -#endif - - virtual CachePushStreamChild* - CreatePushStream(nsIAsyncInputStream* aStream) override; - - Listener* mListener; - nsRefPtr mManager; - const CacheId mCacheId; - nsCOMPtr mInitiatingThread; - nsTArray mStateList; - uint32_t mPendingCount; - ErrorResult mResult; - nsCOMPtr mRunnable; - -public: - NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::cache::FetchPut) -}; - -} // namespace cache -} // namespace dom -} // namespace mozilla - -#endif // mozilla_dom_cache_FetchPut_h diff --git a/dom/cache/Manager.cpp b/dom/cache/Manager.cpp index cf2548a31b..468d069b9a 100644 --- a/dom/cache/Manager.cpp +++ b/dom/cache/Manager.cpp @@ -133,6 +133,24 @@ namespace mozilla { namespace dom { namespace cache { +namespace { + +bool IsHeadRequest(CacheRequest aRequest, CacheQueryParams aParams) +{ + return !aParams.ignoreMethod() && aRequest.method().LowerCaseEqualsLiteral("head"); +} + +bool IsHeadRequest(CacheRequestOrVoid aRequest, CacheQueryParams aParams) +{ + if (aRequest.type() == CacheRequestOrVoid::TCacheRequest) { + return !aParams.ignoreMethod() && + aRequest.get_CacheRequest().method().LowerCaseEqualsLiteral("head"); + } + return false; +} + +} // namespace + // ---------------------------------------------------------------------------- // Singleton class to track Manager instances and ensure there is only @@ -513,7 +531,9 @@ public: mArgs.params(), &mFoundResponse, &mResponse); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - if (!mFoundResponse || !mResponse.mHasBodyId) { + if (!mFoundResponse || !mResponse.mHasBodyId + || IsHeadRequest(mArgs.request(), mArgs.params())) { + mResponse.mHasBodyId = false; return rv; } @@ -576,7 +596,9 @@ public: if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } for (uint32_t i = 0; i < mSavedResponses.Length(); ++i) { - if (!mSavedResponses[i].mHasBodyId) { + if (!mSavedResponses[i].mHasBodyId + || IsHeadRequest(mArgs.requestOrVoid(), mArgs.params())) { + mSavedResponses[i].mHasBodyId = false; continue; } @@ -1065,7 +1087,9 @@ public: if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } for (uint32_t i = 0; i < mSavedRequests.Length(); ++i) { - if (!mSavedRequests[i].mHasBodyId) { + if (!mSavedRequests[i].mHasBodyId + || IsHeadRequest(mArgs.requestOrVoid(), mArgs.params())) { + mSavedRequests[i].mHasBodyId = false; continue; } @@ -1127,7 +1151,9 @@ public: &mSavedResponse); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } - if (!mFoundResponse || !mSavedResponse.mHasBodyId) { + if (!mFoundResponse || !mSavedResponse.mHasBodyId + || IsHeadRequest(mArgs.request(), mArgs.params())) { + mSavedResponse.mHasBodyId = false; return rv; } @@ -1626,7 +1652,6 @@ Manager::ExecuteCacheOp(Listener* aListener, CacheId aCacheId, { NS_ASSERT_OWNINGTHREAD(Manager); MOZ_ASSERT(aListener); - MOZ_ASSERT(aOpArgs.type() != CacheOpArgs::TCacheAddAllArgs); MOZ_ASSERT(aOpArgs.type() != CacheOpArgs::TCachePutAllArgs); if (mState == Closing) { diff --git a/dom/cache/Manager.h b/dom/cache/Manager.h index 50a8a7c1fd..51396f430a 100644 --- a/dom/cache/Manager.h +++ b/dom/cache/Manager.h @@ -18,6 +18,9 @@ class nsIInputStream; class nsIThread; namespace mozilla { + +class ErrorResult; + namespace dom { namespace cache { diff --git a/dom/cache/QuotaClient.cpp b/dom/cache/QuotaClient.cpp index fcc6d0a740..3428f91aee 100644 --- a/dom/cache/QuotaClient.cpp +++ b/dom/cache/QuotaClient.cpp @@ -110,6 +110,12 @@ public: InitOrigin(PersistenceType aPersistenceType, const nsACString& aGroup, const nsACString& aOrigin, UsageInfo* aUsageInfo) override { + // The QuotaManager passes a nullptr UsageInfo if there is no quota being + // enforced against the origin. + if (!aUsageInfo) { + return NS_OK; + } + return GetUsageForOrigin(aPersistenceType, aGroup, aOrigin, aUsageInfo); } @@ -118,6 +124,8 @@ public: const nsACString& aOrigin, UsageInfo* aUsageInfo) override { + MOZ_ASSERT(aUsageInfo); + QuotaManager* qm = QuotaManager::Get(); MOZ_ASSERT(qm); diff --git a/dom/cache/TypeUtils.cpp b/dom/cache/TypeUtils.cpp index e79ec4cc0c..4af9c25ed8 100644 --- a/dom/cache/TypeUtils.cpp +++ b/dom/cache/TypeUtils.cpp @@ -15,7 +15,6 @@ #include "mozilla/dom/cache/CacheTypes.h" #include "mozilla/dom/cache/ReadStream.h" #include "mozilla/ipc/BackgroundChild.h" -#include "mozilla/ipc/FileDescriptorSetChild.h" #include "mozilla/ipc/PBackgroundChild.h" #include "mozilla/ipc/PFileDescriptorSetChild.h" #include "mozilla/ipc/InputStreamUtils.h" @@ -30,85 +29,16 @@ #include "nsCRT.h" #include "nsHttp.h" -namespace { +namespace mozilla { +namespace dom { +namespace cache { -using mozilla::ErrorResult; -using mozilla::unused; -using mozilla::void_t; -using mozilla::dom::InternalHeaders; -using mozilla::dom::cache::CacheReadStream; -using mozilla::dom::cache::HeadersEntry; using mozilla::ipc::BackgroundChild; using mozilla::ipc::FileDescriptor; using mozilla::ipc::PBackgroundChild; using mozilla::ipc::PFileDescriptorSetChild; -// Utility function to remove the fragment from a URL, check its scheme, and optionally -// provide a URL without the query. We're not using nsIURL or URL to do this because -// they require going to the main thread. -static void -ProcessURL(nsAString& aUrl, bool* aSchemeValidOut, - nsAString* aUrlWithoutQueryOut, ErrorResult& aRv) -{ - NS_ConvertUTF16toUTF8 flatURL(aUrl); - const char* url = flatURL.get(); - - // off the main thread URL parsing using nsStdURLParser. - nsCOMPtr urlParser = new nsStdURLParser(); - - uint32_t pathPos; - int32_t pathLen; - uint32_t schemePos; - int32_t schemeLen; - aRv = urlParser->ParseURL(url, flatURL.Length(), &schemePos, &schemeLen, - nullptr, nullptr, // ignore authority - &pathPos, &pathLen); - if (NS_WARN_IF(aRv.Failed())) { return; } - - if (aSchemeValidOut) { - nsAutoCString scheme(Substring(flatURL, schemePos, schemeLen)); - *aSchemeValidOut = scheme.LowerCaseEqualsLiteral("http") || - scheme.LowerCaseEqualsLiteral("https") || - scheme.LowerCaseEqualsLiteral("app"); - } - - uint32_t queryPos; - int32_t queryLen; - uint32_t refPos; - int32_t refLen; - - aRv = urlParser->ParsePath(url + pathPos, flatURL.Length() - pathPos, - nullptr, nullptr, // ignore filepath - &queryPos, &queryLen, - &refPos, &refLen); - if (NS_WARN_IF(aRv.Failed())) { - return; - } - - // TODO: Remove this once Request/Response properly strip the fragment (bug 1110476) - if (refLen >= 0) { - // ParsePath gives us ref position relative to the start of the path - refPos += pathPos; - - aUrl = Substring(aUrl, 0, refPos - 1); - } - - if (!aUrlWithoutQueryOut) { - return; - } - - if (queryLen < 0) { - *aUrlWithoutQueryOut = aUrl; - return; - } - - // ParsePath gives us query position relative to the start of the path - queryPos += pathPos; - - // We want everything before the query sine we already removed the trailing - // fragment - *aUrlWithoutQueryOut = Substring(aUrl, 0, queryPos - 1); -} +namespace { static bool HasVaryStar(mozilla::dom::InternalHeaders* aHeaders) @@ -174,18 +104,6 @@ ToHeadersEntryList(nsTArray& aOut, InternalHeaders* aHeaders) } // namespace -namespace mozilla { -namespace dom { -namespace cache { - -using mozilla::ipc::BackgroundChild; -using mozilla::ipc::FileDescriptor; -using mozilla::ipc::FileDescriptorSetChild; -using mozilla::ipc::PFileDescriptorSetChild; -using mozilla::ipc::PBackgroundChild; -using mozilla::ipc::OptionalFileDescriptorSet; - - already_AddRefed TypeUtils::ToInternalRequest(const RequestOrUSVString& aIn, BodyAction aBodyAction, ErrorResult& aRv) @@ -225,9 +143,8 @@ TypeUtils::ToInternalRequest(const OwningRequestOrUSVString& aIn, void TypeUtils::ToCacheRequest(CacheRequest& aOut, InternalRequest* aIn, - BodyAction aBodyAction, - ReferrerAction aReferrerAction, - SchemeAction aSchemeAction, ErrorResult& aRv) + BodyAction aBodyAction, SchemeAction aSchemeAction, + ErrorResult& aRv) { MOZ_ASSERT(aIn); @@ -249,16 +166,8 @@ TypeUtils::ToCacheRequest(CacheRequest& aOut, InternalRequest* aIn, aRv.ThrowTypeError(MSG_INVALID_URL_SCHEME, &label, &aOut.url()); return; } - - if (aSchemeAction == NetworkErrorOnInvalidScheme) { - aRv.Throw(NS_ERROR_DOM_NETWORK_ERR); - return; - } } - if (aReferrerAction == ExpandReferrer) { - UpdateRequestReferrer(GetGlobalObject(), aIn); - } aIn->GetReferrer(aOut.referrer()); nsRefPtr headers = aIn->Headers(); @@ -315,7 +224,7 @@ TypeUtils::ToCacheResponseWithoutBody(CacheResponse& aOut, } ToHeadersEntryList(aOut.headers(), headers); aOut.headersGuard() = headers->Guard(); - aOut.securityInfo() = aIn.GetSecurityInfo(); + aOut.channelInfo() = aIn.GetChannelInfo().AsIPCChannelInfo(); } void @@ -352,7 +261,6 @@ TypeUtils::ToCacheQueryParams(CacheQueryParams& aOut, aOut.ignoreSearch() = aIn.mIgnoreSearch; aOut.ignoreMethod() = aIn.mIgnoreMethod; aOut.ignoreVary() = aIn.mIgnoreVary; - aOut.prefixMatch() = aIn.mPrefixMatch; aOut.cacheNameSet() = aIn.mCacheName.WasPassed(); if (aOut.cacheNameSet()) { aOut.cacheName() = aIn.mCacheName.Value(); @@ -382,7 +290,7 @@ TypeUtils::ToResponse(const CacheResponse& aIn) ir->Headers()->Fill(*internalHeaders, result); MOZ_ASSERT(!result.Failed()); - ir->SetSecurityInfo(aIn.securityInfo()); + ir->InitChannelInfo(aIn.channelInfo()); nsCOMPtr stream = ReadStream::Create(aIn.body()); ir->SetBody(stream); @@ -466,6 +374,73 @@ TypeUtils::ToInternalHeaders(const nsTArray& aHeadersEntryList, return ref.forget(); } +// Utility function to remove the fragment from a URL, check its scheme, and optionally +// provide a URL without the query. We're not using nsIURL or URL to do this because +// they require going to the main thread. +// static +void +TypeUtils::ProcessURL(nsAString& aUrl, bool* aSchemeValidOut, + nsAString* aUrlWithoutQueryOut, ErrorResult& aRv) +{ + NS_ConvertUTF16toUTF8 flatURL(aUrl); + const char* url = flatURL.get(); + + // off the main thread URL parsing using nsStdURLParser. + nsCOMPtr urlParser = new nsStdURLParser(); + + uint32_t pathPos; + int32_t pathLen; + uint32_t schemePos; + int32_t schemeLen; + aRv = urlParser->ParseURL(url, flatURL.Length(), &schemePos, &schemeLen, + nullptr, nullptr, // ignore authority + &pathPos, &pathLen); + if (NS_WARN_IF(aRv.Failed())) { return; } + + if (aSchemeValidOut) { + nsAutoCString scheme(Substring(flatURL, schemePos, schemeLen)); + *aSchemeValidOut = scheme.LowerCaseEqualsLiteral("http") || + scheme.LowerCaseEqualsLiteral("https"); + } + + uint32_t queryPos; + int32_t queryLen; + uint32_t refPos; + int32_t refLen; + + aRv = urlParser->ParsePath(url + pathPos, flatURL.Length() - pathPos, + nullptr, nullptr, // ignore filepath + &queryPos, &queryLen, + &refPos, &refLen); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + // TODO: Remove this once Request/Response properly strip the fragment (bug 1110476) + if (refLen >= 0) { + // ParsePath gives us ref position relative to the start of the path + refPos += pathPos; + + aUrl = Substring(aUrl, 0, refPos - 1); + } + + if (!aUrlWithoutQueryOut) { + return; + } + + if (queryLen < 0) { + *aUrlWithoutQueryOut = aUrl; + return; + } + + // ParsePath gives us query position relative to the start of the path + queryPos += pathPos; + + // We want everything before the query sine we already removed the trailing + // fragment + *aUrlWithoutQueryOut = Substring(aUrl, 0, queryPos - 1); +} + void TypeUtils::CheckAndSetBodyUsed(Request* aRequest, BodyAction aBodyAction, ErrorResult& aRv) diff --git a/dom/cache/TypeUtils.h b/dom/cache/TypeUtils.h index ea2f971394..ca675e262c 100644 --- a/dom/cache/TypeUtils.h +++ b/dom/cache/TypeUtils.h @@ -46,17 +46,10 @@ public: ReadBody }; - enum ReferrerAction - { - PassThroughReferrer, - ExpandReferrer - }; - enum SchemeAction { IgnoreInvalidScheme, - TypeErrorOnInvalidScheme, - NetworkErrorOnInvalidScheme + TypeErrorOnInvalidScheme }; ~TypeUtils() { } @@ -80,8 +73,8 @@ public: void ToCacheRequest(CacheRequest& aOut, InternalRequest* aIn, - BodyAction aBodyAction, ReferrerAction aReferrerAction, - SchemeAction aSchemeAction, ErrorResult& aRv); + BodyAction aBodyAction, SchemeAction aSchemeAction, + ErrorResult& aRv); void ToCacheResponseWithoutBody(CacheResponse& aOut, InternalResponse& aIn, @@ -107,6 +100,10 @@ public: ToInternalHeaders(const nsTArray& aHeadersEntryList, HeadersGuardEnum aGuard = HeadersGuardEnum::None); + static void + ProcessURL(nsAString& aUrl, bool* aSchemeValidOut, + nsAString* aUrlWithoutQueryOut, ErrorResult& aRv); + private: void CheckAndSetBodyUsed(Request* aRequest, BodyAction aBodyAction, diff --git a/dom/cache/moz.build b/dom/cache/moz.build index cd7514fb64..ee6ed50f6d 100644 --- a/dom/cache/moz.build +++ b/dom/cache/moz.build @@ -21,11 +21,11 @@ EXPORTS.mozilla.dom.cache += [ 'CacheStorageParent.h', 'CacheStreamControlChild.h', 'CacheStreamControlParent.h', + 'Connection.h', 'Context.h', 'DBAction.h', 'DBSchema.h', 'Feature.h', - 'FetchPut.h', 'FileUtils.h', 'IPCUtils.h', 'Manager.h', @@ -57,11 +57,11 @@ UNIFIED_SOURCES += [ 'CacheStorageParent.cpp', 'CacheStreamControlChild.cpp', 'CacheStreamControlParent.cpp', + 'Connection.cpp', 'Context.cpp', 'DBAction.cpp', 'DBSchema.cpp', 'Feature.cpp', - 'FetchPut.cpp', 'FileUtils.cpp', 'Manager.cpp', 'ManagerId.cpp', diff --git a/dom/cache/test/mochitest/large_url_list.js b/dom/cache/test/mochitest/large_url_list.js new file mode 100644 index 0000000000..40fb7acfb2 --- /dev/null +++ b/dom/cache/test/mochitest/large_url_list.js @@ -0,0 +1,1002 @@ +var largeUrlList = [ + 'http://example.com/cia5rherm0000hkjx7r8of8b1/cia5rhern0001hkjxes0mptxq/cia5rhern0002hkjx0ze9t1oy/cia5rhern0003hkjxa07rei71', + 'http://example.com/cia5rhern0004hkjx15dh2a23/cia5rhern0005hkjxukeg547n/cia5rhern0006hkjxn78qlfk0/cia5rhern0007hkjxx7pkqem5', + 'http://example.com/cia5rhern0008hkjx6xh95dcp/cia5rhern0009hkjxu3212vkx/cia5rhern000ahkjxlhu69sv7/cia5rhern000bhkjxwffaxb8c', + 'http://example.com/cia5rhern000chkjxylxzrc0l/cia5rhern000dhkjxoe8p7jap/cia5rhern000ehkjx49ssgz0i/cia5rhern000fhkjxlqcm6jq9', + 'http://example.com/cia5rhern000ghkjxjfag4uqk/cia5rhern000hhkjxxjmvakfj/cia5rhern000ihkjxp5j7d9ic/cia5rhern000jhkjxdog5rxfb', + 'http://example.com/cia5rhern000khkjxrdrwsflr/cia5rhern000lhkjxjqj2ydrd/cia5rhern000mhkjxujiqkc1c/cia5rhern000nhkjxw0nk2nvg', + 'http://example.com/cia5rhern000ohkjxpglj3lmb/cia5rhern000phkjxjekhrrqd/cia5rhern000qhkjxalsyb73l/cia5rhern000rhkjxyjbnn6er', + 'http://example.com/cia5rhero000shkjxmhknjjrv/cia5rhero000thkjx8xtfdrgh/cia5rhero000uhkjxpe3dwxk6/cia5rhero000vhkjxint5ohhn', + 'http://example.com/cia5rhero000whkjxvaiypkdj/cia5rhero000xhkjxaqm599g5/cia5rhero000yhkjxivrhnj8f/cia5rhero000zhkjxvmd4t8h6', + 'http://example.com/cia5rhero0010hkjxsx3ke38t/cia5rhero0011hkjx8xnw9tqf/cia5rhero0012hkjxl0lf3iup/cia5rhero0013hkjxbvhj1jj9', + 'http://example.com/cia5rhero0014hkjx4fmpoeu0/cia5rhero0015hkjxwpptyghv/cia5rhero0016hkjxva5k7lbw/cia5rhero0017hkjx138e6bmj', + 'http://example.com/cia5rhero0018hkjxx7j78vei/cia5rhero0019hkjx6xrih33b/cia5rhero001ahkjxd4zgngrg/cia5rhero001bhkjx1tloxb1q', + 'http://example.com/cia5rhero001chkjxrt8zdtde/cia5rhero001dhkjxm9mbqci7/cia5rhero001ehkjxowk5e4q6/cia5rhero001fhkjxh6qmsl1v', + 'http://example.com/cia5rhero001ghkjxfadnveq2/cia5rhero001hhkjx4edkaq54/cia5rhero001ihkjx9fqoqxga/cia5rhero001jhkjx58vqa1zr', + 'http://example.com/cia5rhero001khkjxdrmypltg/cia5rhero001lhkjxhqdzis67/cia5rhero001mhkjxcggzuqdx/cia5rhero001nhkjxctfvi81n', + 'http://example.com/cia5rhero001ohkjxe8pk4cgt/cia5rhero001phkjx6sm4qi4x/cia5rhero001qhkjxm9lqhc6e/cia5rhero001rhkjxy9k6yacj', + 'http://example.com/cia5rhero001shkjx0zfh0f80/cia5rhero001thkjx06fdx9h9/cia5rherp001uhkjxqdmh6jtq/cia5rherp001vhkjxywng3c85', + 'http://example.com/cia5rherp001whkjxm2qihavv/cia5rherp001xhkjxw9v508hp/cia5rherp001yhkjx3jtn1gc8/cia5rherp001zhkjxsmvesh8h', + 'http://example.com/cia5rherp0020hkjxz8yaovlg/cia5rherp0021hkjx66mvfalu/cia5rherp0022hkjxqpilypyo/cia5rherp0023hkjxsp9txbui', + 'http://example.com/cia5rherp0024hkjxcc9ni9ai/cia5rherp0025hkjxslerqaui/cia5rherp0026hkjxe0jl3k05/cia5rherp0027hkjxr0hf5tjn', + 'http://example.com/cia5rherp0028hkjxsg9l2j7w/cia5rherp0029hkjx8zs585su/cia5rherp002ahkjxuwrjr76m/cia5rherp002bhkjxh5nsz7nf', + 'http://example.com/cia5rherp002chkjxzph0v3u0/cia5rherp002dhkjxdayhh6lg/cia5rherp002ehkjxrkvcxz41/cia5rherp002fhkjxlqa9ul9r', + 'http://example.com/cia5rherp002ghkjxf6egbyrh/cia5rherp002hhkjxaw3isyif/cia5rherp002ihkjxboruj7fc/cia5rherp002jhkjx6fgq322u', + 'http://example.com/cia5rherp002khkjx5adgzcsd/cia5rherp002lhkjxo84dwluf/cia5rherp002mhkjxijwq9esb/cia5rherp002nhkjx59ky0n1a', + 'http://example.com/cia5rherp002ohkjx8r9ldy53/cia5rherp002phkjxn5vwv7yi/cia5rherp002qhkjxdsg26179/cia5rherp002rhkjxxvufen34', + 'http://example.com/cia5rherp002shkjxj17ade31/cia5rherp002thkjx37xtqdld/cia5rherp002uhkjxbl9a3b96/cia5rherp002vhkjxrq82quxi', + 'http://example.com/cia5rherp002whkjxj0kiekos/cia5rherp002xhkjxltksjia2/cia5rherp002yhkjx9f6j16y4/cia5rherp002zhkjxub535mh5', + 'http://example.com/cia5rherp0030hkjxsd01uqhn/cia5rherp0031hkjxxy7v0enj/cia5rherp0032hkjxm7afimyn/cia5rherp0033hkjxu9rm6wnw', + 'http://example.com/cia5rherp0034hkjxoe7lub90/cia5rherp0035hkjxdqfkfwe5/cia5rherp0036hkjx7bydg1o0/cia5rherp0037hkjxwiayjj4h', + 'http://example.com/cia5rherp0038hkjxrxyltlgt/cia5rherp0039hkjxq8m8gzd9/cia5rherp003ahkjxfzlwk84z/cia5rherp003bhkjxhzsd9psq', + 'http://example.com/cia5rherp003chkjx8k4d0ev6/cia5rherp003dhkjxe5rhd9bk/cia5rherp003ehkjxhdb9pe1z/cia5rherp003fhkjxnwfpch8g', + 'http://example.com/cia5rherp003ghkjxv07j52h2/cia5rherp003hhkjx1i0x37cm/cia5rherp003ihkjxey8otr6v/cia5rherp003jhkjxk1np7zo5', + 'http://example.com/cia5rherp003khkjxscsb30qa/cia5rherp003lhkjxcuap7ls3/cia5rherq003mhkjxsdoqgpxu/cia5rherq003nhkjxz21dqdpc', + 'http://example.com/cia5rherq003ohkjx66ms0nn0/cia5rherq003phkjxb8hblxdd/cia5rherq003qhkjxmr2bkt0b/cia5rherq003rhkjxkla58f3d', + 'http://example.com/cia5rherr003shkjxutmljflc/cia5rherr003thkjx9sglm092/cia5rherr003uhkjx6ttae5q1/cia5rherr003vhkjx2i22wbci', + 'http://example.com/cia5rherr003whkjx0gmqk0qu/cia5rherr003xhkjxnnopsaqa/cia5rherr003yhkjxhicji4pa/cia5rherr003zhkjxg7dm7usj', + 'http://example.com/cia5rherr0040hkjxqymjv2aj/cia5rherr0041hkjxgjublrsc/cia5rherr0042hkjxu34zz1y2/cia5rherr0043hkjx0v1t6s9s', + 'http://example.com/cia5rherr0044hkjxourvl7pc/cia5rherr0045hkjxem1bv16o/cia5rherr0046hkjxlza5reyz/cia5rherr0047hkjxc0hqmpn8', + 'http://example.com/cia5rherr0048hkjxab7n8nos/cia5rherr0049hkjxjwltgxvq/cia5rherr004ahkjxpskjnyvt/cia5rherr004bhkjx9t1zafr8', + 'http://example.com/cia5rherr004chkjxos007jlr/cia5rherr004dhkjxside72xk/cia5rherr004ehkjxmsb8r01v/cia5rherr004fhkjx1c1v6ypg', + 'http://example.com/cia5rherr004ghkjxl9hin1sd/cia5rherr004hhkjx1ktxr1zz/cia5rherr004ihkjxjnq6wq6b/cia5rherr004jhkjx6z9ffji0', + 'http://example.com/cia5rherr004khkjxxu7r4mzw/cia5rherr004lhkjxj9vru5i2/cia5rherr004mhkjxowwrqrzg/cia5rherr004nhkjxejliyhz4', + 'http://example.com/cia5rherr004ohkjx23jh1sos/cia5rherr004phkjxrq8hqfpy/cia5rherr004qhkjx4232owb6/cia5rherr004rhkjx6dbg83qw', + 'http://example.com/cia5rherr004shkjx6t43ggcp/cia5rherr004thkjxmf2k2w6n/cia5rherr004uhkjxwhhmhvx5/cia5rherr004vhkjxt4qahjbz', + 'http://example.com/cia5rherr004whkjx0bkl7a31/cia5rherr004xhkjxk34mm030/cia5rherr004yhkjxoundtp7u/cia5rherr004zhkjxr1htazrx', + 'http://example.com/cia5rherr0050hkjxfm9e7rcy/cia5rherr0051hkjxvp7y4api/cia5rherr0052hkjxdairex18/cia5rherr0053hkjxoqt8of5n', + 'http://example.com/cia5rherr0054hkjxbg8to9br/cia5rherr0055hkjxymwr54ie/cia5rherr0056hkjxez64qj8r/cia5rherr0057hkjxann7kwhj', + 'http://example.com/cia5rherr0058hkjxov69tz76/cia5rherr0059hkjxxh6rrvs8/cia5rherr005ahkjxwsmsljoc/cia5rherr005bhkjxi5o005me', + 'http://example.com/cia5rherr005chkjxu6w2wby8/cia5rherr005dhkjxvcmga3pp/cia5rherr005ehkjxcr90eaeq/cia5rherr005fhkjxon6fcg5h', + 'http://example.com/cia5rherr005ghkjxqo4vucke/cia5rherr005hhkjxxvdv1j8f/cia5rherr005ihkjxo2vl23qb/cia5rherr005jhkjx3tbu637k', + 'http://example.com/cia5rherr005khkjx8fnhdy4n/cia5rherr005lhkjxvgvc27dd/cia5rherr005mhkjxydbxzvrw/cia5rherr005nhkjx0ev5rnx0', + 'http://example.com/cia5rherr005ohkjxymk6ffpy/cia5rherr005phkjx79eg202o/cia5rherr005qhkjxloql9sm9/cia5rherr005rhkjxf0fcwa88', + 'http://example.com/cia5rherr005shkjxkclvai2n/cia5rherr005thkjx6lhii1h5/cia5rherr005uhkjx21imwf9f/cia5rherr005vhkjxct3akix6', + 'http://example.com/cia5rherr005whkjxam3tkrf9/cia5rherr005xhkjx3ooh8d77/cia5rherr005yhkjx3ih1052q/cia5rherr005zhkjxjn08ve87', + 'http://example.com/cia5rherr0060hkjxbovht1zk/cia5rherr0061hkjx3mq7yzke/cia5rherr0062hkjxc3jyhls3/cia5rherr0063hkjxw5y2kgtl', + 'http://example.com/cia5rherr0064hkjxlzchg2lf/cia5rherr0065hkjxjf4qz7h7/cia5rherr0066hkjxhzxfzly2/cia5rherr0067hkjxxpsm77n2', + 'http://example.com/cia5rherr0068hkjxqakagjms/cia5rherr0069hkjxahy5cahd/cia5rherr006ahkjxlx3eo0d8/cia5rherr006bhkjxqhufagp0', + 'http://example.com/cia5rherr006chkjxs6l4reuh/cia5rherr006dhkjx22dhomat/cia5rherr006ehkjxv8bhjw6s/cia5rherr006fhkjxiembar5h', + 'http://example.com/cia5rherr006ghkjx7uk2ahcv/cia5rherr006hhkjxrptkdzpc/cia5rherr006ihkjxoelimxey/cia5rherr006jhkjxh00sc9q0', + 'http://example.com/cia5rherr006khkjx7bjaq4p8/cia5rherr006lhkjx0t17bwqu/cia5rherr006mhkjx63m6nx0x/cia5rherr006nhkjx4aylco20', + 'http://example.com/cia5rherr006ohkjxqncx2bfn/cia5rherr006phkjxcs9lhj4k/cia5rherr006qhkjxltiaiox0/cia5rherr006rhkjxpxhk6ceh', + 'http://example.com/cia5rherr006shkjxkjpkluh5/cia5rherr006thkjxksjhi0rp/cia5rherr006uhkjxuluxeq56/cia5rherr006vhkjx2i5mkp5o', + 'http://example.com/cia5rherr006whkjx94ph66s7/cia5rherr006xhkjx963ey6z0/cia5rherr006yhkjxklr3zey7/cia5rherr006zhkjxhi9ckzcj', + 'http://example.com/cia5rherr0070hkjx8qu91dki/cia5rherr0071hkjxnsj72h7a/cia5rherr0072hkjxpcch22pw/cia5rherr0073hkjxr88cl6td', + 'http://example.com/cia5rherr0074hkjx33v1pc9p/cia5rherr0075hkjxbijzax59/cia5rherr0076hkjxdatwas40/cia5rherr0077hkjxp92fa2bh', + 'http://example.com/cia5rherr0078hkjxids7sgwp/cia5rherr0079hkjxptr6nbvk/cia5rherr007ahkjx51bynaee/cia5rherr007bhkjx2f0oimkg', + 'http://example.com/cia5rherr007chkjxj243t6fq/cia5rherr007dhkjxx0z54s2z/cia5rherr007ehkjxvtk1saqr/cia5rherr007fhkjxc99ot3di', + 'http://example.com/cia5rherr007ghkjxcdyrgbe5/cia5rherr007hhkjx4g2zda93/cia5rherr007ihkjx3dhwfh5w/cia5rherr007jhkjxnucjju1k', + 'http://example.com/cia5rherr007khkjxtucv4xam/cia5rherr007lhkjxzb52dzam/cia5rherr007mhkjxe5ytefsg/cia5rherr007nhkjxhn1htqim', + 'http://example.com/cia5rherr007ohkjxyhvqunje/cia5rherr007phkjxg1rmr8ia/cia5rherr007qhkjxvne6tsg2/cia5rherr007rhkjx92plnv33', + 'http://example.com/cia5rherr007shkjxedxbud9y/cia5rherr007thkjxsfcv846z/cia5rherr007uhkjxrtoyvp1k/cia5rherr007vhkjxwkkvb63p', + 'http://example.com/cia5rherr007whkjxhom33u72/cia5rherr007xhkjx00i52itn/cia5rherr007yhkjxlbwyrfbh/cia5rherr007zhkjx7hqxgotj', + 'http://example.com/cia5rherr0080hkjxm6fr03t9/cia5rherr0081hkjxsskkldqg/cia5rherr0082hkjx6agt52v9/cia5rherr0083hkjxmw1qpljq', + 'http://example.com/cia5rherr0084hkjx66lbczid/cia5rherr0085hkjx1yhf4qqz/cia5rherr0086hkjx156ah7x1/cia5rhers0087hkjxf7zmqrz3', + 'http://example.com/cia5rhers0088hkjxlvoy1vtq/cia5rhers0089hkjxwopzmrl8/cia5rhers008ahkjxa76skipt/cia5rhers008bhkjxchpeggii', + 'http://example.com/cia5rhers008chkjx77jxzrj5/cia5rhers008dhkjxwdtj4fxt/cia5rhers008ehkjxyk1rbqs4/cia5rhers008fhkjxi6c9h7on', + 'http://example.com/cia5rhers008ghkjxcjbq4qio/cia5rhers008hhkjx46uf4z78/cia5rhers008ihkjxiwjs7rs8/cia5rhers008jhkjx6gow2oc8', + 'http://example.com/cia5rhers008khkjxam5doybe/cia5rhers008lhkjx33h8n79o/cia5rhers008mhkjxch0uhhqa/cia5rhers008nhkjxvkh7mio2', + 'http://example.com/cia5rhers008ohkjxfixhe4vq/cia5rhers008phkjxrw2g2zi3/cia5rhers008qhkjxoynia1m2/cia5rhers008rhkjxoh1yvlac', + 'http://example.com/cia5rhers008shkjxewzwesdt/cia5rhers008thkjx2yx5o3rs/cia5rhers008uhkjxm75q9exl/cia5rhers008vhkjx4jogh9q0', + 'http://example.com/cia5rhers008whkjxdo6vglei/cia5rhers008xhkjx0bx8rrph/cia5rhers008yhkjx7sotv4z4/cia5rhers008zhkjxid6dq8hl', + 'http://example.com/cia5rhers0090hkjxpmid4zyj/cia5rhers0091hkjx7swur7ci/cia5rhers0092hkjxd4mc5j7l/cia5rhers0093hkjxri89p7jy', + 'http://example.com/cia5rhers0094hkjxirrewas9/cia5rhers0095hkjx1wkqq1g1/cia5rhers0096hkjxvpc3hp0t/cia5rhers0097hkjx1ugphwhs', + 'http://example.com/cia5rhers0098hkjxxptvggpn/cia5rhers0099hkjx0hgy6tpl/cia5rhers009ahkjxrgk4cwx1/cia5rhers009bhkjxhff7ocag', + 'http://example.com/cia5rhers009chkjxt2zanh56/cia5rhers009dhkjxeh17c4wg/cia5rhers009ehkjxkhdljhm4/cia5rhers009fhkjxa78k166d', + 'http://example.com/cia5rhers009ghkjxblshruiu/cia5rhers009hhkjx6bpkys70/cia5rhers009ihkjx097xi5jr/cia5rhers009jhkjxl1v2ym6h', + 'http://example.com/cia5rhers009khkjxvciqe48d/cia5rhers009lhkjxxgfzrzqn/cia5rhers009mhkjx99k29erp/cia5rhers009nhkjxnmdineg9', + 'http://example.com/cia5rhers009ohkjx0rr58cbk/cia5rhers009phkjxo0wwxbtm/cia5rhers009qhkjx8j0n0ta4/cia5rhers009rhkjx8jok0mo9', + 'http://example.com/cia5rhers009shkjxo1trhdsu/cia5rhers009thkjxtukv1mwy/cia5rhers009uhkjx0e82f0we/cia5rhers009vhkjx8j0ysbxu', + 'http://example.com/cia5rhers009whkjxdl5recmi/cia5rhers009xhkjx801pcdzp/cia5rhers009yhkjx2wnyv52l/cia5rhers009zhkjxdmjnipn0', + 'http://example.com/cia5rhers00a0hkjxj46kgklk/cia5rhers00a1hkjxmaxskno1/cia5rhers00a2hkjxrv6jtia4/cia5rhers00a3hkjx524js0bd', + 'http://example.com/cia5rhers00a4hkjx6y55uml1/cia5rhers00a5hkjxahhtmal1/cia5rhers00a6hkjx0pt7rbot/cia5rhers00a7hkjxtboirxmu', + 'http://example.com/cia5rhers00a8hkjxqmrjjens/cia5rhers00a9hkjxxbmuxaqn/cia5rhers00aahkjx2fdz4j2q/cia5rhers00abhkjxflwb8nml', + 'http://example.com/cia5rhers00achkjxc7n5xjqb/cia5rhers00adhkjx98mur9hy/cia5rhers00aehkjxrmv4miio/cia5rhers00afhkjxm1wmuk24', + 'http://example.com/cia5rhers00aghkjxhh9nyajy/cia5rhers00ahhkjxypuj93ni/cia5rhers00aihkjxqhvpkhvu/cia5rhers00ajhkjxcebhpi2g', + 'http://example.com/cia5rhers00akhkjxpq2z92bg/cia5rhers00alhkjx6ztkugxa/cia5rhers00amhkjxys8qr2ae/cia5rhers00anhkjxugpiwy4o', + 'http://example.com/cia5rhers00aohkjx5lptk7ll/cia5rhert00aphkjx3m0tdlo6/cia5rhert00aqhkjxtun34qda/cia5rhert00arhkjxgn2cukgo', + 'http://example.com/cia5rhert00ashkjxgndi0t1w/cia5rhert00athkjxngdtw2ac/cia5rhert00auhkjxtlqdm9tk/cia5rhert00avhkjxpmv39lb6', + 'http://example.com/cia5rhert00awhkjxsm1i5606/cia5rhert00axhkjxiini4u7n/cia5rhert00ayhkjxawcdih8y/cia5rhert00azhkjx2uwcskyo', + 'http://example.com/cia5rhert00b0hkjxjosiu610/cia5rhert00b1hkjx0inj6sis/cia5rhert00b2hkjx687j5ca5/cia5rhert00b3hkjxaaepyb37', + 'http://example.com/cia5rhert00b4hkjxccpt8awt/cia5rhert00b5hkjxb4kuj419/cia5rhert00b6hkjxq8bi4zga/cia5rhert00b7hkjxrgomql9g', + 'http://example.com/cia5rhert00b8hkjxjxy3fhb0/cia5rhert00b9hkjxftkoktwh/cia5rhert00bahkjxjx5o3cnn/cia5rhert00bbhkjxa6frtobg', + 'http://example.com/cia5rhert00bchkjxe5tkaifo/cia5rhert00bdhkjx5jppppvo/cia5rhert00behkjxdmsqdq9p/cia5rhert00bfhkjxnw4tk86n', + 'http://example.com/cia5rhert00bghkjx49wz4l5r/cia5rhert00bhhkjx2mfza2xe/cia5rhert00bihkjxbsd9ovfr/cia5rhert00bjhkjx3vye5ep3', + 'http://example.com/cia5rhert00bkhkjxwtdsj589/cia5rhert00blhkjxmjchgsmf/cia5rhert00bmhkjxm5fxzqpq/cia5rhert00bnhkjxyf65cmel', + 'http://example.com/cia5rhert00bohkjx7c22ja5m/cia5rhert00bphkjxhe6e895b/cia5rhert00bqhkjxj6kge909/cia5rhert00brhkjx65rkq4ma', + 'http://example.com/cia5rhert00bshkjx1cn2s8w0/cia5rhert00bthkjxlbbqak7z/cia5rhert00buhkjxfy9yreib/cia5rhert00bvhkjxaivuz0fw', + 'http://example.com/cia5rhert00bwhkjxinogvtl2/cia5rhert00bxhkjxw62nijla/cia5rhert00byhkjxl5nlc2eo/cia5rhert00bzhkjxhlmdfgi7', + 'http://example.com/cia5rhert00c0hkjx4n6b40nr/cia5rhert00c1hkjxsnmkujr5/cia5rhert00c2hkjxv6w0h717/cia5rhert00c3hkjxrcvb5qs6', + 'http://example.com/cia5rhert00c4hkjxgd95z86n/cia5rhert00c5hkjxv1tvmf9c/cia5rhert00c6hkjx4cv3ix8y/cia5rhert00c7hkjxli2to7w7', + 'http://example.com/cia5rhert00c8hkjxgaiuvkul/cia5rhert00c9hkjxkcpdyj68/cia5rhert00cahkjxjxrfjfl0/cia5rhert00cbhkjxk64v5mhq', + 'http://example.com/cia5rhert00cchkjxzkjnwkn1/cia5rhert00cdhkjx1cdlk3ms/cia5rhert00cehkjxaw4lnp43/cia5rhert00cfhkjxz4hk5gsw', + 'http://example.com/cia5rhert00cghkjxpil3za50/cia5rhert00chhkjxp7t5jz46/cia5rhert00cihkjx5cwk8th0/cia5rhert00cjhkjxaxp4y2o0', + 'http://example.com/cia5rhert00ckhkjxcylxiiem/cia5rhert00clhkjxzkx50uda/cia5rhert00cmhkjxs5uv5t4e/cia5rhert00cnhkjxxz9rplpv', + 'http://example.com/cia5rhert00cohkjxa1r51z2k/cia5rhert00cphkjx1zw5bwvd/cia5rhert00cqhkjxkmcxldzz/cia5rhert00crhkjx8xm9oeny', + 'http://example.com/cia5rhert00cshkjxuoqaoftt/cia5rhert00cthkjx1x01dmec/cia5rhert00cuhkjx9sixehxp/cia5rhert00cvhkjxomyssiza', + 'http://example.com/cia5rhert00cwhkjx7et34wux/cia5rhert00cxhkjxncva74m3/cia5rhert00cyhkjxfkizvrik/cia5rhert00czhkjxgbzmphlh', + 'http://example.com/cia5rhert00d0hkjx5amei299/cia5rhert00d1hkjxumygde64/cia5rhert00d2hkjxpnuisg9m/cia5rhert00d3hkjxakoawt1r', + 'http://example.com/cia5rhert00d4hkjx1h1sd1xk/cia5rhert00d5hkjxiszqt7gt/cia5rhert00d6hkjxrgly5qmw/cia5rhert00d7hkjx0khb7is3', + 'http://example.com/cia5rhert00d8hkjx8q3rh7mm/cia5rhert00d9hkjxsc3r3d6p/cia5rhert00dahkjxi4ka0kza/cia5rhert00dbhkjxzvnrcsv0', + 'http://example.com/cia5rhert00dchkjxqlqg4rat/cia5rhert00ddhkjxtjjv0jz2/cia5rhert00dehkjxf7uayrst/cia5rhert00dfhkjxzjqnclyl', + 'http://example.com/cia5rhert00dghkjxdq5ojgf7/cia5rhert00dhhkjxs3fjff88/cia5rheru00dihkjxo2axpgvn/cia5rheru00djhkjx8lrs9xha', + 'http://example.com/cia5rheru00dkhkjx45m33mct/cia5rheru00dlhkjxzrjk6adw/cia5rheru00dmhkjxhhgorfhb/cia5rheru00dnhkjxwadtejpl', + 'http://example.com/cia5rheru00dohkjxj8rdgozp/cia5rheru00dphkjxsqktwr0d/cia5rheru00dqhkjxlodva5ul/cia5rheru00drhkjxy79ve13k', + 'http://example.com/cia5rheru00dshkjxwawhyuhd/cia5rheru00dthkjxovwtz7zd/cia5rheru00duhkjxnhebnm70/cia5rheru00dvhkjxt28pubcm', + 'http://example.com/cia5rheru00dwhkjxasdto0h0/cia5rheru00dxhkjxvoxohbir/cia5rheru00dyhkjxr50yvvwt/cia5rheru00dzhkjxiffhdptt', + 'http://example.com/cia5rheru00e0hkjx9htn1puz/cia5rheru00e1hkjxqvm483lo/cia5rheru00e2hkjxbr3v8ysf/cia5rheru00e3hkjx1xxys4u4', + 'http://example.com/cia5rheru00e4hkjx0pdbwiqd/cia5rheru00e5hkjxcjyvelv6/cia5rheru00e6hkjxfar4jska/cia5rheru00e7hkjxfkw7rkrq', + 'http://example.com/cia5rheru00e8hkjx1rtfronr/cia5rheru00e9hkjxpgfyuqxd/cia5rheru00eahkjx9hw9j0h3/cia5rheru00ebhkjx83acges9', + 'http://example.com/cia5rheru00echkjxbrhqzezc/cia5rheru00edhkjxu3iycpdz/cia5rheru00eehkjxs91i2o4s/cia5rheru00efhkjxgsawfxoa', + 'http://example.com/cia5rheru00eghkjxdhzfetw8/cia5rheru00ehhkjx1wnw1va3/cia5rheru00eihkjxo0vft0jh/cia5rheru00ejhkjxa8vgdyzn', + 'http://example.com/cia5rheru00ekhkjxfo36jiae/cia5rheru00elhkjxim0qkqcu/cia5rheru00emhkjxyv3cm8s8/cia5rheru00enhkjx4zsm4g1k', + 'http://example.com/cia5rherv00eohkjxl16e03j1/cia5rherv00ephkjxtfqgb09e/cia5rherv00eqhkjxxnuib8vx/cia5rherv00erhkjx3c2eh7rw', + 'http://example.com/cia5rherv00eshkjxk2ugn862/cia5rherv00ethkjx4a4i5bu4/cia5rherv00euhkjx4x1ll1hu/cia5rherv00evhkjx4a90801w', + 'http://example.com/cia5rherv00ewhkjx16ofuvju/cia5rherv00exhkjxbjicg8ee/cia5rherv00eyhkjxi37ly2xd/cia5rherv00ezhkjxme2gnc8n', + 'http://example.com/cia5rherv00f0hkjxupxvqy6f/cia5rherv00f1hkjxif6y2ebt/cia5rherv00f2hkjx2hxktwi1/cia5rherv00f3hkjxboq2udk4', + 'http://example.com/cia5rherv00f4hkjxe5t1s9vm/cia5rherv00f5hkjx2p1bkk8q/cia5rherv00f6hkjx3ugeare1/cia5rherv00f7hkjxw5xpdje2', + 'http://example.com/cia5rherv00f8hkjxr347e4en/cia5rherv00f9hkjxjmjvxubi/cia5rherv00fahkjxhgdld04j/cia5rherv00fbhkjxury8qhpe', + 'http://example.com/cia5rherv00fchkjx36kklfip/cia5rherv00fdhkjx51q4kxdz/cia5rherv00fehkjxm1xs1n6p/cia5rherv00ffhkjxzrms1ho0', + 'http://example.com/cia5rherv00fghkjxq54a40bh/cia5rherv00fhhkjxfjd33rlp/cia5rherv00fihkjx8ydqzwn4/cia5rherv00fjhkjx800mxgbz', + 'http://example.com/cia5rherv00fkhkjxr5ktedsn/cia5rherv00flhkjxkh6ip88y/cia5rherv00fmhkjxx824akqe/cia5rherv00fnhkjxn8b9z0y4', + 'http://example.com/cia5rherv00fohkjximtuc4oo/cia5rherv00fphkjxo8xnj3a1/cia5rherv00fqhkjx3frdvz0l/cia5rherv00frhkjxnt4ip06p', + 'http://example.com/cia5rherv00fshkjxebbqlsj4/cia5rherv00fthkjxhejhs2en/cia5rherv00fuhkjxww4v962t/cia5rherv00fvhkjx5n0tpk2r', + 'http://example.com/cia5rherv00fwhkjxomhb4741/cia5rherv00fxhkjxkgs095w0/cia5rherv00fyhkjxtve4rfnk/cia5rherv00fzhkjxkl9m13ke', + 'http://example.com/cia5rherv00g0hkjxwfqm1g52/cia5rherv00g1hkjx0sajw56m/cia5rherv00g2hkjxcs30a0m9/cia5rherv00g3hkjxuq2edyvx', + 'http://example.com/cia5rherv00g4hkjxpmffchhu/cia5rherv00g5hkjxkhh7fc92/cia5rherv00g6hkjxsak4bwp4/cia5rherv00g7hkjxemsqgdmx', + 'http://example.com/cia5rherv00g8hkjxxd4iaucx/cia5rherv00g9hkjx82yt525c/cia5rherv00gahkjx9higv8wz/cia5rherv00gbhkjx0bxq8ejn', + 'http://example.com/cia5rherv00gchkjxolbgqpvx/cia5rherv00gdhkjxh1q1mlra/cia5rherv00gehkjx6rmpv7a6/cia5rherv00gfhkjx7v471pja', + 'http://example.com/cia5rherv00gghkjx4swi554w/cia5rherv00ghhkjxrbcdqi5x/cia5rherv00gihkjxrzkje2z3/cia5rherv00gjhkjx23xefzn2', + 'http://example.com/cia5rherv00gkhkjx7om372u4/cia5rherv00glhkjx7t4qbrj9/cia5rherv00gmhkjxm4021iiz/cia5rherv00gnhkjxgxmosagw', + 'http://example.com/cia5rherv00gohkjx92hhv2g9/cia5rherv00gphkjx7bf3eo7t/cia5rherv00gqhkjx6wj13cjb/cia5rherv00grhkjx9xdd6xqg', + 'http://example.com/cia5rherv00gshkjxapw92pmq/cia5rherv00gthkjx7uqh0m79/cia5rherv00guhkjx9uxnghi5/cia5rherv00gvhkjx9dk24e4x', + 'http://example.com/cia5rherv00gwhkjxm8spj2z7/cia5rherv00gxhkjx1bzjkoj5/cia5rherv00gyhkjxj4n460vb/cia5rherv00gzhkjx6824pani', + 'http://example.com/cia5rherv00h0hkjx8wfm03s5/cia5rherv00h1hkjxboapbcoy/cia5rherv00h2hkjxqgzz6g4s/cia5rherv00h3hkjxcbpuhpk2', + 'http://example.com/cia5rherv00h4hkjx0eho0z4b/cia5rherv00h5hkjx1z59ioaa/cia5rherv00h6hkjxzqmxtasv/cia5rherv00h7hkjxflt86e5f', + 'http://example.com/cia5rherv00h8hkjxxlr7008t/cia5rherv00h9hkjxz8kctoc9/cia5rherv00hahkjxq60p0kx0/cia5rherv00hbhkjxd0zud58a', + 'http://example.com/cia5rherv00hchkjx4o8rphb9/cia5rherv00hdhkjxs3n7yko7/cia5rherv00hehkjxnxwntfg5/cia5rherv00hfhkjxb39xgejv', + 'http://example.com/cia5rherv00hghkjxb1yco8y1/cia5rherv00hhhkjx2v6pz42w/cia5rherv00hihkjx400aow1n/cia5rherv00hjhkjxtwik0d1s', + 'http://example.com/cia5rherv00hkhkjxwnaoc3oc/cia5rherv00hlhkjxsmntvjhm/cia5rherv00hmhkjx4ag0j8cq/cia5rherv00hnhkjxbiigrngm', + 'http://example.com/cia5rherv00hohkjx2s4opprd/cia5rherv00hphkjx8pkc39p2/cia5rherv00hqhkjxif9qfvct/cia5rherv00hrhkjxe4vtvd6z', + 'http://example.com/cia5rherv00hshkjxampz8fwm/cia5rherv00hthkjxf48ybrar/cia5rherv00huhkjx3asrrys5/cia5rherv00hvhkjxc570882r', + 'http://example.com/cia5rherv00hwhkjxfooadgv0/cia5rherv00hxhkjxnmtttd03/cia5rherv00hyhkjx0deqal5b/cia5rherv00hzhkjxleqe94v1', + 'http://example.com/cia5rherv00i0hkjxs3ps41wq/cia5rherv00i1hkjxw46qft2r/cia5rherv00i2hkjxfmfjpssy/cia5rherv00i3hkjxayjpzx43', + 'http://example.com/cia5rherv00i4hkjxpxotlyhp/cia5rherv00i5hkjxhz77s1t6/cia5rherv00i6hkjxwgw9wpq7/cia5rherv00i7hkjxy724la5w', + 'http://example.com/cia5rherv00i8hkjx6wko56vc/cia5rherv00i9hkjxiqs85dqr/cia5rherv00iahkjx32pvc8lh/cia5rherv00ibhkjxouu7jcs7', + 'http://example.com/cia5rherv00ichkjxub141io8/cia5rherv00idhkjx4xp7yqzz/cia5rherv00iehkjx4l9xkah3/cia5rherv00ifhkjx3qco4ypm', + 'http://example.com/cia5rherv00ighkjx9sp14gne/cia5rherv00ihhkjx713r0569/cia5rherv00iihkjxorq3d5yp/cia5rherv00ijhkjxtdo94p17', + 'http://example.com/cia5rherv00ikhkjx4j5jbzre/cia5rherv00ilhkjxcgsn6xlu/cia5rherv00imhkjxx8la8kf5/cia5rherv00inhkjx3hz5opiw', + 'http://example.com/cia5rherv00iohkjx3dgmyl4e/cia5rherv00iphkjx8glt32u3/cia5rherv00iqhkjx6cb6wlxs/cia5rherv00irhkjxza76xuqt', + 'http://example.com/cia5rherv00ishkjxo51i49wr/cia5rherv00ithkjxxa546aho/cia5rherv00iuhkjx50q0dsyb/cia5rherv00ivhkjxcq50j6b5', + 'http://example.com/cia5rherv00iwhkjxaklfq2jh/cia5rherv00ixhkjxq47wm5d8/cia5rherv00iyhkjx93fsh773/cia5rherv00izhkjx05qw5ls6', + 'http://example.com/cia5rherv00j0hkjxcpk80dqg/cia5rherv00j1hkjxe5fze7ph/cia5rherv00j2hkjx0b4x6jj2/cia5rherv00j3hkjxslnfjk8f', + 'http://example.com/cia5rherv00j4hkjxn02a7r4w/cia5rherv00j5hkjxqbq2abzk/cia5rherv00j6hkjx046gf9gh/cia5rherv00j7hkjxw8y6aybl', + 'http://example.com/cia5rherv00j8hkjxua9fh4f2/cia5rherv00j9hkjx0n7j7tf0/cia5rherv00jahkjx1h8p0yzt/cia5rherv00jbhkjx6isout34', + 'http://example.com/cia5rherv00jchkjxscxpp7mk/cia5rherv00jdhkjxx7314ajy/cia5rherv00jehkjx2wej8wjh/cia5rherv00jfhkjxs4i5eiho', + 'http://example.com/cia5rherv00jghkjxcrkt8o3t/cia5rherv00jhhkjx2u0sm2r0/cia5rherv00jihkjxtpjif9k6/cia5rherv00jjhkjxaagcsohi', + 'http://example.com/cia5rherv00jkhkjx8gq5x6ov/cia5rherv00jlhkjxev9zq161/cia5rherv00jmhkjx2sjcp2px/cia5rherv00jnhkjxn8jpfipq', + 'http://example.com/cia5rherv00johkjxbx8744i5/cia5rherv00jphkjxi2eza54a/cia5rherv00jqhkjx1q5y2n4p/cia5rherv00jrhkjxvl0soujd', + 'http://example.com/cia5rherv00jshkjxs7ziy4vs/cia5rherv00jthkjx3ewtvj26/cia5rherv00juhkjxkwhid4b7/cia5rherv00jvhkjxx7nurgrx', + 'http://example.com/cia5rherv00jwhkjxxlcfjhj3/cia5rherv00jxhkjxoresrx2f/cia5rherv00jyhkjxtsmt6apo/cia5rherv00jzhkjxghybzsqs', + 'http://example.com/cia5rherv00k0hkjx7o0bc0o5/cia5rherv00k1hkjxmzwu9w7x/cia5rherv00k2hkjxw6z2g76g/cia5rherv00k3hkjx8htly8zt', + 'http://example.com/cia5rherv00k4hkjx59yd941i/cia5rherv00k5hkjx0j5ccesw/cia5rherv00k6hkjxduxlqh65/cia5rherv00k7hkjx5et0n6t0', + 'http://example.com/cia5rherv00k8hkjxj72kcc3p/cia5rherv00k9hkjx0zy8vr32/cia5rherv00kahkjx6kmi5wtf/cia5rherv00kbhkjxh2ut5f4w', + 'http://example.com/cia5rherv00kchkjxq9ki0lgr/cia5rherv00kdhkjx8mafuept/cia5rherv00kehkjx3kffqdj5/cia5rherv00kfhkjxk30tt2a0', + 'http://example.com/cia5rherv00kghkjxaw98qyx2/cia5rherv00khhkjxieg0bycx/cia5rherw00kihkjxdmdkp4x9/cia5rherw00kjhkjxijtzapeq', + 'http://example.com/cia5rherw00kkhkjxh1oh0kl4/cia5rherw00klhkjx6ffidsax/cia5rherw00kmhkjxqtgyxcy0/cia5rherw00knhkjx2xyipie3', + 'http://example.com/cia5rherw00kohkjxdq8vr7d0/cia5rherw00kphkjxes36xbks/cia5rherw00kqhkjx4ektym79/cia5rherw00krhkjxib4btsp0', + 'http://example.com/cia5rherw00kshkjxpc4m0m9e/cia5rherw00kthkjxgdi6nnzj/cia5rherw00kuhkjxcwj7gk86/cia5rherw00kvhkjx7cj949ld', + 'http://example.com/cia5rherw00kwhkjxmwj8brl1/cia5rherw00kxhkjxae8yeb7p/cia5rherw00kyhkjxqze289bb/cia5rherw00kzhkjx2jy2h9ji', + 'http://example.com/cia5rherw00l0hkjxxadfzs3n/cia5rherw00l1hkjxhngih2c7/cia5rherw00l2hkjxn5zwgxrx/cia5rherw00l3hkjxz3mp1gb9', + 'http://example.com/cia5rherw00l4hkjxhcw0erdu/cia5rherw00l5hkjxj6j3l2zh/cia5rherw00l6hkjx533r5z4r/cia5rherw00l7hkjxvjjbvgo6', + 'http://example.com/cia5rherw00l8hkjxx61zlkek/cia5rherw00l9hkjxb7xj73l2/cia5rherw00lahkjxrpt6fk4m/cia5rherw00lbhkjxlron4prb', + 'http://example.com/cia5rherw00lchkjxtq99vutt/cia5rherw00ldhkjxon95rdss/cia5rherw00lehkjxblctcrfw/cia5rherw00lfhkjxc3mffk2x', + 'http://example.com/cia5rherw00lghkjx4v9dujig/cia5rherw00lhhkjx734cxan0/cia5rherw00lihkjxlxx3bx3y/cia5rherw00ljhkjxa5te3vm6', + 'http://example.com/cia5rherw00lkhkjxy4akb3lo/cia5rherw00llhkjxxg2t6ecs/cia5rherw00lmhkjx5v0ur1zi/cia5rherw00lnhkjxuw4grqgg', + 'http://example.com/cia5rherw00lohkjx1tx7bq0f/cia5rherw00lphkjx118uu54q/cia5rherw00lqhkjxo63inpim/cia5rherw00lrhkjx60iprrlh', + 'http://example.com/cia5rherw00lshkjxq67z9znh/cia5rherw00lthkjxknfimvv6/cia5rherw00luhkjxln059pxx/cia5rherw00lvhkjx0b5u1wvh', + 'http://example.com/cia5rherw00lwhkjxdnbo7o8x/cia5rherw00lxhkjxialw39ph/cia5rherw00lyhkjxq44794oz/cia5rherw00lzhkjx063oj379', + 'http://example.com/cia5rherw00m0hkjxa51hcx2d/cia5rherw00m1hkjx5udmt5pw/cia5rherw00m2hkjxh2zenkxy/cia5rherw00m3hkjxaa4b1ukf', + 'http://example.com/cia5rherw00m4hkjxu0txyp2a/cia5rherw00m5hkjxku5t6nia/cia5rherw00m6hkjxpuvkxgxe/cia5rherw00m7hkjx7mgdnt4i', + 'http://example.com/cia5rherw00m8hkjx9tjak8nq/cia5rherw00m9hkjx6tjbc4c1/cia5rherw00mahkjxhrlq09fa/cia5rherw00mbhkjxncngvvyl', + 'http://example.com/cia5rherw00mchkjxqjijeepd/cia5rherw00mdhkjx5n3u57g1/cia5rherw00mehkjxlhfhx86m/cia5rherw00mfhkjxhtz36qq5', + 'http://example.com/cia5rherw00mghkjxiknnl4br/cia5rherw00mhhkjxl1kv5f98/cia5rherw00mihkjxzo6yez34/cia5rherw00mjhkjx2ffeb3lh', + 'http://example.com/cia5rherw00mkhkjxziimpsbk/cia5rherw00mlhkjxp3bcocf5/cia5rherw00mmhkjxbwztbp6k/cia5rherw00mnhkjxyc9eixhz', + 'http://example.com/cia5rherw00mohkjxm8kai7n5/cia5rherw00mphkjx65gvhaf2/cia5rherw00mqhkjxfh4vu8eq/cia5rherw00mrhkjxfv2gj9bt', + 'http://example.com/cia5rherw00mshkjxmnuhrdj4/cia5rherw00mthkjxjj5iizh6/cia5rherw00muhkjxiw62tfl7/cia5rherw00mvhkjxhzemezd3', + 'http://example.com/cia5rherw00mwhkjxlml9rs61/cia5rherw00mxhkjxl2mbwrnp/cia5rherw00myhkjxj96ye1zv/cia5rherw00mzhkjxodju2h5o', + 'http://example.com/cia5rherw00n0hkjxuivmtahu/cia5rherw00n1hkjx8gbwu61v/cia5rherw00n2hkjx1j9gzlce/cia5rherw00n3hkjxaz67uu2l', + 'http://example.com/cia5rherw00n4hkjxoz2v47cp/cia5rherw00n5hkjxc2m0xq1d/cia5rherw00n6hkjxs5pej7ym/cia5rherw00n7hkjxrj28b2tt', + 'http://example.com/cia5rherw00n8hkjxikm4mbwi/cia5rherw00n9hkjxcfsu9f9d/cia5rherw00nahkjxawec3u0q/cia5rherw00nbhkjx38j8uycv', + 'http://example.com/cia5rherw00nchkjxpxks6dv1/cia5rherw00ndhkjxme2uh9cm/cia5rherw00nehkjxiu6s9qet/cia5rherw00nfhkjxjeycmmn7', + 'http://example.com/cia5rherw00nghkjx66hdwbtg/cia5rherw00nhhkjxki3i81i4/cia5rherw00nihkjxvve944gl/cia5rherw00njhkjx3jwe46il', + 'http://example.com/cia5rherw00nkhkjx8znncv54/cia5rherw00nlhkjxsyqrjo5g/cia5rherw00nmhkjxkir8br45/cia5rherw00nnhkjxexumdwa2', + 'http://example.com/cia5rherw00nohkjxzs49c8vj/cia5rherw00nphkjx5eq7mllr/cia5rherw00nqhkjxxqc7wn0n/cia5rherw00nrhkjx3xnlk87f', + 'http://example.com/cia5rherw00nshkjxis89wcyt/cia5rherw00nthkjxmo962d98/cia5rherw00nuhkjx5102qsyd/cia5rherw00nvhkjx4dhdkq7d', + 'http://example.com/cia5rherw00nwhkjx6o2era0y/cia5rherw00nxhkjxibwv5nl3/cia5rherw00nyhkjxnobkkmmt/cia5rherw00nzhkjx2rfj3yw8', + 'http://example.com/cia5rherw00o0hkjxys1f6cpt/cia5rherw00o1hkjxv7y1wk16/cia5rherw00o2hkjx72rwd3p7/cia5rherw00o3hkjxdpw2fjf4', + 'http://example.com/cia5rherw00o4hkjxj0qwjpur/cia5rherw00o5hkjxr0vma4yn/cia5rherw00o6hkjxjdio6m66/cia5rherw00o7hkjx6tah5wos', + 'http://example.com/cia5rherw00o8hkjxtseqnpix/cia5rherw00o9hkjxcll29qxs/cia5rherw00oahkjxy6j4514j/cia5rherw00obhkjxyt12uhto', + 'http://example.com/cia5rherw00ochkjxv94azfo3/cia5rherw00odhkjxvvb9a4cf/cia5rherw00oehkjxro8j0dau/cia5rherw00ofhkjxazh1bnkv', + 'http://example.com/cia5rherw00oghkjxaiqblgaz/cia5rherw00ohhkjxq0jg2ekb/cia5rherw00oihkjxhxp787hh/cia5rherw00ojhkjxwn2bygkc', + 'http://example.com/cia5rherw00okhkjxrvuzh590/cia5rherw00olhkjxukov5sdm/cia5rherw00omhkjxprnz66kz/cia5rherw00onhkjxd19rb8es', + 'http://example.com/cia5rherw00oohkjxdp73iojj/cia5rherw00ophkjx0ejclbnc/cia5rherw00oqhkjxzbrto5j9/cia5rherw00orhkjx64ybndrk', + 'http://example.com/cia5rherw00oshkjxto8g8o9d/cia5rherw00othkjx0siti4zs/cia5rherw00ouhkjxn4k2rxwd/cia5rherw00ovhkjxlfh7kchr', + 'http://example.com/cia5rherw00owhkjx5u0y9ly7/cia5rherw00oxhkjxuc2947n2/cia5rherw00oyhkjxclqlf4cz/cia5rherw00ozhkjxw4ljr3n2', + 'http://example.com/cia5rherw00p0hkjxprwdryfg/cia5rherw00p1hkjxnm9mqxjr/cia5rherw00p2hkjxo97f8u6g/cia5rherw00p3hkjxu3v7vxny', + 'http://example.com/cia5rherw00p4hkjx52wsz26a/cia5rherw00p5hkjxbcznr0do/cia5rherw00p6hkjxytrdjjnt/cia5rherw00p7hkjx6l5uvkab', + 'http://example.com/cia5rherw00p8hkjxm2zue659/cia5rherx00p9hkjxbkzeky4l/cia5rherx00pahkjxrqp8ljqj/cia5rherx00pbhkjxuxod17kg', + 'http://example.com/cia5rherx00pchkjxa26ekxyy/cia5rherx00pdhkjx0kxle2w1/cia5rherx00pehkjxxugq6n6r/cia5rherx00pfhkjx2ucx69kc', + 'http://example.com/cia5rherx00pghkjxnqt22yl7/cia5rherx00phhkjxuxv53gmq/cia5rherx00pihkjxcb70hxmq/cia5rherx00pjhkjxlvjdb6xl', + 'http://example.com/cia5rherx00pkhkjxjc8qkw9h/cia5rherx00plhkjxcbs6t9k7/cia5rherx00pmhkjxh6cijkuv/cia5rherx00pnhkjxpf14evbg', + 'http://example.com/cia5rherx00pohkjxfgxqyo6d/cia5rherx00pphkjxtpilb91o/cia5rherx00pqhkjxny7abx9y/cia5rherx00prhkjx0l5g83bc', + 'http://example.com/cia5rherx00pshkjxxuxfrdbr/cia5rherx00pthkjxxnj6u6sk/cia5rherx00puhkjxpubm3g6s/cia5rherx00pvhkjxyj7u9c6t', + 'http://example.com/cia5rherx00pwhkjx6ppgibkl/cia5rherx00pxhkjxvv4p4kry/cia5rherx00pyhkjxpkbho8g0/cia5rherx00pzhkjxivjo0784', + 'http://example.com/cia5rherx00q0hkjxffxtz7i5/cia5rherx00q1hkjxygfowug9/cia5rherx00q2hkjxvsau87zx/cia5rherx00q3hkjx6z1kw9b2', + 'http://example.com/cia5rherx00q4hkjx4auglr08/cia5rherx00q5hkjxno848f23/cia5rherx00q6hkjxy6y8cv6y/cia5rherx00q7hkjxzzoogxhg', + 'http://example.com/cia5rherx00q8hkjx70m64of5/cia5rherx00q9hkjxg5c1aukr/cia5rherx00qahkjxqn1h5a85/cia5rherx00qbhkjxg2cf36ig', + 'http://example.com/cia5rherx00qchkjxlwf1o9ji/cia5rherx00qdhkjx1gdhjcsr/cia5rherx00qehkjx172b5dpn/cia5rherx00qfhkjxe3uruk25', + 'http://example.com/cia5rherx00qghkjx2ptty8ex/cia5rherx00qhhkjx5latps1q/cia5rherx00qihkjx9bdo19z2/cia5rherx00qjhkjxfj5a849t', + 'http://example.com/cia5rherx00qkhkjxataa91qp/cia5rherx00qlhkjxkos37rp2/cia5rherx00qmhkjxnr52z1ck/cia5rherx00qnhkjxg2wv4j3b', + 'http://example.com/cia5rherx00qohkjx5zybfy6z/cia5rherx00qphkjx3jotkzjk/cia5rherx00qqhkjxzqnuoxc1/cia5rherx00qrhkjxjqx7n6dd', + 'http://example.com/cia5rherx00qshkjxfusl4u8i/cia5rherx00qthkjx7nax9k3j/cia5rherx00quhkjxce6sda7o/cia5rherx00qvhkjxvjw9krhf', + 'http://example.com/cia5rherx00qwhkjx3myek18h/cia5rherx00qxhkjxye1vc3g5/cia5rherx00qyhkjx0qnkwhv8/cia5rherx00qzhkjx7xbpfb2g', + 'http://example.com/cia5rherx00r0hkjxr05rkysp/cia5rherx00r1hkjx5skxve27/cia5rherx00r2hkjxm42ww2wl/cia5rherx00r3hkjxvgaok3d7', + 'http://example.com/cia5rherx00r4hkjxhqn73qfk/cia5rherx00r5hkjx0vhqzi3y/cia5rherx00r6hkjxzo83uw13/cia5rherx00r7hkjxshtbkjap', + 'http://example.com/cia5rherx00r8hkjxkolk05tr/cia5rherx00r9hkjx87txftcu/cia5rherx00rahkjx7zt1mxfl/cia5rherx00rbhkjxj3225obu', + 'http://example.com/cia5rherx00rchkjxec3j4cbw/cia5rherx00rdhkjx2o60bre2/cia5rherx00rehkjx1gza5jo1/cia5rherx00rfhkjx6i15t347', + 'http://example.com/cia5rherx00rghkjxqsx1tilb/cia5rherx00rhhkjxld7q3ees/cia5rherx00rihkjxywwmwh7a/cia5rherx00rjhkjxa7lzkj0b', + 'http://example.com/cia5rherx00rkhkjxo2uekt9y/cia5rherx00rlhkjxz1y92fdx/cia5rherx00rmhkjxrls010bq/cia5rherx00rnhkjx5phg7y9q', + 'http://example.com/cia5rherx00rohkjxt69rlmpb/cia5rherx00rphkjxxdo9vbof/cia5rherx00rqhkjxgnvs2gxf/cia5rherx00rrhkjx5vv7kk7v', + 'http://example.com/cia5rherx00rshkjxfn0v3dx4/cia5rherx00rthkjx5wkktp8p/cia5rherx00ruhkjx6palbn57/cia5rherx00rvhkjxugk01wvb', + 'http://example.com/cia5rherx00rwhkjxzgza4olt/cia5rherx00rxhkjxwdssdcbj/cia5rherx00ryhkjxxnyxv3t6/cia5rherx00rzhkjxhr6w38i4', + 'http://example.com/cia5rherx00s0hkjxerf3k9ib/cia5rherx00s1hkjxnqqw079u/cia5rherx00s2hkjxifw1h8n3/cia5rherx00s3hkjxd05rx85o', + 'http://example.com/cia5rherx00s4hkjx2x89mh5w/cia5rherx00s5hkjx87d0h7li/cia5rherx00s6hkjxqjueoeqw/cia5rherx00s7hkjxyq9w3n82', + 'http://example.com/cia5rherx00s8hkjxzigd1zp5/cia5rherx00s9hkjxdtx2amst/cia5rherx00sahkjx2onc2f21/cia5rherx00sbhkjx4hgu22zb', + 'http://example.com/cia5rherx00schkjxrz7trjeu/cia5rherx00sdhkjxwmrp365i/cia5rherx00sehkjx7eq317yf/cia5rherx00sfhkjxo93dnhcw', + 'http://example.com/cia5rherx00sghkjx2zsmv5zb/cia5rherx00shhkjxuu1u80qs/cia5rherx00sihkjx8avektr4/cia5rherx00sjhkjxpg3tjre5', + 'http://example.com/cia5rherx00skhkjxm9hrr8dp/cia5rherx00slhkjxmklu8fxx/cia5rherx00smhkjxpz58b4co/cia5rherx00snhkjx6kflkbwz', + 'http://example.com/cia5rherx00sohkjx6zveco0b/cia5rherx00sphkjx9tzv10q9/cia5rherx00sqhkjx5kw9y9vt/cia5rherx00srhkjx5q2dpc7o', + 'http://example.com/cia5rherx00sshkjxfmf3zzhg/cia5rherx00sthkjx085rnzf5/cia5rherx00suhkjxo7eaxytl/cia5rherx00svhkjx3bfbsur9', + 'http://example.com/cia5rherx00swhkjxzihyd64f/cia5rherx00sxhkjxor1bcxl3/cia5rhery00syhkjxlensj1wa/cia5rhery00szhkjxwk1jdpzj', + 'http://example.com/cia5rhery00t0hkjxz5hf0kfl/cia5rhery00t1hkjx36ar1r16/cia5rhery00t2hkjxcv7t3hqq/cia5rhery00t3hkjxkzgqe0a6', + 'http://example.com/cia5rhery00t4hkjxpmbibq16/cia5rhery00t5hkjxrqr5n4lt/cia5rhery00t6hkjx4npmmnvj/cia5rhery00t7hkjxewqanavg', + 'http://example.com/cia5rhery00t8hkjxci9wud4s/cia5rhery00t9hkjxui809qxy/cia5rhery00tahkjx870oqect/cia5rhery00tbhkjx8g24crc0', + 'http://example.com/cia5rhery00tchkjxzjllkr5i/cia5rhery00tdhkjxqnnjio0r/cia5rhery00tehkjxnice4c5a/cia5rhery00tfhkjx0b0tfkbd', + 'http://example.com/cia5rhery00tghkjx1hfn5jnm/cia5rhery00thhkjx0m68lrh0/cia5rhery00tihkjxe24uktvm/cia5rhery00tjhkjxwudbxvgf', + 'http://example.com/cia5rhery00tkhkjxyupjrqmt/cia5rhery00tlhkjxns9kt84a/cia5rhery00tmhkjxnjnkvsza/cia5rhery00tnhkjx2u1kf42m', + 'http://example.com/cia5rhery00tohkjxv8euxxvv/cia5rhery00tphkjxcewtixg8/cia5rhery00tqhkjxm0fvhod1/cia5rhery00trhkjxzfels6hy', + 'http://example.com/cia5rhery00tshkjxcnmhpytv/cia5rhery00tthkjxgmwy284j/cia5rhery00tuhkjxvl9f0bet/cia5rhery00tvhkjxvd9h00tu', + 'http://example.com/cia5rhery00twhkjxworddumj/cia5rhery00txhkjx5hn3bob3/cia5rhery00tyhkjxwxhy5o31/cia5rhery00tzhkjxy7swe892', + 'http://example.com/cia5rhery00u0hkjx9v2rskyu/cia5rhery00u1hkjxt65535lp/cia5rhery00u2hkjxiephk9x0/cia5rhery00u3hkjxylul9icr', + 'http://example.com/cia5rhery00u4hkjxo1tucbyj/cia5rhery00u5hkjxchfewpdu/cia5rhery00u6hkjxzh1f7dsv/cia5rhery00u7hkjxow4myzvc', + 'http://example.com/cia5rhery00u8hkjxjkljwzmx/cia5rhery00u9hkjxb2hq0zff/cia5rhery00uahkjx3zil5iye/cia5rhery00ubhkjxpx6l4i62', + 'http://example.com/cia5rhery00uchkjxzybwg1aj/cia5rhery00udhkjxbc85v998/cia5rhery00uehkjx9x8k1ebt/cia5rhery00ufhkjxziinfjvs', + 'http://example.com/cia5rhery00ughkjxqjq7rbqe/cia5rhery00uhhkjxn422gi7s/cia5rhery00uihkjx6wdh0kru/cia5rhery00ujhkjxgx2z6i30', + 'http://example.com/cia5rhery00ukhkjxxwekiqd8/cia5rhery00ulhkjxti0u03ji/cia5rhery00umhkjxneh94911/cia5rhery00unhkjx5uothdt2', + 'http://example.com/cia5rhery00uohkjxh8wvz750/cia5rhery00uphkjxya408v8j/cia5rhery00uqhkjxowcw4c0j/cia5rhery00urhkjxp0hxhyjr', + 'http://example.com/cia5rhery00ushkjxj5ezkt47/cia5rhery00uthkjxfcbhp09u/cia5rhery00uuhkjxw14otqjw/cia5rhery00uvhkjxhkdyfxrs', + 'http://example.com/cia5rhery00uwhkjxehm078n0/cia5rhery00uxhkjxbadu0bio/cia5rhery00uyhkjxak1ocp8h/cia5rhery00uzhkjxing0qkah', + 'http://example.com/cia5rhery00v0hkjxl8s5to3x/cia5rhery00v1hkjx9t9obxjk/cia5rhery00v2hkjx37ndtfyo/cia5rhery00v3hkjxgsrqx8wo', + 'http://example.com/cia5rhery00v4hkjxcv3mqk3k/cia5rhery00v5hkjxm8gbw43x/cia5rhery00v6hkjxu55dmspc/cia5rhery00v7hkjx34me677j', + 'http://example.com/cia5rhery00v8hkjxwecik8go/cia5rhery00v9hkjxap89471j/cia5rhery00vahkjxllo77l7s/cia5rhery00vbhkjxrqbtypmt', + 'http://example.com/cia5rhery00vchkjx4uu12kzr/cia5rhery00vdhkjx5sxrg1cw/cia5rhery00vehkjxpimi78w5/cia5rhery00vfhkjxvu1h0bnc', + 'http://example.com/cia5rhery00vghkjxryq1kku2/cia5rhery00vhhkjx8yq7g6dg/cia5rhery00vihkjxrhoe95rs/cia5rhery00vjhkjxg036tj71', + 'http://example.com/cia5rhery00vkhkjxfk603ubw/cia5rhery00vlhkjxv6cvncpa/cia5rhery00vmhkjxtlptcbfj/cia5rhery00vnhkjx019qtozs', + 'http://example.com/cia5rhery00vohkjx55roqfyq/cia5rhery00vphkjxq51jjd0w/cia5rhery00vqhkjxzl5r049u/cia5rhery00vrhkjx8w085tma', + 'http://example.com/cia5rhery00vshkjx43gw6sxr/cia5rhery00vthkjx20l8em51/cia5rhery00vuhkjxdwoh2vk9/cia5rhery00vvhkjxw1g6vrut', + 'http://example.com/cia5rhery00vwhkjxvn2ebxzm/cia5rhery00vxhkjxz2hlqhzd/cia5rhery00vyhkjxqvadz9wb/cia5rhery00vzhkjx9rh2a3uh', + 'http://example.com/cia5rhery00w0hkjxa0rqkpov/cia5rhery00w1hkjxp833u09z/cia5rhery00w2hkjxn2awahj4/cia5rhery00w3hkjxwqcb9cgd', + 'http://example.com/cia5rhery00w4hkjx2qn7xtr0/cia5rhery00w5hkjx4mw9p5o5/cia5rhery00w6hkjx1h6f1nne/cia5rhery00w7hkjxqgof32en', + 'http://example.com/cia5rhery00w8hkjx40u225qd/cia5rhery00w9hkjx6jc1e5lj/cia5rhery00wahkjxqqn44l7k/cia5rhery00wbhkjxdwnm2lan', + 'http://example.com/cia5rhery00wchkjxmf3n36f9/cia5rhery00wdhkjxjitsfpkb/cia5rhery00wehkjxrwzzunbx/cia5rhery00wfhkjxlu5ar2zw', + 'http://example.com/cia5rhery00wghkjxkoktvbqj/cia5rhery00whhkjxjs6l9c1l/cia5rhery00wihkjx4c6l8wcz/cia5rhery00wjhkjxwrrx1kta', + 'http://example.com/cia5rhery00wkhkjx4o64pmsg/cia5rhery00wlhkjx5jwjko3n/cia5rhery00wmhkjxuj79fu0e/cia5rhery00wnhkjxdxjnmwu4', + 'http://example.com/cia5rhery00wohkjxnj9yz95o/cia5rhery00wphkjxqs39n7we/cia5rhery00wqhkjxw7enbbt8/cia5rhery00wrhkjxhirvzsj8', + 'http://example.com/cia5rhery00wshkjxklfnj99r/cia5rhery00wthkjxyvxvxzxm/cia5rhery00wuhkjxselr08ti/cia5rhery00wvhkjx0n4m1gwb', + 'http://example.com/cia5rhery00wwhkjxidbq1641/cia5rhery00wxhkjxer0uj0bx/cia5rhery00wyhkjx3rj98a5m/cia5rhery00wzhkjxar7ucjyp', + 'http://example.com/cia5rhery00x0hkjxikq8tega/cia5rhery00x1hkjxgq9t00p7/cia5rhery00x2hkjxzczhf4ta/cia5rhery00x3hkjxs8rl0xle', + 'http://example.com/cia5rhery00x4hkjxtpgdpam7/cia5rhery00x5hkjxnudpwm02/cia5rhery00x6hkjx96jfugp6/cia5rhery00x7hkjxugyl5bsm', + 'http://example.com/cia5rhery00x8hkjx26k3912r/cia5rhery00x9hkjx8j1o37fy/cia5rhery00xahkjxcnx1kjtl/cia5rhery00xbhkjxws0y4q9u', + 'http://example.com/cia5rhery00xchkjxnceot2tu/cia5rhery00xdhkjxmfcanvsn/cia5rhery00xehkjxti3dt4zk/cia5rhery00xfhkjx4r9pxmk8', + 'http://example.com/cia5rhery00xghkjxcl0s61iu/cia5rhery00xhhkjxy85ou2fq/cia5rhery00xihkjx8p53n3u3/cia5rhery00xjhkjx6dzo2asw', + 'http://example.com/cia5rhery00xkhkjxcfvflel3/cia5rhery00xlhkjxjs82vnte/cia5rhery00xmhkjxrisis221/cia5rhery00xnhkjx92ojt9kd', + 'http://example.com/cia5rhery00xohkjxxu18v57w/cia5rhery00xphkjx5gfp65ut/cia5rhery00xqhkjx0zug4xqu/cia5rhery00xrhkjxxqj8k3ce', + 'http://example.com/cia5rhery00xshkjxbpshqaoi/cia5rhery00xthkjxthb498xj/cia5rhery00xuhkjxu6o0heam/cia5rhery00xvhkjxcxp7f3yh', + 'http://example.com/cia5rhery00xwhkjxre2n0fww/cia5rhery00xxhkjxtu8s69t6/cia5rhery00xyhkjx7q1fm4xo/cia5rhery00xzhkjx6o1nu6ga', + 'http://example.com/cia5rhery00y0hkjxlsfyk6o1/cia5rhery00y1hkjxpaqyucwy/cia5rhery00y2hkjxapp8yfj0/cia5rhery00y3hkjx0fnzbtnb', + 'http://example.com/cia5rhery00y4hkjxlm2p2v6d/cia5rhery00y5hkjx0vzxj59x/cia5rhery00y6hkjxkamlg3ck/cia5rhery00y7hkjxkayvfx3a', + 'http://example.com/cia5rhery00y8hkjxdwdcp5cs/cia5rherz00y9hkjxjb0teg8f/cia5rherz00yahkjxs1uzi2l6/cia5rherz00ybhkjxp449moik', + 'http://example.com/cia5rherz00ychkjxwvi52xlt/cia5rherz00ydhkjxr7g9xhla/cia5rherz00yehkjxmrqqq92m/cia5rherz00yfhkjxljrtx13a', + 'http://example.com/cia5rherz00yghkjx6sxe01ps/cia5rherz00yhhkjxh27l6kvi/cia5rherz00yihkjxbrazjcpk/cia5rherz00yjhkjxmi6ft3qb', + 'http://example.com/cia5rherz00ykhkjxifkk8p2x/cia5rherz00ylhkjxpqz9t3cb/cia5rherz00ymhkjxc00r45v0/cia5rherz00ynhkjxgnbgvycv', + 'http://example.com/cia5rherz00yohkjxmr9yq0jk/cia5rherz00yphkjxaamwbi9t/cia5rherz00yqhkjxv53l03jj/cia5rherz00yrhkjxsvkylos9', + 'http://example.com/cia5rherz00yshkjx3vhhy1ut/cia5rherz00ythkjxnx4cssnw/cia5rherz00yuhkjxyojj0lzk/cia5rherz00yvhkjxoyozoftr', + 'http://example.com/cia5rherz00ywhkjxl9hqei9p/cia5rherz00yxhkjxyefcfdtf/cia5rherz00yyhkjxpmcgonjp/cia5rherz00yzhkjxtp7r9f3r', + 'http://example.com/cia5rherz00z0hkjxrhistyei/cia5rherz00z1hkjxinya1udt/cia5rherz00z2hkjxt2uibesw/cia5rherz00z3hkjxrv504cf7', + 'http://example.com/cia5rherz00z4hkjxo0a9j311/cia5rherz00z5hkjxazvef1je/cia5rherz00z6hkjxb75qrdko/cia5rherz00z7hkjxnv1dgzal', + 'http://example.com/cia5rherz00z8hkjx8ny20q55/cia5rherz00z9hkjxwlj70f0m/cia5rherz00zahkjx42hyz0m2/cia5rherz00zbhkjxwotnxn7y', + 'http://example.com/cia5rherz00zchkjxa1tke93x/cia5rherz00zdhkjxzd8wghy0/cia5rherz00zehkjxba383v6a/cia5rherz00zfhkjxjkcs4bwl', + 'http://example.com/cia5rherz00zghkjxk8sklzkb/cia5rherz00zhhkjxfwy3q53n/cia5rherz00zihkjxvheuc2pr/cia5rherz00zjhkjxbzljm6zl', + 'http://example.com/cia5rherz00zkhkjxv84kqu85/cia5rherz00zlhkjxso9bvlw7/cia5rherz00zmhkjxib6w80kz/cia5rherz00znhkjx41d5nf7s', + 'http://example.com/cia5rherz00zohkjxdd3iy5vu/cia5rherz00zphkjxwad53vl4/cia5rherz00zqhkjx4ad3e109/cia5rherz00zrhkjx4r6bwsia', + 'http://example.com/cia5rherz00zshkjx1rzmmdvs/cia5rherz00zthkjx6165yn1j/cia5rherz00zuhkjxnefkdvqx/cia5rherz00zvhkjxgm3q4960', + 'http://example.com/cia5rherz00zwhkjx78quz63t/cia5rherz00zxhkjxdj7uhzb1/cia5rherz00zyhkjxgfrciagd/cia5rherz00zzhkjx2fhew3jm', + 'http://example.com/cia5rherz0100hkjxpoguaspc/cia5rherz0101hkjxj8wf0hvv/cia5rherz0102hkjxft1bu9bg/cia5rherz0103hkjxh7rc7icq', + 'http://example.com/cia5rherz0104hkjxl336njqd/cia5rherz0105hkjxq5od0pbq/cia5rherz0106hkjxtgk2vqld/cia5rherz0107hkjxnyquy58x', + 'http://example.com/cia5rherz0108hkjx0e8vlkuv/cia5rherz0109hkjx1w4ao3zl/cia5rherz010ahkjx0uvsczyx/cia5rherz010bhkjxctueaktb', + 'http://example.com/cia5rherz010chkjxgocyaoln/cia5rherz010dhkjxvym9xjpu/cia5rherz010ehkjxkmv9qd1h/cia5rherz010fhkjxux4kwy9m', + 'http://example.com/cia5rherz010ghkjxgjg51jll/cia5rherz010hhkjxheyloqok/cia5rherz010ihkjxocdihpf0/cia5rherz010jhkjxkjob7g4l', + 'http://example.com/cia5rherz010khkjxyqnrc520/cia5rherz010lhkjxyfmva03e/cia5rherz010mhkjx7pf5a1q1/cia5rherz010nhkjxvcbajwq5', + 'http://example.com/cia5rherz010ohkjxlj1peujy/cia5rherz010phkjxbe0edddw/cia5rherz010qhkjxese0v0d0/cia5rherz010rhkjx3z5sqmvd', + 'http://example.com/cia5rherz010shkjx031cq64v/cia5rherz010thkjxo31twbuq/cia5rherz010uhkjxx0w89wkt/cia5rherz010vhkjxunc21rd5', + 'http://example.com/cia5rherz010whkjxokxwgntu/cia5rherz010xhkjx8zhcummr/cia5rherz010yhkjxrr19wgdd/cia5rherz010zhkjx3krrrmfy', + 'http://example.com/cia5rherz0110hkjxsnag5gy8/cia5rherz0111hkjxti3yt0uo/cia5rherz0112hkjxuvxezwly/cia5rherz0113hkjx42tjs0w1', + 'http://example.com/cia5rherz0114hkjxhap5ggkp/cia5rherz0115hkjxhpvs28ez/cia5rherz0116hkjxchtzr6ub/cia5rherz0117hkjxldrskwe8', + 'http://example.com/cia5rherz0118hkjx3moumoc6/cia5rherz0119hkjx8jmsv1po/cia5rherz011ahkjx5tbk1781/cia5rherz011bhkjxda8axg94', + 'http://example.com/cia5rherz011chkjxpch14tnq/cia5rherz011dhkjx779uxhge/cia5rherz011ehkjxd1gossfl/cia5rherz011fhkjxe6fsxfle', + 'http://example.com/cia5rherz011ghkjxkv0e5x6o/cia5rherz011hhkjxrmm01lop/cia5rherz011ihkjxadtpfth3/cia5rherz011jhkjxmsbqnjtx', + 'http://example.com/cia5rherz011khkjxs9v8q989/cia5rherz011lhkjxbb47ojmz/cia5rherz011mhkjxfqrbtm2s/cia5rherz011nhkjxf0ukz3z7', + 'http://example.com/cia5rherz011ohkjx1ljxj85v/cia5rherz011phkjxzyc52kr1/cia5rherz011qhkjxx03aq7rt/cia5rherz011rhkjxib8yi6wz', + 'http://example.com/cia5rherz011shkjxelvnazea/cia5rherz011thkjx6ge3ekjc/cia5rherz011uhkjxj6spkxml/cia5rherz011vhkjx2g4n6l67', + 'http://example.com/cia5rherz011whkjxoa9tcq7o/cia5rherz011xhkjx0aa7y41v/cia5rherz011yhkjx56c9pqwk/cia5rherz011zhkjx5iy36w89', + 'http://example.com/cia5rherz0120hkjx04d4k3fi/cia5rherz0121hkjxvl6nw4m5/cia5rherz0122hkjxxc1jbk55/cia5rherz0123hkjx0pjnf99r', + 'http://example.com/cia5rherz0124hkjx91z4pze5/cia5rherz0125hkjxg7qc4u38/cia5rherz0126hkjx1yegfvw4/cia5rherz0127hkjxl15ygp9h', + 'http://example.com/cia5rherz0128hkjx4tkyj2om/cia5rherz0129hkjx7h7oqbrp/cia5rherz012ahkjxkajenzcs/cia5rherz012bhkjxi4bowdxs', + 'http://example.com/cia5rherz012chkjx2szlut25/cia5rherz012dhkjxupa098p0/cia5rherz012ehkjxlg93n5ca/cia5rherz012fhkjx3e3lqc5s', + 'http://example.com/cia5rherz012ghkjxiypurty6/cia5rherz012hhkjxnpuba7yn/cia5rherz012ihkjxqajb87r0/cia5rherz012jhkjx3w8tfq58', + 'http://example.com/cia5rherz012khkjx6kpvfmqc/cia5rherz012lhkjxle4ozx4b/cia5rherz012mhkjxcx3zh6l8/cia5rherz012nhkjxwhol1p7z', + 'http://example.com/cia5rherz012ohkjx8hcbuea8/cia5rherz012phkjxjn78pjpu/cia5rherz012qhkjxcso0w7ob/cia5rherz012rhkjxuwsrzjnb', + 'http://example.com/cia5rherz012shkjxmfrs9kl4/cia5rherz012thkjxqd79q3jk/cia5rherz012uhkjx4y92c2l9/cia5rherz012vhkjxah5zn3ql', + 'http://example.com/cia5rherz012whkjxnoco7250/cia5rherz012xhkjx50xbnyn6/cia5rherz012yhkjxhiz0qo7f/cia5rherz012zhkjxpvm1udb0', + 'http://example.com/cia5rherz0130hkjxo3o4fndr/cia5rherz0131hkjxkz4fvrzq/cia5rherz0132hkjxz7tax104/cia5rherz0133hkjx1g0g06dn', + 'http://example.com/cia5rherz0134hkjx5y2031n3/cia5rherz0135hkjx7uqpqo6r/cia5rherz0136hkjxy33y4m0i/cia5rherz0137hkjxi2zwuxom', + 'http://example.com/cia5rherz0138hkjx2r63kcp2/cia5rherz0139hkjxqhukego2/cia5rherz013ahkjxrct5kwo5/cia5rherz013bhkjxlwdsrkdb', + 'http://example.com/cia5rherz013chkjxqb7drtld/cia5rherz013dhkjxhk7nzkp6/cia5rherz013ehkjxt59enx4r/cia5rherz013fhkjxmequgudl', + 'http://example.com/cia5rherz013ghkjxsvtndprv/cia5rherz013hhkjx5qzj9yky/cia5rherz013ihkjx7wi51091/cia5rherz013jhkjx07qd1vho', + 'http://example.com/cia5rherz013khkjxxhuteqrg/cia5rherz013lhkjxnytihmq0/cia5rherz013mhkjxxcuyopb7/cia5rherz013nhkjxz8wjm6kv', + 'http://example.com/cia5rherz013ohkjxti624ceh/cia5rherz013phkjx9es0m8z1/cia5rherz013qhkjx2thq0yiq/cia5rherz013rhkjxcz1h935h', + 'http://example.com/cia5rherz013shkjxy1fcs2p9/cia5rherz013thkjxqj1f3hzd/cia5rherz013uhkjx9n3img9m/cia5rherz013vhkjxbbsd9s7u', + 'http://example.com/cia5rherz013whkjxzrks74yw/cia5rherz013xhkjx19u9gnum/cia5rherz013yhkjxf189dqov/cia5rherz013zhkjxn840ifqp', + 'http://example.com/cia5rherz0140hkjxzflkd8o3/cia5rherz0141hkjxth1k5pcv/cia5rherz0142hkjxk4tx2d6t/cia5rherz0143hkjxua6in4hi', + 'http://example.com/cia5rherz0144hkjxh5223dp4/cia5rherz0145hkjxggdx0inf/cia5rherz0146hkjxukma20rn/cia5rherz0147hkjxbz6yr3vj', + 'http://example.com/cia5rherz0148hkjxj6yz49cp/cia5rherz0149hkjxnshaboc7/cia5rherz014ahkjx9k7w03oz/cia5rherz014bhkjxim3qdl32', + 'http://example.com/cia5rherz014chkjxkwm4bedt/cia5rherz014dhkjxd87owzz9/cia5rherz014ehkjx7gvsq5h8/cia5rherz014fhkjxvg5i2lzo', + 'http://example.com/cia5rhes0014ghkjx57nwx3bd/cia5rhes0014hhkjx7yg5lnmm/cia5rhes0014ihkjxpcj5y5pc/cia5rhes0014jhkjxx7pqjwyi', + 'http://example.com/cia5rhes0014khkjx8je94eu9/cia5rhes0014lhkjxj7s0ayqj/cia5rhes0014mhkjxaj8frq8f/cia5rhes0014nhkjxsvfwikzc', + 'http://example.com/cia5rhes0014ohkjx7w5zhsfa/cia5rhes0014phkjxb8znpn93/cia5rhes0014qhkjx26dojt2q/cia5rhes0014rhkjx4z51j3v1', + 'http://example.com/cia5rhes0014shkjxsrfqfh66/cia5rhes0014thkjxchkutc4l/cia5rhes0014uhkjx61hu197u/cia5rhes0014vhkjx88tbe055', + 'http://example.com/cia5rhes0014whkjxnsy6o8oh/cia5rhes0014xhkjxhf7qa11c/cia5rhes0014yhkjx8dg54bhs/cia5rhes0014zhkjxwddjjbfx', + 'http://example.com/cia5rhes00150hkjxvbx9bs0t/cia5rhes00151hkjx1ndja821/cia5rhes00152hkjxgre5jaft/cia5rhes00153hkjx403j16ab', + 'http://example.com/cia5rhes00154hkjx850qetqc/cia5rhes00155hkjx25fuxyq1/cia5rhes00156hkjxt0otyqf9/cia5rhes00157hkjxkrckit2g', + 'http://example.com/cia5rhes00158hkjxeka610dd/cia5rhes00159hkjxirohiw4g/cia5rhes0015ahkjx4a2e7hwj/cia5rhes0015bhkjx54ew959x', + 'http://example.com/cia5rhes0015chkjxdbymvixv/cia5rhes0015dhkjxehyc0l2p/cia5rhes0015ehkjxkkzsw7sr/cia5rhes0015fhkjx694x9jdr', + 'http://example.com/cia5rhes0015ghkjx8524c513/cia5rhes0015hhkjx9gbh0axg/cia5rhes0015ihkjxhux4m9va/cia5rhes0015jhkjxizfpn19a', + 'http://example.com/cia5rhes0015khkjxmy1viucg/cia5rhes0015lhkjx4k9cpi1x/cia5rhes0015mhkjxlwccit5i/cia5rhes0015nhkjxyyerl1x5', + 'http://example.com/cia5rhes0015ohkjxse6u4cq1/cia5rhes0015phkjxmbosv5k1/cia5rhes0015qhkjxagd18q9e/cia5rhes0015rhkjx18k37mza', + 'http://example.com/cia5rhes0015shkjxkf88qpr2/cia5rhes0015thkjxxjngloiv/cia5rhes0015uhkjxw3p51ph3/cia5rhes0015vhkjxuv117q6n', + 'http://example.com/cia5rhes0015whkjxepiuli5w/cia5rhes0015xhkjx6912ozju/cia5rhes0015yhkjxs50s4iw9/cia5rhes0015zhkjx4fqv3fj5', + 'http://example.com/cia5rhes00160hkjxaezek04y/cia5rhes00161hkjxo7jexmqa/cia5rhes00162hkjx8qt4t84r/cia5rhes00163hkjx0x35v1ea', + 'http://example.com/cia5rhes00164hkjxum5cztru/cia5rhes00165hkjxykw801f6/cia5rhes00166hkjx87cgbtl9/cia5rhes00167hkjxr5laneo4', + 'http://example.com/cia5rhes00168hkjx4675xx8q/cia5rhes00169hkjxa69bs98w/cia5rhes0016ahkjxgxbg1ktg/cia5rhes0016bhkjxcssrfeb6', + 'http://example.com/cia5rhes0016chkjxskrmxbeu/cia5rhes0016dhkjxchuf9w7d/cia5rhes0016ehkjx96tmup0w/cia5rhes0016fhkjx3b2ir9k8', + 'http://example.com/cia5rhes0016ghkjxshn9r5cd/cia5rhes0016hhkjxt0okuboo/cia5rhes0016ihkjx3xc5n1z7/cia5rhes0016jhkjxmm1rhinv', + 'http://example.com/cia5rhes0016khkjxs4md552m/cia5rhes0016lhkjxdqs7jlks/cia5rhes0016mhkjxsbqpbr27/cia5rhes0016nhkjxsw0eoqxh', + 'http://example.com/cia5rhes0016ohkjxj1uyawl4/cia5rhes0016phkjx4s3keqvp/cia5rhes0016qhkjxlr2ujty3/cia5rhes0016rhkjxshluxgzs', + 'http://example.com/cia5rhes0016shkjxmjm47a2n/cia5rhes0016thkjxvawl3vod/cia5rhes0016uhkjxlkwcmxrn/cia5rhes0016vhkjx0yrlq0k0', + 'http://example.com/cia5rhes0016whkjx47wacy0d/cia5rhes0016xhkjxx20x2adr/cia5rhes0016yhkjxpyafhbax/cia5rhes0016zhkjxkm7homqb', + 'http://example.com/cia5rhes00170hkjx58dppj32/cia5rhes00171hkjxgl9ekfiu/cia5rhes00172hkjx35hn4ajh/cia5rhes00173hkjx4k5x795i', + 'http://example.com/cia5rhes00174hkjx431k7e7c/cia5rhes00175hkjxdfvrfwqy/cia5rhes00176hkjx2wqmhu6x/cia5rhes00177hkjxd8ykmqj9', + 'http://example.com/cia5rhes00178hkjxpbwz0dv1/cia5rhes00179hkjxfxnx8xde/cia5rhes0017ahkjxnumozh8d/cia5rhes0017bhkjxjvgv7bu3', + 'http://example.com/cia5rhes0017chkjx6cbtaca4/cia5rhes0017dhkjxl6oa2n77/cia5rhes0017ehkjxv4e7c73p/cia5rhes0017fhkjxarapkb0w', + 'http://example.com/cia5rhes0017ghkjxvzviznq4/cia5rhes0017hhkjxkxkq2w0r/cia5rhes0017ihkjxfdhga9qz/cia5rhes0017jhkjxzr0zbpli', + 'http://example.com/cia5rhes0017khkjxd6x7dl0m/cia5rhes0017lhkjxpa472u8x/cia5rhes0017mhkjxi7scj2zd/cia5rhes0017nhkjxcrar3doc', + 'http://example.com/cia5rhes1017ohkjxim1b2tgs/cia5rhes1017phkjxido7zpq3/cia5rhes1017qhkjxzhgszmuh/cia5rhes1017rhkjxh9n4vlu9', + 'http://example.com/cia5rhes1017shkjxazazffwt/cia5rhes1017thkjxt1mu7dkg/cia5rhes1017uhkjx79p8vex3/cia5rhes1017vhkjxzk3rwfaj', + 'http://example.com/cia5rhes1017whkjxg9ldz44h/cia5rhes1017xhkjxubjc7d35/cia5rhes1017yhkjxyn9t58r7/cia5rhes1017zhkjx822o28jf', + 'http://example.com/cia5rhes10180hkjxy0zeitqi/cia5rhes10181hkjxuiumypud/cia5rhes10182hkjxqhf3xprn/cia5rhes10183hkjx9orcdf2t', + 'http://example.com/cia5rhes10184hkjx60vpjosn/cia5rhes10185hkjxiuxdbrjp/cia5rhes10186hkjxjazso4v3/cia5rhes10187hkjx1yda3p8i', + 'http://example.com/cia5rhes10188hkjx67qn30yn/cia5rhes10189hkjxd8e62yud/cia5rhes1018ahkjxr1ogihzw/cia5rhes1018bhkjxqa83yxs2', + 'http://example.com/cia5rhes1018chkjxcijm6ol2/cia5rhes1018dhkjxn27lkryl/cia5rhes1018ehkjxin74swtd/cia5rhes1018fhkjxc3n9hjub', + 'http://example.com/cia5rhes1018ghkjxs06i4n1v/cia5rhes1018hhkjxtbrrprdd/cia5rhes1018ihkjxh375u2d5/cia5rhes1018jhkjxwe4m1w3k', + 'http://example.com/cia5rhes1018khkjxc2fo3tyn/cia5rhes1018lhkjx11wqgr3o/cia5rhes1018mhkjx55cz73km/cia5rhes1018nhkjx027g05rh', + 'http://example.com/cia5rhes1018ohkjxuxt9w0qg/cia5rhes1018phkjxuppi0zpt/cia5rhes1018qhkjx3hedzfgq/cia5rhes1018rhkjxdbef85sg', + 'http://example.com/cia5rhes1018shkjxh23bn4hl/cia5rhes1018thkjx2jo33xt5/cia5rhes1018uhkjxgrf6z3q5/cia5rhes1018vhkjxs51u2bsq', + 'http://example.com/cia5rhes1018whkjxh98q42o4/cia5rhes1018xhkjxtktqtwob/cia5rhes1018yhkjxxf9qq7me/cia5rhes1018zhkjx540am2xr', + 'http://example.com/cia5rhes10190hkjxcom1s1af/cia5rhes10191hkjxr15i8zfz/cia5rhes10192hkjxbsyx6pqa/cia5rhes10193hkjx5lfk3tnz', + 'http://example.com/cia5rhes10194hkjx63khbmh5/cia5rhes10195hkjx23tzm25c/cia5rhes10196hkjx3tu55kps/cia5rhes10197hkjxt9kgwuye', + 'http://example.com/cia5rhes10198hkjxvsic8zyi/cia5rhes10199hkjxiqcxj6ha/cia5rhes1019ahkjxul53ymxv/cia5rhes1019bhkjx8j5i4gjo', + 'http://example.com/cia5rhes1019chkjxhkzab45h/cia5rhes1019dhkjxp9m537kv/cia5rhes1019ehkjxflgayfwl/cia5rhes1019fhkjxpxcfuwm7', + 'http://example.com/cia5rhes1019ghkjx0ec3mbfs/cia5rhes1019hhkjxpzum6b24/cia5rhes1019ihkjx8l7ygjw5/cia5rhes1019jhkjxc4kywxia', + 'http://example.com/cia5rhes1019khkjxcgdm2x8i/cia5rhes1019lhkjxsd4z7axk/cia5rhes1019mhkjxrivl4h0v/cia5rhes1019nhkjxwq5r7rjw', + 'http://example.com/cia5rhes1019ohkjxlbgrt2qs/cia5rhes1019phkjxrqx7xr97/cia5rhes1019qhkjxqxuaxnbc/cia5rhes1019rhkjx479nva7e', + 'http://example.com/cia5rhes1019shkjxo903skww/cia5rhes1019thkjx835fib01/cia5rhes1019uhkjxqnb5hb1c/cia5rhes1019vhkjx985hdr8a', + 'http://example.com/cia5rhes1019whkjxul29xzs7/cia5rhes1019xhkjx8v769rft/cia5rhes1019yhkjx8moz4avh/cia5rhes1019zhkjxltk1bmj1', + 'http://example.com/cia5rhes101a0hkjxgj1bgqcu/cia5rhes101a1hkjxam87hyv6/cia5rhes101a2hkjx6n7xfzcf/cia5rhes101a3hkjxwhuzx4bu', + 'http://example.com/cia5rhes101a4hkjxkz4gt4bb/cia5rhes101a5hkjx8jto6sbw/cia5rhes101a6hkjxdkz6q053/cia5rhes101a7hkjxafsa477k', + 'http://example.com/cia5rhes101a8hkjxmawq81f9/cia5rhes101a9hkjx7a1m25vl/cia5rhes101aahkjxn9vib2k1/cia5rhes101abhkjxnd9lr35m', + 'http://example.com/cia5rhes101achkjxaz60ife8/cia5rhes101adhkjxb9jyduwc/cia5rhes101aehkjximmqxxdc/cia5rhes101afhkjxrivbhs77', + 'http://example.com/cia5rhes101aghkjxrq2da1pd/cia5rhes101ahhkjxjcuopb44/cia5rhes101aihkjxmeqnm90k/cia5rhes101ajhkjxli2mp598', + 'http://example.com/cia5rhes101akhkjx3uzisjok/cia5rhes101alhkjx2z2rnozw/cia5rhes101amhkjxektigddg/cia5rhes101anhkjxvsxrqsn5', + 'http://example.com/cia5rhes101aohkjxfb78h44w/cia5rhes101aphkjx2g9hr8n2/cia5rhes101aqhkjx61plt1q1/cia5rhes101arhkjxajstaro6', + 'http://example.com/cia5rhes101ashkjxm5zox05g/cia5rhes101athkjxqnofipd6/cia5rhes101auhkjxr39j3scv/cia5rhes101avhkjxcnv43592', + 'http://example.com/cia5rhes101awhkjxi5amucip/cia5rhes101axhkjxy05rb4by/cia5rhes101ayhkjxoqbug0w2/cia5rhes101azhkjxobqv30io', + 'http://example.com/cia5rhes101b0hkjxgtxrq6a1/cia5rhes101b1hkjx3kckskk9/cia5rhes101b2hkjxgp3x3n2k/cia5rhes101b3hkjxjoa91opd', + 'http://example.com/cia5rhes101b4hkjxl0uryndo/cia5rhes101b5hkjxn8o6oumu/cia5rhes101b6hkjxrjbze70s/cia5rhes101b7hkjxv5mrjv5y', + 'http://example.com/cia5rhes101b8hkjx7yeg3s3m/cia5rhes101b9hkjxnchy3mil/cia5rhes101bahkjxawomopeo/cia5rhes101bbhkjx9oml99jy', + 'http://example.com/cia5rhes101bchkjxzaccplvr/cia5rhes101bdhkjxmv4u1l8n/cia5rhes101behkjxja90rgy0/cia5rhes101bfhkjxolfzxocw', + 'http://example.com/cia5rhes101bghkjxy1vfbaaw/cia5rhes101bhhkjxg6xznpan/cia5rhes101bihkjxlg9fzku8/cia5rhes101bjhkjxnh2hjnui', + 'http://example.com/cia5rhes101bkhkjxsclo2zp3/cia5rhes101blhkjxuvrcudv5/cia5rhes101bmhkjx605j2zjj/cia5rhes101bnhkjx2xml0fvu', + 'http://example.com/cia5rhes101bohkjx5gf3ijos/cia5rhes101bphkjxpe0su46e/cia5rhes101bqhkjxs22f7ad2/cia5rhes101brhkjx9agg7eo1', + 'http://example.com/cia5rhes101bshkjx3sn7g8yy/cia5rhes101bthkjxe04n3g8b/cia5rhes101buhkjxgy5w6ts0/cia5rhes101bvhkjx7q91193i', + 'http://example.com/cia5rhes101bwhkjxfgzjxdtg/cia5rhes101bxhkjx9fof34tp/cia5rhes101byhkjxoyqeyb8o/cia5rhes101bzhkjxs4h8rhgv', + 'http://example.com/cia5rhes101c0hkjxxj1zh5us/cia5rhes101c1hkjxoc2z40bk/cia5rhes101c2hkjxp2mh4dck/cia5rhes101c3hkjx11dpdt65', + 'http://example.com/cia5rhes101c4hkjxrfzebrql/cia5rhes101c5hkjxhgaz06ty/cia5rhes101c6hkjxrs79e85g/cia5rhes101c7hkjxggrhbqyx', + 'http://example.com/cia5rhes101c8hkjxvgxdgnbw/cia5rhes101c9hkjxyjttv60a/cia5rhes101cahkjxsq6fq2jl/cia5rhes101cbhkjx7q41av4q', + 'http://example.com/cia5rhes101cchkjxovipv8ev/cia5rhes101cdhkjxqmj2adv7/cia5rhes101cehkjxkac54kr0/cia5rhes101cfhkjxuqcmlixm', + 'http://example.com/cia5rhes101cghkjx8p3hy50r/cia5rhes101chhkjxitheakp9/cia5rhes101cihkjxrm1z2bcp/cia5rhes101cjhkjx6e60mdcr', + 'http://example.com/cia5rhes101ckhkjxavxp0z0w/cia5rhes101clhkjxo78s8ce3/cia5rhes101cmhkjx3q5plsy4/cia5rhes101cnhkjxbr2dyljs', + 'http://example.com/cia5rhes101cohkjxx6uzzh6z/cia5rhes101cphkjx39t0gmdt/cia5rhes101cqhkjx5cpi5gv2/cia5rhes101crhkjx8tiw2khg', + 'http://example.com/cia5rhes101cshkjxx26p2oew/cia5rhes101cthkjxh5x6cctw/cia5rhes101cuhkjxmqbok5qb/cia5rhes101cvhkjx98q4u6vg', + 'http://example.com/cia5rhes101cwhkjxca46qqdc/cia5rhes101cxhkjxkya9jblw/cia5rhes101cyhkjxsx55uj72/cia5rhes101czhkjx4px01ypv', + 'http://example.com/cia5rhes201d0hkjxrfq1bxuy/cia5rhes201d1hkjxum4pm3s6/cia5rhes201d2hkjx9djj6tvc/cia5rhes201d3hkjxkobt5p5a', + 'http://example.com/cia5rhes201d4hkjx6vbuy1h3/cia5rhes201d5hkjxtyrtq6sn/cia5rhes201d6hkjxyn0dbgeq/cia5rhes201d7hkjx9g1d2pu0', + 'http://example.com/cia5rhes201d8hkjxsci6f24w/cia5rhes201d9hkjxd8q6ugbk/cia5rhes201dahkjx8c8yunrs/cia5rhes201dbhkjxb657b9hh', + 'http://example.com/cia5rhes201dchkjxhpytp0es/cia5rhes201ddhkjxzz6or9dl/cia5rhes201dehkjxvt1iaj4e/cia5rhes201dfhkjxcovukh36', + 'http://example.com/cia5rhes201dghkjxs4wcuyr5/cia5rhes201dhhkjxa3ltvy94/cia5rhes201dihkjx1q3i72ys/cia5rhes201djhkjxjgthq1xi', + 'http://example.com/cia5rhes201dkhkjxsvsw8r7g/cia5rhes201dlhkjxho9dzz7z/cia5rhes201dmhkjxtd6y9lt9/cia5rhes201dnhkjx2mryfja5', + 'http://example.com/cia5rhes201dohkjx1qpsam6z/cia5rhes201dphkjxyqckmdus/cia5rhes201dqhkjx05x0cua4/cia5rhes201drhkjxlv48ezca', + 'http://example.com/cia5rhes201dshkjxv3tvrlnv/cia5rhes201dthkjxsb1vp68a/cia5rhes201duhkjxr3dpwbsl/cia5rhes201dvhkjxooy13asr', + 'http://example.com/cia5rhes201dwhkjxy2yxmf1a/cia5rhes201dxhkjxg7ddbk62/cia5rhes201dyhkjxyfw66d9i/cia5rhes201dzhkjxhiriqvpp', + 'http://example.com/cia5rhes201e0hkjxgnojdvfu/cia5rhes201e1hkjx35d46pkf/cia5rhes201e2hkjx8al6xyxc/cia5rhes201e3hkjxpm8o33n5', + 'http://example.com/cia5rhes201e4hkjxgmt6q22i/cia5rhes201e5hkjxcxujptph/cia5rhes201e6hkjxvjqvqv5y/cia5rhes201e7hkjx7frk9v00', + 'http://example.com/cia5rhes201e8hkjxwksc2h6k/cia5rhes201e9hkjxmrv2nebe/cia5rhes201eahkjxju4ycxem/cia5rhes201ebhkjxu63x5ai0', + 'http://example.com/cia5rhes201echkjxq4et8qb3/cia5rhes201edhkjxmawlqvb6/cia5rhes201eehkjx5mvbc5jf/cia5rhes201efhkjxf81g9ft0', + 'http://example.com/cia5rhes201eghkjxwc2n8rrz/cia5rhes201ehhkjx96jrb9qp/cia5rhes201eihkjxolmvqk0b/cia5rhes201ejhkjx8t4yxqdy', + 'http://example.com/cia5rhes201ekhkjxjj375p8m/cia5rhes201elhkjxd7n988u0/cia5rhes201emhkjx4sgv75jt/cia5rhes201enhkjx89v2rpwd', + 'http://example.com/cia5rhes201eohkjx441c02sl/cia5rhes201ephkjxicff9k4p/cia5rhes201eqhkjx5c7sjm4x/cia5rhes201erhkjxvl0a13y1', + 'http://example.com/cia5rhes201eshkjxf8inxrty/cia5rhes201ethkjxqjixrhe3/cia5rhes201euhkjx6cq543as/cia5rhes201evhkjxiq4rvbm6', + 'http://example.com/cia5rhes201ewhkjxpzr481o0/cia5rhes201exhkjxfqo3ya1u/cia5rhes201eyhkjxzaieceuz/cia5rhes201ezhkjxrp9aiyto', + 'http://example.com/cia5rhes201f0hkjxdxg04ktt/cia5rhes201f1hkjxc5xqh8w6/cia5rhes201f2hkjxxqt7mk69/cia5rhes201f3hkjxhz4mt35k', + 'http://example.com/cia5rhes201f4hkjxwif8ix73/cia5rhes201f5hkjxcnk41o2f/cia5rhes201f6hkjxqvmwkmte/cia5rhes201f7hkjxjhf0rwkd', + 'http://example.com/cia5rhes201f8hkjxgu0mbayd/cia5rhes201f9hkjxoirm2pi6/cia5rhes201fahkjx3eggxv1v/cia5rhes201fbhkjx3dr0v5lr', + 'http://example.com/cia5rhes201fchkjx9bl00653/cia5rhes201fdhkjxd986f7dy/cia5rhes201fehkjxcjhtezko/cia5rhes201ffhkjx2g6pp08r', + 'http://example.com/cia5rhes201fghkjxieabfnjf/cia5rhes201fhhkjxhncmkptc/cia5rhes201fihkjxd2idn405/cia5rhes201fjhkjxxh12k6dz', + 'http://example.com/cia5rhes201fkhkjxjhb8dl6c/cia5rhes201flhkjxjesmmxj5/cia5rhes201fmhkjxgdq4watu/cia5rhes201fnhkjxcx2v7046', + 'http://example.com/cia5rhes201fohkjxooyrgbd6/cia5rhes201fphkjxnswgkqhg/cia5rhes201fqhkjxr1olqtyi/cia5rhes201frhkjxpylkppc7', + 'http://example.com/cia5rhes201fshkjxss3f3m7a/cia5rhes201fthkjxtb752b31/cia5rhes201fuhkjxl8v5tked/cia5rhes201fvhkjxs83n3lna', + 'http://example.com/cia5rhes201fwhkjx7mn2ufyp/cia5rhes201fxhkjxykgvds9s/cia5rhes201fyhkjxzj880aau/cia5rhes201fzhkjx9hmzn5w1', + 'http://example.com/cia5rhes201g0hkjx40m23pfq/cia5rhes201g1hkjxu5axzq44/cia5rhes201g2hkjxtenwlezp/cia5rhes201g3hkjxeaanxtc3', + 'http://example.com/cia5rhes201g4hkjxfko2j17a/cia5rhes201g5hkjxdngk92iq/cia5rhes201g6hkjxyiixm3h3/cia5rhes201g7hkjx1e0o9rbr', + 'http://example.com/cia5rhes201g8hkjxzvuuf9mr/cia5rhes201g9hkjx9i4067eb/cia5rhes201gahkjxe0877b7o/cia5rhes201gbhkjxhjeqydx3', + 'http://example.com/cia5rhes201gchkjx28buxxph/cia5rhes201gdhkjx11mlvzu6/cia5rhes201gehkjx56z31f3p/cia5rhes201gfhkjxon8mxyaq', + 'http://example.com/cia5rhes201gghkjxzhavbsbu/cia5rhes201ghhkjxpalfbbgq/cia5rhes201gihkjxmg14pb0i/cia5rhes201gjhkjxz1k6lfox', + 'http://example.com/cia5rhes201gkhkjxr91y8n1x/cia5rhes201glhkjxcd8gf56b/cia5rhes201gmhkjxgmgi5aag/cia5rhes201gnhkjxzuskm0u3', + 'http://example.com/cia5rhes201gohkjxh6stmdzj/cia5rhes201gphkjxjhxmrc1z/cia5rhes201gqhkjxbsb6x26m/cia5rhes201grhkjx2qjq5azu', + 'http://example.com/cia5rhes201gshkjxwgykuiuh/cia5rhes201gthkjxshezzoh7/cia5rhes201guhkjxhxk5wn0c/cia5rhes201gvhkjxfgd3dy5o', + 'http://example.com/cia5rhes201gwhkjxrjdm59mt/cia5rhes201gxhkjx5p1au9tm/cia5rhes201gyhkjx2fhr9h8l/cia5rhes201gzhkjx1d6ey84l', + 'http://example.com/cia5rhes201h0hkjxw539qclb/cia5rhes201h1hkjxtasbgd4k/cia5rhes201h2hkjxnggs4jvi/cia5rhes201h3hkjx10i4oa01', + 'http://example.com/cia5rhes201h4hkjxc4yc7ah2/cia5rhes201h5hkjxpp8h6vjy/cia5rhes201h6hkjx3is219tv/cia5rhes201h7hkjxi5vczfdr', + 'http://example.com/cia5rhes201h8hkjx0pnfnjv8/cia5rhes201h9hkjxvab7mw12/cia5rhes201hahkjxpdkx31mo/cia5rhes201hbhkjx5qrrpii9', + 'http://example.com/cia5rhes201hchkjxxpstoh0r/cia5rhes201hdhkjxd3fqr26w/cia5rhes201hehkjxa89a8p00/cia5rhes201hfhkjxjb7dx816', + 'http://example.com/cia5rhes201hghkjxoundwtvv/cia5rhes201hhhkjxq0l8n544/cia5rhes201hihkjxfnxig9tq/cia5rhes201hjhkjxmthbhba3', + 'http://example.com/cia5rhes201hkhkjxh8del6ix/cia5rhes201hlhkjxbbkqryiz/cia5rhes201hmhkjxsrbt8rwc/cia5rhes201hnhkjxvjinr83g', + 'http://example.com/cia5rhes201hohkjxnm39jamh/cia5rhes201hphkjxbpdbg85s/cia5rhes201hqhkjxt4xsrvvw/cia5rhes201hrhkjx28uncmqm', + 'http://example.com/cia5rhes201hshkjx9y3havo3/cia5rhes201hthkjxsl3xf65k/cia5rhes201huhkjxevlc6mpu/cia5rhes201hvhkjxios9hjnc', + 'http://example.com/cia5rhes201hwhkjx1gqoupdx/cia5rhes201hxhkjxoezqmdn4/cia5rhes201hyhkjxsd34q556/cia5rhes201hzhkjxvkqinyu3', + 'http://example.com/cia5rhes201i0hkjxtuqp1qgj/cia5rhes201i1hkjxjq0bui86/cia5rhes201i2hkjxlu4behua/cia5rhes201i3hkjxbarxd26f', + 'http://example.com/cia5rhes201i4hkjx1dgyo81l/cia5rhes201i5hkjx3xb9oqcc/cia5rhes201i6hkjxgiyz2tkh/cia5rhes201i7hkjx6w3cspdt', + 'http://example.com/cia5rhes201i8hkjxgazao8qk/cia5rhes201i9hkjx9g0kulps/cia5rhes201iahkjxo3xkc8pd/cia5rhes201ibhkjx1dqefi47', + 'http://example.com/cia5rhes201ichkjxg5gkkixu/cia5rhes201idhkjxy4ocvh6v/cia5rhes201iehkjx4cyin399/cia5rhes201ifhkjxx2gjdbml', + 'http://example.com/cia5rhes201ighkjxhg0kk0p1/cia5rhes201ihhkjxt9erqxzy/cia5rhes201iihkjxorqialmn/cia5rhes201ijhkjxuj6s809s', + 'http://example.com/cia5rhes201ikhkjx0vne1gub/cia5rhes201ilhkjxvumlvx2e/cia5rhes201imhkjxkwp8knsu/cia5rhes201inhkjxi7n4t5yd', + 'http://example.com/cia5rhes201iohkjxzho5l61h/cia5rhes201iphkjxxe8fo8zr/cia5rhes201iqhkjxqnsimx8u/cia5rhes201irhkjxecgdhcvp', + 'http://example.com/cia5rhes201ishkjx2fi6dek5/cia5rhes201ithkjxf3i7k6mm/cia5rhes201iuhkjx0uioh430/cia5rhes201ivhkjxqw1gumyl', + 'http://example.com/cia5rhes201iwhkjxlqpv0zzb/cia5rhes201ixhkjxfxx80lsv/cia5rhes201iyhkjx6f1rt1ik/cia5rhes201izhkjx0pmgbqf9', + 'http://example.com/cia5rhes201j0hkjxi3jo8eqm/cia5rhes201j1hkjx8iahnhoa/cia5rhes201j2hkjxcp1bjuci/cia5rhes201j3hkjxushcyv9h', + 'http://example.com/cia5rhes201j4hkjxmfy3u8bq/cia5rhes201j5hkjxl6j0ozf5/cia5rhes201j6hkjx0jrm1hr3/cia5rhes201j7hkjxkgzgfuc2', + 'http://example.com/cia5rhes201j8hkjx6sbhqirt/cia5rhes201j9hkjx3jm8ttkr/cia5rhes201jahkjx4ww2jkjb/cia5rhes201jbhkjx2vwmj8mw', + 'http://example.com/cia5rhes201jchkjx6s6c38ry/cia5rhes201jdhkjxo5iduoju/cia5rhes201jehkjxl337z10k/cia5rhes201jfhkjxennzj2ed', + 'http://example.com/cia5rhes201jghkjx65xshc5s/cia5rhes201jhhkjxtrvnzhf5/cia5rhes201jihkjxers53yxq/cia5rhes201jjhkjxw8nisucr', + 'http://example.com/cia5rhes301jkhkjx7rpx2kp1/cia5rhes301jlhkjxa3840rux/cia5rhes301jmhkjx1943602l/cia5rhes301jnhkjxft42idno', + 'http://example.com/cia5rhes301johkjxoa2e62n8/cia5rhes301jphkjx8jfoflvc/cia5rhes301jqhkjxjt9rh0u6/cia5rhes301jrhkjxofoa9vq2', + 'http://example.com/cia5rhes301jshkjxnm4wl4ab/cia5rhes301jthkjx89ehf3ty/cia5rhes301juhkjxwubr4oap/cia5rhes301jvhkjxk2e8yz43', + 'http://example.com/cia5rhes301jwhkjx4bo3r583/cia5rhes301jxhkjxfmbottug/cia5rhes301jyhkjx86nc7v6s/cia5rhes301jzhkjx9h9x7167', + 'http://example.com/cia5rhes301k0hkjx0oc98odu/cia5rhes301k1hkjxdjynl4c1/cia5rhes301k2hkjxc471ye9i/cia5rhes301k3hkjxcawvse26', + 'http://example.com/cia5rhes301k4hkjxsqss9ydm/cia5rhes301k5hkjx9e2cz05j/cia5rhes301k6hkjxjb18jpjj/cia5rhes301k7hkjxai3k1edl', + 'http://example.com/cia5rhes301k8hkjxeqdbbtwd/cia5rhes301k9hkjxeliovzfz/cia5rhes301kahkjxt8kvwaw8/cia5rhes301kbhkjx334ytlc2', + 'http://example.com/cia5rhes301kchkjxl1lize37/cia5rhes301kdhkjxczqjsftr/cia5rhes301kehkjxercwjrhh/cia5rhes301kfhkjxdeb3fvgv', + 'http://example.com/cia5rhes301kghkjx0yk4gm2e/cia5rhes301khhkjxt4gdd4ly/cia5rhes301kihkjxegsqzb2u/cia5rhes301kjhkjxp1cug6e2', + 'http://example.com/cia5rhes301kkhkjxyhe6rxl6/cia5rhes301klhkjxqfebfzea/cia5rhes301kmhkjxrz0qs4pq/cia5rhes301knhkjxyskiwz5y', + 'http://example.com/cia5rhes301kohkjx3tpgvarg/cia5rhes301kphkjx19vihidz/cia5rhes301kqhkjxos60mu4k/cia5rhes301krhkjxbvenxr93', + 'http://example.com/cia5rhes301kshkjx9ysyvjir/cia5rhes301kthkjxrk2z4v9t/cia5rhes301kuhkjxitxi78qg/cia5rhes301kvhkjx6m0cf7dl', + 'http://example.com/cia5rhes301kwhkjxt4f6hr4z/cia5rhes301kxhkjxilxbilms/cia5rhes301kyhkjxotkf8aaj/cia5rhes301kzhkjx7czn8fdy', + 'http://example.com/cia5rhes301l0hkjxf9jhzc7i/cia5rhes301l1hkjx1by7b0y3/cia5rhes301l2hkjxoo8obxiq/cia5rhes301l3hkjxvoc40tkj', + 'http://example.com/cia5rhes301l4hkjxgjpxmlpv/cia5rhes301l5hkjx94yuj664/cia5rhes301l6hkjxr8e8y97y/cia5rhes301l7hkjxwznfxlhr', + 'http://example.com/cia5rhes301l8hkjxif8hgss9/cia5rhes301l9hkjxls026lu2/cia5rhes301lahkjx8g221cqp/cia5rhes301lbhkjx5nnfkl1o', + 'http://example.com/cia5rhes301lchkjx55outsg7/cia5rhes301ldhkjxa32ta3im/cia5rhes301lehkjxqzx1v4ag/cia5rhes301lfhkjxzs4h9iq8', + 'http://example.com/cia5rhes301lghkjx9zymf1is/cia5rhes301lhhkjxxi8tt4p0/cia5rhes301lihkjxwsqjsjxe/cia5rhes301ljhkjxa2tfzt6w', + 'http://example.com/cia5rhes301lkhkjxs9t7x1x9/cia5rhes301llhkjxo81fsok6/cia5rhes301lmhkjxgi0i3j3b/cia5rhes301lnhkjx5i1k3l6t', + 'http://example.com/cia5rhes301lohkjxj1t98ds7/cia5rhes301lphkjxdee7ecco/cia5rhes301lqhkjxgfslix18/cia5rhes301lrhkjx4w0teefo', + 'http://example.com/cia5rhes301lshkjxghppkl49/cia5rhes301lthkjx7b8lqwg7/cia5rhes301luhkjx9a10fkrm/cia5rhes301lvhkjxbbotm5de', + 'http://example.com/cia5rhes301lwhkjxmjdqwoun/cia5rhes301lxhkjxqscqyygp/cia5rhes301lyhkjx9v1twpxt/cia5rhes301lzhkjxedovrwz9', + 'http://example.com/cia5rhes301m0hkjxa79l057t/cia5rhes301m1hkjxi4lf4pam/cia5rhes301m2hkjxbc54aj2i/cia5rhes301m3hkjxh0uiocv9', + 'http://example.com/cia5rhes301m4hkjxehur1yoh/cia5rhes301m5hkjxa360j1dg/cia5rhes301m6hkjxxu566hbq/cia5rhes301m7hkjxx3ynzmno', + 'http://example.com/cia5rhes301m8hkjxboi8g565/cia5rhes301m9hkjxcw8d20dp/cia5rhes301mahkjxgep6vvnb/cia5rhes301mbhkjxaig0kixq', + 'http://example.com/cia5rhes301mchkjxrjfsemox/cia5rhes301mdhkjxwifr2cdy/cia5rhes301mehkjx0mfczty9/cia5rhes301mfhkjxw0d7du38', + 'http://example.com/cia5rhes301mghkjxma95b0fw/cia5rhes301mhhkjxe08g59uf/cia5rhes301mihkjx6uflwnsd/cia5rhes301mjhkjxrwodr62q', + 'http://example.com/cia5rhes301mkhkjx6rb2vspf/cia5rhes301mlhkjx95l13vr9/cia5rhes301mmhkjx8nf0whp6/cia5rhes301mnhkjxpa5k4qfz', + 'http://example.com/cia5rhes301mohkjx3lbw4jvc/cia5rhes301mphkjx80vx4999/cia5rhes301mqhkjxyyl6bvqt/cia5rhes301mrhkjxq4xrjjqk', + 'http://example.com/cia5rhes301mshkjx8bf0phng/cia5rhes301mthkjxorxsclwf/cia5rhes301muhkjxi92z3o3d/cia5rhes301mvhkjxc3pvc5j6', + 'http://example.com/cia5rhes301mwhkjxm1nnxnhb/cia5rhes301mxhkjxtkkdy3i1/cia5rhes301myhkjxcrbfcl0b/cia5rhes301mzhkjxq6p026u3', + 'http://example.com/cia5rhes301n0hkjx1c2gv11c/cia5rhes301n1hkjxxfi36cpe/cia5rhes301n2hkjx38o7jvti/cia5rhes301n3hkjx9x9p0qh6', + 'http://example.com/cia5rhes301n4hkjx04xmiymb/cia5rhes301n5hkjx1bimz4eh/cia5rhes301n6hkjxnjqswkw6/cia5rhes301n7hkjxx11qd98z', + 'http://example.com/cia5rhes301n8hkjxdgkuvg3u/cia5rhes301n9hkjx9408l8sy/cia5rhes301nahkjxbdy48hsf/cia5rhes301nbhkjx9gbl5y30', + 'http://example.com/cia5rhes301nchkjx4rj2l6gk/cia5rhes301ndhkjxw603iycn/cia5rhes301nehkjxq9p4xm5r/cia5rhes301nfhkjx17hqhk81', + 'http://example.com/cia5rhes301nghkjxm9macc2i/cia5rhes301nhhkjxubpvluxn/cia5rhes301nihkjxtznh1gve/cia5rhes301njhkjxwft7i8zr', + 'http://example.com/cia5rhes301nkhkjxf33xqqss/cia5rhes301nlhkjxt2nsxi7y/cia5rhes301nmhkjxx1k3jzgs/cia5rhes301nnhkjx377meb7x', + 'http://example.com/cia5rhes301nohkjx1iur2w22/cia5rhes301nphkjxrj1q40j2/cia5rhes301nqhkjxpv78uwi7/cia5rhes301nrhkjx74y43ako', + 'http://example.com/cia5rhes301nshkjxgqi4066n/cia5rhes301nthkjxzeax16t1/cia5rhes301nuhkjxkus1cy9e/cia5rhes301nvhkjxrk8s23la', + 'http://example.com/cia5rhes301nwhkjxtz5i4jno/cia5rhes301nxhkjxnve6r7to/cia5rhes301nyhkjxoy3cq981/cia5rhes301nzhkjxsteraq5a', + 'http://example.com/cia5rhes301o0hkjx0bjvkfri/cia5rhes301o1hkjxl3tpcl1b/cia5rhes301o2hkjxih6vk4ck/cia5rhes301o3hkjxh0vrl561', + 'http://example.com/cia5rhes301o4hkjxmqogfrad/cia5rhes301o5hkjx9m8hdpcc/cia5rhes301o6hkjxaluh3wcr/cia5rhes301o7hkjx4m8wommz', + 'http://example.com/cia5rhes301o8hkjxml1xa1az/cia5rhes301o9hkjxx678ystu/cia5rhes301oahkjxndq6nh65/cia5rhes301obhkjxadfjm3wa', + 'http://example.com/cia5rhes301ochkjxwz6f2spm/cia5rhes301odhkjxgvmfaaq8/cia5rhes301oehkjxt17j08ud/cia5rhes301ofhkjxneg64ahh', + 'http://example.com/cia5rhes301oghkjx2odplosg/cia5rhes301ohhkjx6lsxvvhc/cia5rhes301oihkjx1zjlr3lf/cia5rhes301ojhkjx4too7ovk', + 'http://example.com/cia5rhes301okhkjx5ie6svqi/cia5rhes301olhkjx1dvra0d8/cia5rhes301omhkjx01eottp8/cia5rhes301onhkjx0k4cthfm', + 'http://example.com/cia5rhes301oohkjx2uggrotk/cia5rhes301ophkjxo0nc672k/cia5rhes301oqhkjxyxv3yip2/cia5rhes301orhkjx1lzdi04w', + 'http://example.com/cia5rhes301oshkjx239gzsvl/cia5rhes301othkjxegmfaqs4/cia5rhes301ouhkjx3k7u7klw/cia5rhes301ovhkjxx0w3i22n', + 'http://example.com/cia5rhes301owhkjx43szuyvt/cia5rhes301oxhkjxwn8rt15b/cia5rhes301oyhkjxn9plrtrh/cia5rhes301ozhkjx939j8ua7', + 'http://example.com/cia5rhes301p0hkjx933v5a7c/cia5rhes301p1hkjxnptb4syc/cia5rhes301p2hkjxlbdlt4c7/cia5rhes301p3hkjxdnx9ndcb', + 'http://example.com/cia5rhes301p4hkjxgwkvdwyk/cia5rhes301p5hkjxu5l7j6a8/cia5rhes301p6hkjx69gflmy6/cia5rhes301p7hkjxl0ebaafj', + 'http://example.com/cia5rhes301p8hkjxf8355jja/cia5rhes301p9hkjxynm9lc74/cia5rhes301pahkjx9gj8htwg/cia5rhes301pbhkjx9dnwyvr7', + 'http://example.com/cia5rhes401pchkjxvc8103ko/cia5rhes401pdhkjxdvj8k8ys/cia5rhes401pehkjx1yvwz1t3/cia5rhes401pfhkjx9tdmf2pk', + 'http://example.com/cia5rhes401pghkjxm2moyuwg/cia5rhes401phhkjx6sd8pxql/cia5rhes401pihkjx0f56qfr0/cia5rhes401pjhkjxe09zm4ee', + 'http://example.com/cia5rhes401pkhkjxmw6xikqs/cia5rhes401plhkjxtrw32c5n/cia5rhes401pmhkjx2gs5r2uw/cia5rhes401pnhkjx3lnqjuvy', + 'http://example.com/cia5rhes401pohkjx18h593mm/cia5rhes401pphkjx74f4atkm/cia5rhes401pqhkjxl804wbka/cia5rhes401prhkjxvjq32png', + 'http://example.com/cia5rhes401pshkjxq7ig2fmw/cia5rhes401pthkjx94834nui/cia5rhes401puhkjxg5h7u1tk/cia5rhes401pvhkjx83fsa82j', + 'http://example.com/cia5rhes401pwhkjxslfaan9d/cia5rhes401pxhkjx5qqbf367/cia5rhes401pyhkjx9uafkt0z/cia5rhes401pzhkjxyk4qxvdq', + 'http://example.com/cia5rhes401q0hkjxxovjahis/cia5rhes401q1hkjx7811zjvy/cia5rhes401q2hkjx87k6qna2/cia5rhes401q3hkjxoj0w4dpu', + 'http://example.com/cia5rhes401q4hkjxln1jw5x1/cia5rhes401q5hkjxrh7gm7b7/cia5rhes401q6hkjx7r2y10bk/cia5rhes401q7hkjxhqkthpq6', + 'http://example.com/cia5rhes401q8hkjx6u394gyd/cia5rhes401q9hkjxrtrhrat9/cia5rhes401qahkjxdt7xqdcp/cia5rhes401qbhkjxm5ymdwfi', + 'http://example.com/cia5rhes401qchkjx025wiukn/cia5rhes401qdhkjxpovs1w4l/cia5rhes401qehkjxjdc5rv3v/cia5rhes401qfhkjxe3c0v82a', + 'http://example.com/cia5rhes401qghkjxzhl0kyt9/cia5rhes401qhhkjxx91x3w69/cia5rhes401qihkjxsldvc9au/cia5rhes401qjhkjxnag09g7f', + 'http://example.com/cia5rhes401qkhkjxp81l6si9/cia5rhes401qlhkjxdzg4648q/cia5rhes401qmhkjx5rysqo3m/cia5rhes401qnhkjxquhuyu1t', + 'http://example.com/cia5rhes401qohkjxsko3ojrg/cia5rhes401qphkjxvda749pk/cia5rhes401qqhkjxudg42xak/cia5rhes401qrhkjx7edixyt2', + 'http://example.com/cia5rhes401qshkjx9jc7o9ik/cia5rhes401qthkjxvhwfd027/cia5rhes401quhkjx22ja7ygg/cia5rhes401qvhkjxx2yc25pu', + 'http://example.com/cia5rhes401qwhkjxs1yqlawy/cia5rhes401qxhkjxap6eqaza/cia5rhes401qyhkjxhbq9zkww/cia5rhes401qzhkjx5j6rm35k', + 'http://example.com/cia5rhes401r0hkjxk8f23od8/cia5rhes401r1hkjxf2hw9jtn/cia5rhes401r2hkjx0dcvkzlo/cia5rhes401r3hkjxqgiol3kt', + 'http://example.com/cia5rhes401r4hkjxk8rzt66b/cia5rhes401r5hkjx4zc092sq/cia5rhes401r6hkjxdkgh2lu3/cia5rhes401r7hkjxrxdp47yk', + 'http://example.com/cia5rhes401r8hkjxl08yep62/cia5rhes401r9hkjx1xzzdt21/cia5rhes401rahkjxl1d6b0c6/cia5rhes401rbhkjx6zyydco5', + 'http://example.com/cia5rhes401rchkjx76v07kx8/cia5rhes401rdhkjxr6p5yan5/cia5rhes401rehkjxx6g8s1x3/cia5rhes401rfhkjxjwzjn0xv', + 'http://example.com/cia5rhes401rghkjxhiae442a/cia5rhes401rhhkjx28xtp7j6/cia5rhes401rihkjxqsail6xh/cia5rhes401rjhkjxr7kiu6ki', + 'http://example.com/cia5rhes401rkhkjxqne03tot/cia5rhes401rlhkjxjhxcypey/cia5rhes401rmhkjxsma2ekxx/cia5rhes401rnhkjx02z0sp28', + 'http://example.com/cia5rhes401rohkjxnrlforbh/cia5rhes401rphkjxuuk7smlp/cia5rhes401rqhkjxp387ih60/cia5rhes401rrhkjxxn9o7q8q', + 'http://example.com/cia5rhes401rshkjxh4lkmqca/cia5rhes401rthkjx0jm2hnqp/cia5rhes401ruhkjxeo73uqck/cia5rhes401rvhkjxjbn6t4yy', + 'http://example.com/cia5rhes401rwhkjxzepyozzy/cia5rhes401rxhkjx1pykuc1m/cia5rhes401ryhkjxpgqmomw4/cia5rhes401rzhkjx3zbmunev', + 'http://example.com/cia5rhes401s0hkjxjbbqwl70/cia5rhes401s1hkjx9xuj3zqs/cia5rhes401s2hkjxafsii503/cia5rhes401s3hkjxuu216w98', + 'http://example.com/cia5rhes401s4hkjxbbt02xp1/cia5rhes401s5hkjx1wbhsus2/cia5rhes401s6hkjx1ml5tjx2/cia5rhes401s7hkjxmxzwknq3', + 'http://example.com/cia5rhes401s8hkjxjna2smh1/cia5rhes401s9hkjxxqoxe1xs/cia5rhes401sahkjxta8cres1/cia5rhes401sbhkjxlobgkg5k', + 'http://example.com/cia5rhes401schkjx4a93mw54/cia5rhes401sdhkjx11zbb5rf/cia5rhes401sehkjxztk9dbrf/cia5rhes401sfhkjxgh3yzmo1', + 'http://example.com/cia5rhes401sghkjxc28fo8pm/cia5rhes401shhkjx94mcy08n/cia5rhes401sihkjxk9c7sc1a/cia5rhes401sjhkjx2kpauvo7', + 'http://example.com/cia5rhes401skhkjxil6rkwln/cia5rhes401slhkjx7rw9fbmh/cia5rhes401smhkjxp1azo0ra/cia5rhes401snhkjxjn6ske3g', + 'http://example.com/cia5rhes401sohkjxld97hjxq/cia5rhes401sphkjxw88rub86/cia5rhes401sqhkjxdepedlux/cia5rhes401srhkjxtz9wqykr', + 'http://example.com/cia5rhes401sshkjx23nj0gw3/cia5rhes401sthkjxamuty3aa/cia5rhes401suhkjxkzkkksxw/cia5rhes401svhkjxfy7t55xc', + 'http://example.com/cia5rhes401swhkjx2fv1t47w/cia5rhes401sxhkjx493ijzth/cia5rhes401syhkjxlt98ctc5/cia5rhes401szhkjxcgaqy6ks', + 'http://example.com/cia5rhes401t0hkjxibyaz6lz/cia5rhes401t1hkjxajqz3b7v/cia5rhes401t2hkjxutdwnzqk/cia5rhes401t3hkjxqr2zpknp', + 'http://example.com/cia5rhes401t4hkjx2me8lthv/cia5rhes401t5hkjxej9j1ggl/cia5rhes401t6hkjxoxxbptsl/cia5rhes401t7hkjx31lkyc9v', + 'http://example.com/cia5rhes401t8hkjxljaq5d24/cia5rhes401t9hkjxcj9ozsjc/cia5rhes401tahkjx45acwqjh/cia5rhes401tbhkjxsfepsuqn', + 'http://example.com/cia5rhes401tchkjx2d54v2mc/cia5rhes401tdhkjxg995kn83/cia5rhes401tehkjx3sa4rnpk/cia5rhes401tfhkjx9p5zj2fw', + 'http://example.com/cia5rhes401tghkjxetiwdot9/cia5rhes401thhkjxdzft6ee5/cia5rhes401tihkjxc44k574p/cia5rhes401tjhkjxhlaamwjt', + 'http://example.com/cia5rhes401tkhkjxfhcebmkr/cia5rhes401tlhkjx6d2ahwy8/cia5rhes401tmhkjxnvqrt43n/cia5rhes401tnhkjx6y3x0tl6', + 'http://example.com/cia5rhes401tohkjxm76vc3bd/cia5rhes401tphkjxe4toa8ix/cia5rhes401tqhkjx44k31o69/cia5rhes401trhkjx06h29ag1', + 'http://example.com/cia5rhes401tshkjx0en3ww5b/cia5rhes401tthkjxj0sbg5rs/cia5rhes401tuhkjx4mbcemrx/cia5rhes401tvhkjxzah5kckz', + 'http://example.com/cia5rhes401twhkjxbc8vo2b5/cia5rhes401txhkjx1quodrlw/cia5rhes401tyhkjx5q10omzn/cia5rhes401tzhkjxhoknv1pd', + 'http://example.com/cia5rhes401u0hkjxyboul8es/cia5rhes401u1hkjxb2vn5wu5/cia5rhes401u2hkjxat1dog9k/cia5rhes401u3hkjxg9cpxurx', + 'http://example.com/cia5rhes401u4hkjxcy69r1cg/cia5rhes401u5hkjxakh0jykj/cia5rhes401u6hkjxpsaz87je/cia5rhes401u7hkjx3ujs32jl', + 'http://example.com/cia5rhes401u8hkjxogdqi93b/cia5rhes401u9hkjxh0e8e5it/cia5rhes401uahkjxcypc72ho/cia5rhes401ubhkjx07fholph', + 'http://example.com/cia5rhes401uchkjx96q3s4y9/cia5rhes401udhkjxbw6z849k/cia5rhes401uehkjxhbtqh3g4/cia5rhes401ufhkjxbp1hjydk', + 'http://example.com/cia5rhes401ughkjxq6z30rsc/cia5rhes401uhhkjxgnc7011n/cia5rhes401uihkjx00l0t29g/cia5rhes401ujhkjxpdkefo86', + 'http://example.com/cia5rhes401ukhkjx5w5u4uez/cia5rhes401ulhkjxkp60rcm2/cia5rhes401umhkjx2o152chr/cia5rhes401unhkjxj1c837fv', + 'http://example.com/cia5rhes401uohkjxkm3hwgxw/cia5rhes401uphkjxr9fpwgxo/cia5rhes401uqhkjxbju1cc6a/cia5rhes401urhkjxnyjsugye', + 'http://example.com/cia5rhes401ushkjxnl9fzmwd/cia5rhes401uthkjx829ud4hl/cia5rhes401uuhkjxgzo6bd97/cia5rhes401uvhkjxninvqfmi', + 'http://example.com/cia5rhes401uwhkjx23xkeeyb/cia5rhes401uxhkjxr7f81k32/cia5rhes401uyhkjxu8gwxp2s/cia5rhes401uzhkjx0zbojk5h', + 'http://example.com/cia5rhes401v0hkjxnp3m2er4/cia5rhes401v1hkjxh6zxquzd/cia5rhes401v2hkjxcp9r8512/cia5rhes401v3hkjxfj2ziffr', + 'http://example.com/cia5rhes401v4hkjx450sdsy6/cia5rhes401v5hkjxid7nsxhs/cia5rhes401v6hkjx5umhcl29/cia5rhes401v7hkjx8c4ntx9f', + 'http://example.com/cia5rhes401v8hkjxm7493idl/cia5rhes401v9hkjxvp3boxa7/cia5rhes401vahkjxpdhxc0bd/cia5rhes401vbhkjxjq8g7bbv', + 'http://example.com/cia5rhes401vchkjxutbrig4f/cia5rhes401vdhkjxs6v3l5bs/cia5rhes401vehkjxz0s7ot2j/cia5rhes401vfhkjx6e86cpuy', + 'http://example.com/cia5rhes401vghkjxn3ce11di/cia5rhes401vhhkjx0lgmp1co/cia5rhes401vihkjxgjby3l0n/cia5rhes401vjhkjxh7bj2rti', + 'http://example.com/cia5rhes401vkhkjxq9xd82bm/cia5rhes401vlhkjxs2x6daye/cia5rhes401vmhkjxnv72qdm3/cia5rhes401vnhkjxjuu4sj2i', + 'http://example.com/cia5rhes401vohkjxf8wvg4tv/cia5rhes401vphkjxus1ibfvl/cia5rhes401vqhkjxaapjjznh/cia5rhes401vrhkjxhpfk9ana', + 'http://example.com/cia5rhes401vshkjxb314pxv2/cia5rhes401vthkjxcfenzpqi/cia5rhes401vuhkjxvbef4uzt/cia5rhes401vvhkjxcg2mtju1', + 'http://example.com/cia5rhes401vwhkjxobjolxrt/cia5rhes401vxhkjx8n6q1mbj/cia5rhes401vyhkjx1ffiobsm/cia5rhes401vzhkjx845f4yrb', + 'http://example.com/cia5rhes501w0hkjxnkd5nvx4/cia5rhes501w1hkjxd10zyp5f/cia5rhes501w2hkjxed1isr4c/cia5rhes501w3hkjx52w25p6h', + 'http://example.com/cia5rhes501w4hkjxz590qwcl/cia5rhes501w5hkjxirypp5am/cia5rhes501w6hkjx0la9fxfb/cia5rhes501w7hkjxqn8mmj3v', + 'http://example.com/cia5rhes501w8hkjxtvynj745/cia5rhes501w9hkjxicsfn3ft/cia5rhes501wahkjxawuf4y2u/cia5rhes501wbhkjxuioyhj09', + 'http://example.com/cia5rhes501wchkjxpzp5z6gl/cia5rhes501wdhkjxqyu6yfrv/cia5rhes501wehkjxksohpokd/cia5rhes501wfhkjxocde2wt3', + 'http://example.com/cia5rhes501wghkjxocbfchks/cia5rhes501whhkjx0tpiw1nt/cia5rhes501wihkjxslhtkvr0/cia5rhes501wjhkjx3f2wxmki', + 'http://example.com/cia5rhes501wkhkjxpmltynvl/cia5rhes501wlhkjxig3aj85x/cia5rhes501wmhkjxplefjg23/cia5rhes501wnhkjxanfao1fs', + 'http://example.com/cia5rhes501wohkjx9gnuza2e/cia5rhes501wphkjxi89ym1sn/cia5rhes501wqhkjxmpb91ix0/cia5rhes501wrhkjx6vdiefye', + 'http://example.com/cia5rhes501wshkjxxy1dl1w5/cia5rhes501wthkjxs40731ag/cia5rhes501wuhkjx5tu8ptk3/cia5rhes501wvhkjxb83m364e', + 'http://example.com/cia5rhes501wwhkjxyxi7zia4/cia5rhes501wxhkjxfttjkfl1/cia5rhes501wyhkjx73a609nu/cia5rhes501wzhkjxhrqkcsc9', + 'http://example.com/cia5rhes501x0hkjxpzoc18gx/cia5rhes501x1hkjx2evbj8dh/cia5rhes501x2hkjxcmt0dte5/cia5rhes501x3hkjxs2o08cdn', + 'http://example.com/cia5rhes501x4hkjxn8ppwkl6/cia5rhes501x5hkjxve994e14/cia5rhes501x6hkjxp3nroxzg/cia5rhes501x7hkjxdzh6iphg', + 'http://example.com/cia5rhes501x8hkjx3dxj6rdf/cia5rhes501x9hkjx0uek477t/cia5rhes501xahkjxyomgqdjw/cia5rhes501xbhkjx0adcgz3e', + 'http://example.com/cia5rhes501xchkjxjr38fuho/cia5rhes501xdhkjxi9h8gxgv/cia5rhes501xehkjx9lnq5x48/cia5rhes501xfhkjx6x7q34qn', + 'http://example.com/cia5rhes501xghkjx7kdv4j16/cia5rhes501xhhkjxzh3h1621/cia5rhes501xihkjx2tll48zr/cia5rhes501xjhkjx2mgqrjx7', + 'http://example.com/cia5rhes501xkhkjxb38ktam2/cia5rhes501xlhkjxqe4kly98/cia5rhes501xmhkjxs8kc7y4g/cia5rhes501xnhkjxon8hd6t9', + 'http://example.com/cia5rhes501xohkjxvh07sqak/cia5rhes501xphkjxa8cjku7k/cia5rhes501xqhkjx7czbmtzz/cia5rhes501xrhkjx2v5gm68q', + 'http://example.com/cia5rhes501xshkjxhafeuujz/cia5rhes501xthkjx83z1ik2e/cia5rhes501xuhkjxfwfbdp20/cia5rhes501xvhkjxat92izys', + 'http://example.com/cia5rhes501xwhkjxmxkavbl1/cia5rhes501xxhkjxjmwfaudp/cia5rhes501xyhkjxjb6y5ckv/cia5rhes501xzhkjxx1qzw43n', + 'http://example.com/cia5rhes501y0hkjxjqq91tnx/cia5rhes501y1hkjx5fqvt95y/cia5rhes501y2hkjx213i79od/cia5rhes501y3hkjx2bhrwh3c', + 'http://example.com/cia5rhes501y4hkjxpg0w8tm1/cia5rhes501y5hkjx4rfqmukn/cia5rhes501y6hkjxdmm6zlwo/cia5rhes501y7hkjxuuszpo6e', + 'http://example.com/cia5rhes501y8hkjx1dijvw0o/cia5rhes501y9hkjxh1co3ai2/cia5rhes501yahkjxfcerdd6h/cia5rhes501ybhkjx52yrnztr', + 'http://example.com/cia5rhes501ychkjxmuxdm6gn/cia5rhes501ydhkjxk9cu7gzp/cia5rhes501yehkjxt9czxhe8/cia5rhes501yfhkjxf1hpxe7k', + 'http://example.com/cia5rhes501yghkjxzyfsx9ee/cia5rhes501yhhkjxoobntt4j/cia5rhes501yihkjxbv4l41i4/cia5rhes501yjhkjx9cg8i6yq', + 'http://example.com/cia5rhes501ykhkjxb8micj52/cia5rhes501ylhkjxi810y8kg/cia5rhes501ymhkjx35pqd2dp/cia5rhes501ynhkjx411ay6w2', + 'http://example.com/cia5rhes501yohkjxp230m8o4/cia5rhes501yphkjx85aei3f0/cia5rhes501yqhkjx39awmvdg/cia5rhes501yrhkjxabhea8z7', + 'http://example.com/cia5rhes501yshkjxmy4w0zr0/cia5rhes501ythkjxxxtlmezs/cia5rhes501yuhkjx8mwm07hi/cia5rhes501yvhkjx1l5p3sr0', + 'http://example.com/cia5rhes501ywhkjxdrcc28nn/cia5rhes501yxhkjxqcqd6ogs/cia5rhes501yyhkjxim858nwj/cia5rhes501yzhkjxpi5u68xr', + 'http://example.com/cia5rhes501z0hkjxgxk8ryu8/cia5rhes501z1hkjx7jqdu67h/cia5rhes501z2hkjx21zj3rmt/cia5rhes501z3hkjxcq1lwavz', + 'http://example.com/cia5rhes501z4hkjxgx1266ef/cia5rhes501z5hkjxi6uyr5et/cia5rhes501z6hkjxhr0eot9n/cia5rhes501z7hkjxyz2oyzjs', + 'http://example.com/cia5rhes501z8hkjx5s5s200w/cia5rhes501z9hkjx67v1yb2z/cia5rhes501zahkjxw5u36sb5/cia5rhes501zbhkjxl17xibdr', + 'http://example.com/cia5rhes501zchkjxpx05d6o1/cia5rhes501zdhkjxiiadtum2/cia5rhes501zehkjxoj9i56gl/cia5rhes501zfhkjxqcxmjy73', + 'http://example.com/cia5rhes501zghkjxegc7tvdy/cia5rhes501zhhkjxqeeoq63e/cia5rhes501zihkjxysrggeqs/cia5rhes501zjhkjxf24x4w8j', + 'http://example.com/cia5rhes501zkhkjx36w5g359/cia5rhes501zlhkjxuornb7pf/cia5rhes501zmhkjx4pvpci2q/cia5rhes501znhkjxbv1oa4fp', + 'http://example.com/cia5rhes501zohkjxb6t1a9pz/cia5rhes501zphkjxg5ezhfdv/cia5rhes501zqhkjxl3efud9l/cia5rhes501zrhkjxcqb7r2sc', + 'http://example.com/cia5rhes501zshkjxd7wcvoav/cia5rhes501zthkjxelhdxd7w/cia5rhes501zuhkjxh07pf32p/cia5rhes501zvhkjxgcxn3nvl', + 'http://example.com/cia5rhes501zwhkjx95ri5zb5/cia5rhes501zxhkjxci9sujxb/cia5rhes501zyhkjx1hzc65ou/cia5rhes501zzhkjxf1kbgic9', + 'http://example.com/cia5rhes50200hkjxphxlxmld/cia5rhes50201hkjx0sveusk8/cia5rhes50202hkjxg5822asq/cia5rhes50203hkjxxle2qnr4', + 'http://example.com/cia5rhes50204hkjxswna3iww/cia5rhes50205hkjxo41y7z2t/cia5rhes50206hkjx1auwgf30/cia5rhes50207hkjx3vyiy15y', + 'http://example.com/cia5rhes50208hkjx6n640dxz/cia5rhes50209hkjxxb3tliuh/cia5rhes5020ahkjxht8vaioj/cia5rhes5020bhkjxqjo5gr27', + 'http://example.com/cia5rhes5020chkjxh9wu9gbv/cia5rhes5020dhkjxbrv63660/cia5rhes5020ehkjxbmozonad/cia5rhes5020fhkjxsek9b1wa', + 'http://example.com/cia5rhes5020ghkjxrlfea9iv/cia5rhes5020hhkjxt7qh369y/cia5rhes5020ihkjxkn7yslxt/cia5rhes5020jhkjx2ge4xq51', + 'http://example.com/cia5rhes5020khkjx2sp9c2gt/cia5rhes5020lhkjx1ks9juca/cia5rhes5020mhkjxrova7tax/cia5rhes5020nhkjxnaxah6tg', + 'http://example.com/cia5rhes5020ohkjx9btins8g/cia5rhes5020phkjxy4or4s6u/cia5rhes5020qhkjxxrqcpd3n/cia5rhes5020rhkjxm6xw3z2x', + 'http://example.com/cia5rhes5020shkjxz31fkpjb/cia5rhes5020thkjxsxivj1tx/cia5rhes5020uhkjx218dg3oe/cia5rhes5020vhkjxpxflwg9k', + 'http://example.com/cia5rhes5020whkjx3xpogsrh/cia5rhes5020xhkjxv5k6yvhb/cia5rhes5020yhkjxmg5wu4xg/cia5rhes5020zhkjx49u1376r', + 'http://example.com/cia5rhes50210hkjxu07iog9j/cia5rhes50211hkjxe2zq097b/cia5rhes50212hkjx7d2n5bis/cia5rhes50213hkjx98z0f1wd', + 'http://example.com/cia5rhes50214hkjxxz2fxal3/cia5rhes50215hkjx4cdss157/cia5rhes50216hkjxgemb403b/cia5rhes50217hkjxcx1to7hv', + 'http://example.com/cia5rhes50218hkjxlm8ctocp/cia5rhes50219hkjx1fcacxy3/cia5rhes5021ahkjxx59gdemf/cia5rhes5021bhkjxa8w89mbs', + 'http://example.com/cia5rhes5021chkjxgbgtxsby/cia5rhes5021dhkjxpsb7jlci/cia5rhes5021ehkjxo8ytwukr/cia5rhes5021fhkjxtpoy84xh', + 'http://example.com/cia5rhes5021ghkjxyk2hucae/cia5rhes5021hhkjxyiywhstb/cia5rhes5021ihkjx1sdmxxsc/cia5rhes5021jhkjxp5btccgt', + 'http://example.com/cia5rhes5021khkjxav298li6/cia5rhes5021lhkjx4ba0mhnf/cia5rhes5021mhkjxngkomyhl/cia5rhes5021nhkjxxtqqmtir', + 'http://example.com/cia5rhes5021ohkjxbavqb4tz/cia5rhes5021phkjx1f18irux/cia5rhes5021qhkjxgef61ilr/cia5rhes5021rhkjxeh1y04kj', + 'http://example.com/cia5rhes5021shkjxr4s9i0ob/cia5rhes5021thkjxfocdh5vi/cia5rhes5021uhkjxjcwajris/cia5rhes5021vhkjxitwdjshb', + 'http://example.com/cia5rhes5021whkjxwhlm3an5/cia5rhes5021xhkjx5dcoj15s/cia5rhes5021yhkjxy9biyupr/cia5rhes5021zhkjx6wit7c1p', + 'http://example.com/cia5rhes50220hkjxco3srhrz/cia5rhes50221hkjxn8kb150i/cia5rhes50222hkjxcfl48mla/cia5rhes50223hkjx5wzddel7', + 'http://example.com/cia5rhes50224hkjxv4kbq0bu/cia5rhes50225hkjxdlcujhtv/cia5rhes50226hkjx0nm0ncdj/cia5rhes50227hkjx4hnvg7w9', + 'http://example.com/cia5rhes50228hkjxn2hoexz0/cia5rhes50229hkjx5a0zae0n/cia5rhes5022ahkjx7kw3lf0v/cia5rhes5022bhkjx9uaqp2w5', + 'http://example.com/cia5rhes5022chkjxmllq37r4/cia5rhes5022dhkjxuogvq5kp/cia5rhes5022ehkjxegsxagw5/cia5rhes5022fhkjx25d5a5z8', + 'http://example.com/cia5rhes5022ghkjxwwoecae0/cia5rhes5022hhkjxli8zm9vs/cia5rhes5022ihkjxzxcky0jv/cia5rhes5022jhkjxvsb9g2qa', + 'http://example.com/cia5rhes5022khkjxhwpswkll/cia5rhes5022lhkjxow1y1vc4/cia5rhes5022mhkjxh0o8b4r5/cia5rhes5022nhkjxjsyoo9le', + 'http://example.com/cia5rhes5022ohkjx50pmnu22/cia5rhes5022phkjxfdh1jhl2/cia5rhes5022qhkjxh67gv4up/cia5rhes5022rhkjxmpux301t', + 'http://example.com/cia5rhes5022shkjxmgm2q2tv/cia5rhes5022thkjx7ivn1k01/cia5rhes5022uhkjxs4j1z1st/cia5rhes5022vhkjxh3y1ak61', + 'http://example.com/cia5rhes5022whkjxy2vkf9qu/cia5rhes5022xhkjxotujbeup/cia5rhes5022yhkjx5qiu2ujp/cia5rhes5022zhkjxluajf32y', + 'http://example.com/cia5rhes50230hkjxk7stw4db/cia5rhes60231hkjxf7aj9i0m/cia5rhes60232hkjxziydwog0/cia5rhes60233hkjxx3x1fbuc', + 'http://example.com/cia5rhes60234hkjxg2uqu0ml/cia5rhes60235hkjxq7n4gpgv/cia5rhes60236hkjxolpslbdw/cia5rhes60237hkjxyn1lp5ir', + 'http://example.com/cia5rhes60238hkjxwis4nirx/cia5rhes60239hkjxaiqtx5n6/cia5rhes6023ahkjxsgrablt0/cia5rhes6023bhkjxc06147lu', + 'http://example.com/cia5rhes6023chkjxxge8xmjn/cia5rhes6023dhkjx5j31jwgd/cia5rhes6023ehkjxwuz388j6/cia5rhes6023fhkjx3pdltokg', + 'http://example.com/cia5rhes6023ghkjx6dffsn9x/cia5rhes6023hhkjxzjoqqtor/cia5rhes6023ihkjx3bz79voa/cia5rhes6023jhkjxa7bb04th', + 'http://example.com/cia5rhes6023khkjxhg5ub876/cia5rhes6023lhkjxrklzuro9/cia5rhes6023mhkjx8xmhpdqm/cia5rhes6023nhkjxch1jn490', + 'http://example.com/cia5rhes6023ohkjxhad7g229/cia5rhes6023phkjx4zaksvdn/cia5rhes6023qhkjxx6ko1cpf/cia5rhes6023rhkjx0vireriy', + 'http://example.com/cia5rhes6023shkjxhvae8jtn/cia5rhes6023thkjxw4de6xi4/cia5rhes6023uhkjxzfqht8ml/cia5rhes6023vhkjxs8ul3zvc', + 'http://example.com/cia5rhes6023whkjxdsyyu08r/cia5rhes6023xhkjxhddko66j/cia5rhes6023yhkjxnfhgsx6b/cia5rhes6023zhkjxt63bqpbs', + 'http://example.com/cia5rhes60240hkjxa7oafjex/cia5rhes60241hkjx74x1e2f3/cia5rhes60242hkjxiaptta0r/cia5rhes60243hkjxingpv6qf', + 'http://example.com/cia5rhes60244hkjx832w9v0m/cia5rhes60245hkjxbtb4g19e/cia5rhes60246hkjxahthge6j/cia5rhes60247hkjxhqj3m07o', + 'http://example.com/cia5rhes60248hkjxcf7nc4li/cia5rhes60249hkjxyaeee0po/cia5rhes6024ahkjxz0zbl31v/cia5rhes6024bhkjxyli25oi7', + 'http://example.com/cia5rhes6024chkjxqymyzh67/cia5rhes6024dhkjx41mtrlwg/cia5rhes6024ehkjxupbohin3/cia5rhes6024fhkjx1wtwax3q', + 'http://example.com/cia5rhes6024ghkjxbhnnx8qm/cia5rhes6024hhkjx330f907k/cia5rhes6024ihkjxt8kevs6h/cia5rhes6024jhkjx6fz60hhj', + 'http://example.com/cia5rhes6024khkjx6jh6byd0/cia5rhes6024lhkjxnqak5lqd/cia5rhes6024mhkjx6qi3ka0d/cia5rhes6024nhkjxmydiqa1w', + 'http://example.com/cia5rhes6024ohkjx1wzyvp8g/cia5rhes6024phkjxcpe4crtr/cia5rhes6024qhkjx5k672peu/cia5rhes6024rhkjxrgc14c0o', + 'http://example.com/cia5rhes6024shkjxt3phdd6y/cia5rhes6024thkjxrcolx8rw/cia5rhes6024uhkjx1m8lrl96/cia5rhes6024vhkjx1ub0usjq', + 'http://example.com/cia5rhes6024whkjx30q3vye6/cia5rhes6024xhkjxqhicyl5l/cia5rhes6024yhkjxewkiuvcd/cia5rhes6024zhkjxpi0s95q6', + 'http://example.com/cia5rhes60250hkjx7x45wchz/cia5rhes60251hkjx29nj5yrn/cia5rhes60252hkjxmjtv4j8t/cia5rhes60253hkjx62flt3ct', + 'http://example.com/cia5rhes60254hkjxj24tyltz/cia5rhes60255hkjxu43vfkjt/cia5rhes60256hkjxorb3l17v/cia5rhes60257hkjxuusa9260', + 'http://example.com/cia5rhes60258hkjx2mtr4h7o/cia5rhes60259hkjxfni1laoe/cia5rhes6025ahkjxi8p6cxws/cia5rhes6025bhkjxms0v3mvk', + 'http://example.com/cia5rhes6025chkjxak2ehrye/cia5rhes6025dhkjxkkwv08j7/cia5rhes6025ehkjxmviua90r/cia5rhes6025fhkjxxz5403tq', + 'http://example.com/cia5rhes6025ghkjxw2zi9e42/cia5rhes6025hhkjxcpaquver/cia5rhes6025ihkjxdza15efa/cia5rhes6025jhkjxj10ftcde', + 'http://example.com/cia5rhes6025khkjxzdgyklzu/cia5rhes6025lhkjxepec48wo/cia5rhes6025mhkjxrr0rxhsw/cia5rhes6025nhkjxbx5apxib', + 'http://example.com/cia5rhes6025ohkjxmw1aiv3f/cia5rhes6025phkjxf2m420e9/cia5rhes6025qhkjxjiwth0yz/cia5rhes6025rhkjxrmxufevy', + 'http://example.com/cia5rhes6025shkjxusdiwv01/cia5rhes6025thkjxds425t8m/cia5rhes6025uhkjxuqrtt7if/cia5rhes6025vhkjxowk5zvf3', + 'http://example.com/cia5rhes6025whkjxh652j091/cia5rhes6025xhkjxg7n9opan/cia5rhes6025yhkjxhx4aysaj/cia5rhes6025zhkjxu82h4n54', + 'http://example.com/cia5rhes60260hkjxi674w0z0/cia5rhes60261hkjxojs9dwc5/cia5rhes60262hkjx9zme8232/cia5rhes60263hkjxg3tduw2q', + 'http://example.com/cia5rhes60264hkjxen5f1emm/cia5rhes60265hkjx9wlrydmg/cia5rhes60266hkjxyk0z00l1/cia5rhes60267hkjxim57nlkk', + 'http://example.com/cia5rhes60268hkjx0dxjfg9r/cia5rhes60269hkjxvsd7fx55/cia5rhes6026ahkjxr4wv79py/cia5rhes6026bhkjxbtuynf74', + 'http://example.com/cia5rhes6026chkjx0hbrlens/cia5rhes6026dhkjx4oarjdzi/cia5rhes6026ehkjxcfh9kh1i/cia5rhes6026fhkjxdvhhj9ps', + 'http://example.com/cia5rhes6026ghkjxzbxwxiwi/cia5rhes6026hhkjx10dmy3ck/cia5rhes6026ihkjxrh57qzib/cia5rhes6026jhkjxa6wqf4ro', + 'http://example.com/cia5rhes6026khkjxw4rqjhaq/cia5rhes6026lhkjxuc55dmgp/cia5rhes6026mhkjxlv6a6sz0/cia5rhes6026nhkjxwxm1u6cu', + 'http://example.com/cia5rhes6026ohkjxcezmtk1t/cia5rhes6026phkjxt8hncf2i/cia5rhes6026qhkjxuxprl91o/cia5rhes6026rhkjx9ujzo2je', + 'http://example.com/cia5rhes6026shkjxxutau6ka/cia5rhes6026thkjxa2hy9mje/cia5rhes6026uhkjxr2vho147/cia5rhes6026vhkjx7h70z8i9', + 'http://example.com/cia5rhes6026whkjx1nagxk22/cia5rhes6026xhkjxke02jgeq/cia5rhes6026yhkjxhemx0l0x/cia5rhes6026zhkjx8uhw94o4', + 'http://example.com/cia5rhes60270hkjxtpo8z0gx/cia5rhes60271hkjxaldlng02/cia5rhes60272hkjxi6u6vyos/cia5rhes60273hkjx8t4gz8q3', + 'http://example.com/cia5rhes60274hkjxzetzmgfp/cia5rhes60275hkjxqtd9rh66/cia5rhes60276hkjxo38ak1v6/cia5rhes60277hkjx3t2grzdi', + 'http://example.com/cia5rhes60278hkjxssjf92tp/cia5rhes60279hkjxtdiimuwo/cia5rhes6027ahkjxv7i327um/cia5rhes6027bhkjx34iyiwau', + 'http://example.com/cia5rhes6027chkjxsalv7vq1/cia5rhes6027dhkjxj1qa0eqe/cia5rhes6027ehkjxdstykpct/cia5rhes6027fhkjxep1lg57f', + 'http://example.com/cia5rhes6027ghkjxir6tvp5r/cia5rhes6027hhkjx37mwtxmp/cia5rhes6027ihkjxajh8kdk0/cia5rhes6027jhkjxprxxf6bf', + 'http://example.com/cia5rhes6027khkjxtx8rt4eg/cia5rhes6027lhkjx6stckrq2/cia5rhes6027mhkjxbp2scl06/cia5rhes6027nhkjx5tcodm70', + 'http://example.com/cia5rhes6027ohkjx02hq4e4i/cia5rhes6027phkjxpj98682x/cia5rhes6027qhkjxi6t9w6j8/cia5rhes6027rhkjxdoo5aitq', + 'http://example.com/cia5rhes6027shkjxq61ipcpf/cia5rhes6027thkjx4c95chxk/cia5rhes6027uhkjx5yp65br8/cia5rhes6027vhkjxgaj3cw9t', + 'http://example.com/cia5rhes6027whkjxx18if78t/cia5rhes6027xhkjxeruuk14w/cia5rhes6027yhkjxzur0jh40/cia5rhes6027zhkjx2zxmcdyy', + 'http://example.com/cia5rhes60280hkjxrh298dzu/cia5rhes60281hkjx5m40ppz3/cia5rhes60282hkjxfak6x0vp/cia5rhes60283hkjxcokmxlit', + 'http://example.com/cia5rhes60284hkjx58dts12q/cia5rhes60285hkjx7hgaud95/cia5rhes60286hkjxdycu90lv/cia5rhes60287hkjxjj4cgdk8', + 'http://example.com/cia5rhes60288hkjxai7gc5c8/cia5rhes60289hkjxbnomezv6/cia5rhes6028ahkjxw7wxahj2/cia5rhes6028bhkjx1smzie0j', + 'http://example.com/cia5rhes6028chkjxa57aiiju/cia5rhes6028dhkjxs1etgvw7/cia5rhes6028ehkjxtsbz6p0z/cia5rhes6028fhkjxmo1vsspv', + 'http://example.com/cia5rhes6028ghkjxieobtxp5/cia5rhes6028hhkjx9ragsscj/cia5rhes6028ihkjx385kpk1h/cia5rhes6028jhkjxotj68l1k', + 'http://example.com/cia5rhes6028khkjxea5reemm/cia5rhes6028lhkjx0kwzwbyo/cia5rhes6028mhkjx4nqjjcde/cia5rhes6028nhkjxzrrex5ue', + 'http://example.com/cia5rhes6028ohkjx7t2lhe7z/cia5rhes6028phkjx46qyubif/cia5rhes6028qhkjxjolbuqus/cia5rhes6028rhkjx8r7ii6z7', + 'http://example.com/cia5rhes6028shkjxilpnvd7j/cia5rhes6028thkjxof8m415p/cia5rhes6028uhkjxjp4mywli/cia5rhes6028vhkjxcw58yxw0', + 'http://example.com/cia5rhes6028whkjxhya97tqs/cia5rhes6028xhkjxpezwz1pe/cia5rhes6028yhkjxx59c4igt/cia5rhes6028zhkjxdjv35rpr', + 'http://example.com/cia5rhes60290hkjxnthanean/cia5rhes60291hkjxni7pjxv4/cia5rhes60292hkjx0flrl74n/cia5rhes60293hkjxm9x63zo7', + 'http://example.com/cia5rhes60294hkjxpnfmclsw/cia5rhes60295hkjx56ccc80r/cia5rhes60296hkjx4s91lrwv/cia5rhes60297hkjxf132ofl7', + 'http://example.com/cia5rhes60298hkjxl3mctpt0/cia5rhes60299hkjxvlg5nt62/cia5rhes6029ahkjx336mdt5q/cia5rhes6029bhkjxx1be21if', + 'http://example.com/cia5rhes6029chkjxo22y49m7/cia5rhes6029dhkjx1llimb0p/cia5rhes6029ehkjxt13ucuxv/cia5rhes6029fhkjxh2xoljln', + 'http://example.com/cia5rhes6029ghkjx68wd962d/cia5rhes6029hhkjx387d5swn/cia5rhes6029ihkjxh34aue0p/cia5rhes6029jhkjxfh61fg9l', + 'http://example.com/cia5rhes6029khkjxuz53ttqc/cia5rhes6029lhkjxvrp7a6bu/cia5rhes6029mhkjx5ug57g8j/cia5rhes6029nhkjxiv7fjxr3', + 'http://example.com/cia5rhes6029ohkjx2im4dkbc/cia5rhes6029phkjxk2vkitw7/cia5rhes6029qhkjx1g18697q/cia5rhes6029rhkjxu7cv0cp5', + 'http://example.com/cia5rhes6029shkjxzfgxcfx5/cia5rhes6029thkjx6bi4op1u/cia5rhes6029uhkjx57v7j2tp/cia5rhes6029vhkjxqsn3ros1', + 'http://example.com/cia5rhes7029whkjx33b3346i/cia5rhes7029xhkjxnhbvzlyl/cia5rhes7029yhkjxhofpksax/cia5rhes7029zhkjxpckp9le4', + 'http://example.com/cia5rhes702a0hkjx6pzs7e5d/cia5rhes702a1hkjxp2x65zqo/cia5rhes702a2hkjxu66pcizj/cia5rhes702a3hkjx7o8r0f06', + 'http://example.com/cia5rhes702a4hkjxs3nk500n/cia5rhes702a5hkjxg0rbzm6k/cia5rhes702a6hkjx234c6g7e/cia5rhes702a7hkjx9ocd54xq', + 'http://example.com/cia5rhes702a8hkjxhv3xsjpp/cia5rhes702a9hkjxofxw9mdy/cia5rhes702aahkjxwtmyec4h/cia5rhes702abhkjxly8sn8hi', + 'http://example.com/cia5rhes702achkjx7zlau40c/cia5rhes702adhkjx6i9t1hdm/cia5rhes702aehkjx3w115jp6/cia5rhes702afhkjx3spdsa1v', + 'http://example.com/cia5rhes702aghkjxd4i1f3k7/cia5rhes702ahhkjx1o7338m9/cia5rhes702aihkjx3issv8lp/cia5rhes702ajhkjxkkpxy74s', + 'http://example.com/cia5rhes702akhkjxdng2ft24/cia5rhes702alhkjxvf0nimyo/cia5rhes702amhkjxubx3l0hc/cia5rhes702anhkjxjdg78083', + 'http://example.com/cia5rhes702aohkjxb6np3w0m/cia5rhes702aphkjxbmp49sgd/cia5rhes702aqhkjx3wm23ff0/cia5rhes702arhkjx9ht9wc86', + 'http://example.com/cia5rhes702ashkjxw56jbjfz/cia5rhes702athkjx6js735z5/cia5rhes702auhkjxucfu5lpt/cia5rhes702avhkjxbyglt9ex', + 'http://example.com/cia5rhes702awhkjx18s0uu13/cia5rhes702axhkjxi3zrv40h/cia5rhes702ayhkjx3a8cp916/cia5rhes702azhkjxczqrzngo', + 'http://example.com/cia5rhes702b0hkjxglj4n5o7/cia5rhes702b1hkjx63bg4kb1/cia5rhes702b2hkjx60relgsi/cia5rhes702b3hkjxiol0e8ym', + 'http://example.com/cia5rhes702b4hkjxjfpk1sg5/cia5rhes702b5hkjxk428e7bk/cia5rhes702b6hkjxr97qxcy0/cia5rhes702b7hkjxdyz4rzzn', + 'http://example.com/cia5rhes702b8hkjx7vylah33/cia5rhes702b9hkjxinhs95fl/cia5rhes702bahkjxpengba9m/cia5rhes702bbhkjxh5smj013', + 'http://example.com/cia5rhes702bchkjxqce1aoab/cia5rhes702bdhkjxaiyf10a3/cia5rhes702behkjx5yqopkqf/cia5rhes702bfhkjx3hiu4jp5', + 'http://example.com/cia5rhes702bghkjx27997nof/cia5rhes702bhhkjxh131a1mu/cia5rhes702bihkjxdv7jmcf7/cia5rhes702bjhkjxu56c6np2', + 'http://example.com/cia5rhes702bkhkjxqpt1iswl/cia5rhes702blhkjxxvuevm79/cia5rhes702bmhkjxlb6egm5v/cia5rhes702bnhkjx0frya4zv', + 'http://example.com/cia5rhes702bohkjx62rqvbxx/cia5rhes702bphkjxn5543qcw/cia5rhes702bqhkjxo6xrcl3m/cia5rhes702brhkjxxiyxytk6', + 'http://example.com/cia5rhes702bshkjxtupz79qv/cia5rhes702bthkjx46tmi8da/cia5rhes702buhkjxa076ev9b/cia5rhes702bvhkjxwzgfevcu', + 'http://example.com/cia5rhes702bwhkjxwmx0x18a/cia5rhes702bxhkjxpq4el7be/cia5rhes702byhkjxwlypdgqk/cia5rhes702bzhkjxf16uiqj9', + 'http://example.com/cia5rhes702c0hkjx0oylz3z7/cia5rhes702c1hkjxnka3undy/cia5rhes702c2hkjx9pvadq7q/cia5rhes702c3hkjxubumi03d', + 'http://example.com/cia5rhes702c4hkjxv1je61d0/cia5rhes702c5hkjx3gud1w7h/cia5rhes702c6hkjxhbputn4m/cia5rhes702c7hkjx2fwamiyv', + 'http://example.com/cia5rhes702c8hkjxbgkmje13/cia5rhes702c9hkjxlumxva5q/cia5rhes702cahkjxmiet3v1x/cia5rhes702cbhkjx8ibo8t0v', + 'http://example.com/cia5rhes702cchkjxyl6aj596/cia5rhes702cdhkjxuk4jdais/cia5rhes702cehkjxznkrhgcf/cia5rhes702cfhkjxedld1xxc', + 'http://example.com/cia5rhes702cghkjxc2ry2vt4/cia5rhes702chhkjxahplgyzs/cia5rhes702cihkjxdfgeirre/cia5rhes702cjhkjx5k6zbwnv', + 'http://example.com/cia5rhes702ckhkjxt8jo94yh/cia5rhes702clhkjxsjs9l544/cia5rhes702cmhkjxob8bd0zc/cia5rhes702cnhkjx6cfcl3n9', + 'http://example.com/cia5rhes702cohkjxb9cd9ogj/cia5rhes702cphkjxpoorw1yg/cia5rhes702cqhkjxykcpxjap/cia5rhes702crhkjx3469lxlp', + 'http://example.com/cia5rhes702cshkjxmwi9wm5t/cia5rhes702cthkjx8tmzifvh/cia5rhes702cuhkjx4l68blak/cia5rhes702cvhkjxdxodcgpw', + 'http://example.com/cia5rhes702cwhkjx0tbp18xa/cia5rhes702cxhkjxa9e95679/cia5rhes702cyhkjxpunm4oge/cia5rhes702czhkjxsxewphj9', + 'http://example.com/cia5rhes702d0hkjx1a2yy8af/cia5rhes702d1hkjx4f2cssht/cia5rhes702d2hkjxa1d631y5/cia5rhes702d3hkjx5isc7bl5', + 'http://example.com/cia5rhes702d4hkjxxf0dzxl4/cia5rhes702d5hkjxxnd097v7/cia5rhes702d6hkjx98mpvdya/cia5rhes702d7hkjx284luop7', + 'http://example.com/cia5rhes702d8hkjxy6hghmfk/cia5rhes702d9hkjxr4ozxswm/cia5rhes702dahkjx4aemrdzl/cia5rhes702dbhkjx3b9om3gn', + 'http://example.com/cia5rhes702dchkjx2q559yuu/cia5rhes702ddhkjxr1frvgb5/cia5rhes702dehkjx59to46ip/cia5rhes702dfhkjxtjmix0kn', + 'http://example.com/cia5rhes702dghkjxk4m6a2s0/cia5rhes702dhhkjxfwaeszqy/cia5rhes702dihkjx4zf8y4ca/cia5rhes702djhkjxvhfrquil', + 'http://example.com/cia5rhes702dkhkjx2orxsnm3/cia5rhes702dlhkjx47rdcwpv/cia5rhes702dmhkjx8j62q07m/cia5rhes702dnhkjxt3qftg4a', + 'http://example.com/cia5rhes702dohkjxer57v1ky/cia5rhes702dphkjxjbishjq1/cia5rhes702dqhkjxt8r2fmuw/cia5rhes702drhkjx8etd1xkq', + 'http://example.com/cia5rhes702dshkjxwbjmsogs/cia5rhes702dthkjxzjt0f26i/cia5rhes702duhkjxrspfet0e/cia5rhes702dvhkjx24ih1puf', + 'http://example.com/cia5rhes702dwhkjx4qx5ofni/cia5rhes702dxhkjxyxhxsw0c/cia5rhes702dyhkjx8mi9wbce/cia5rhes702dzhkjxr9gk1g19', + 'http://example.com/cia5rhes702e0hkjxin8zq13k/cia5rhes702e1hkjxn5bq0ikw/cia5rhes702e2hkjxxb2qoxsk/cia5rhes702e3hkjxbco0q0qj', + 'http://example.com/cia5rhes702e4hkjxhxbl6l43/cia5rhes702e5hkjx0zz697fh/cia5rhes702e6hkjxfdsk112c/cia5rhes702e7hkjxabbxyd7j', + 'http://example.com/cia5rhes702e8hkjx3vnctynz/cia5rhes702e9hkjxg4zopm86/cia5rhes702eahkjxo3bg8ml3/cia5rhes702ebhkjxp3aeugu4', + 'http://example.com/cia5rhes702echkjxal3j832h/cia5rhes702edhkjx1lyibi15/cia5rhes702eehkjxstdtwkp6/cia5rhes702efhkjxdnbnyno0', + 'http://example.com/cia5rhes702eghkjx55wp2mw0/cia5rhes702ehhkjxwmxwjl29/cia5rhes702eihkjxg7t126ld/cia5rhes702ejhkjx15qdziu1', + 'http://example.com/cia5rhes702ekhkjxc0im9wy4/cia5rhes702elhkjxh2jd7hzr/cia5rhes702emhkjxcu8r9pzm/cia5rhes702enhkjx9jbgidf1', + 'http://example.com/cia5rhes702eohkjxhlu6h4ep/cia5rhes702ephkjx4mwoc3ql/cia5rhes702eqhkjxe2bwkjv6/cia5rhes702erhkjxh8shrs32', + 'http://example.com/cia5rhes702eshkjxs8w53l9b/cia5rhes702ethkjx1xsjdbbm/cia5rhes702euhkjxjrkym5vf/cia5rhes702evhkjxsuode17c', + 'http://example.com/cia5rhes702ewhkjxj1bzme2d/cia5rhes702exhkjx88mzjzre/cia5rhes702eyhkjxst5flmg9/cia5rhes702ezhkjxdar3h55h', + 'http://example.com/cia5rhes702f0hkjxrdjoki1j/cia5rhes702f1hkjx7iz1lpso/cia5rhes702f2hkjxvsyy2boh/cia5rhes702f3hkjxe4lwxkjq', + 'http://example.com/cia5rhes702f4hkjxhsgvfwf9/cia5rhes702f5hkjxtemdddm6/cia5rhes702f6hkjx8t7z5qmo/cia5rhes702f7hkjxgb9mzb5t', + 'http://example.com/cia5rhes702f8hkjxen7vbt3a/cia5rhes702f9hkjxozpijk1f/cia5rhes702fahkjxh2l1f7h6/cia5rhes702fbhkjxxojzw7gn', + 'http://example.com/cia5rhes702fchkjx0tvnzt2w/cia5rhes702fdhkjxbi6zt33e/cia5rhes702fehkjxd54fxgzx/cia5rhes702ffhkjxsayc02os', + 'http://example.com/cia5rhes702fghkjxpygjjz89/cia5rhes702fhhkjxbct2ojjb/cia5rhes702fihkjxe46ngi4m/cia5rhes702fjhkjxq7azlfig', + 'http://example.com/cia5rhes702fkhkjx1ff4tumn/cia5rhes702flhkjxosiemsy8/cia5rhes702fmhkjx0o6ktv9m/cia5rhes702fnhkjxj9yp67gs', + 'http://example.com/cia5rhes702fohkjxqro5xqt8/cia5rhes702fphkjx6s3gi6y0/cia5rhes702fqhkjxkkab85zz/cia5rhes702frhkjxo03b56tw', + 'http://example.com/cia5rhes702fshkjxlvaiv6rz/cia5rhes702fthkjxvkg1r7dy/cia5rhes702fuhkjx3txhokr4/cia5rhes702fvhkjxtvqvs9ei', + 'http://example.com/cia5rhes702fwhkjx5mknq1w5/cia5rhes702fxhkjxrj6a3pub/cia5rhes702fyhkjxvhu05ms3/cia5rhes702fzhkjxjby42qra', + 'http://example.com/cia5rhes702g0hkjxrcf4pcw1/cia5rhes702g1hkjxn081wq4r/cia5rhes702g2hkjxaf91n239/cia5rhes702g3hkjxxlcnut0h', + 'http://example.com/cia5rhes702g4hkjxboifrcf9/cia5rhes702g5hkjxzdowoz5o/cia5rhes702g6hkjxukarx97t/cia5rhes702g7hkjxccz4m3ra', + 'http://example.com/cia5rhes702g8hkjxcojon0ux/cia5rhes702g9hkjxldlady20/cia5rhes702gahkjxzy3fh4eg/cia5rhes702gbhkjxrbfe6e4i', + 'http://example.com/cia5rhes702gchkjxk66e8nbf/cia5rhes702gdhkjxudeemkvv/cia5rhes702gehkjx3c3hpe66/cia5rhes702gfhkjxn9olbr7q', + 'http://example.com/cia5rhes702gghkjxjutmvz9r/cia5rhes702ghhkjxevjnumc0/cia5rhes702gihkjxcsgdpbt7/cia5rhes702gjhkjxkajsb5n7', + 'http://example.com/cia5rhes702gkhkjxpctjecch/cia5rhes702glhkjx4psglrrf/cia5rhes702gmhkjxqsa29brc/cia5rhes702gnhkjxtu5lc4me', + 'http://example.com/cia5rhes702gohkjxy4ljuvei/cia5rhes702gphkjxscllm1ij/cia5rhes702gqhkjx1e8d9ndd/cia5rhes702grhkjxvt3mx80t', + 'http://example.com/cia5rhes702gshkjxex3gg0nz/cia5rhes702gthkjxlonhrzjs/cia5rhes702guhkjxl4vdp4al/cia5rhes702gvhkjxvu5xtj65', + 'http://example.com/cia5rhes702gwhkjx2eqa8s8p/cia5rhes702gxhkjxzs0d96f8/cia5rhes702gyhkjxh5qhyc6d/cia5rhes702gzhkjxit6h6kq6', + 'http://example.com/cia5rhes702h0hkjxovyyxzzh/cia5rhes702h1hkjxumx2doq9/cia5rhes702h2hkjxe8rwx6ye/cia5rhes702h3hkjxd0biux3c', + 'http://example.com/cia5rhes702h4hkjx0r9rhds4/cia5rhes802h5hkjxxe3pbik6/cia5rhes802h6hkjxkoyqybob/cia5rhes802h7hkjx0s2gcxkk', + 'http://example.com/cia5rhes802h8hkjx1b212net/cia5rhes802h9hkjxhye14m2j/cia5rhes802hahkjxf87hamb3/cia5rhes802hbhkjxvqh1ek5s', + 'http://example.com/cia5rhes802hchkjx5hdnwwul/cia5rhes802hdhkjxyc9ojtpr/cia5rhes802hehkjxdnrgdch1/cia5rhes802hfhkjxj6gwgjbt', + 'http://example.com/cia5rhes802hghkjxehwpywy9/cia5rhes802hhhkjx4lbi6x6l/cia5rhes802hihkjxnpf2cz93/cia5rhes802hjhkjxv9bgej4e', + 'http://example.com/cia5rhes802hkhkjxta1aj8pd/cia5rhes802hlhkjxqot5lx49/cia5rhes802hmhkjxs0uj77o1/cia5rhes802hnhkjx69uqlhl9', + 'http://example.com/cia5rhes802hohkjxsetak465/cia5rhes802hphkjx7cc4cvnw/cia5rhes802hqhkjxyz2rd85f/cia5rhes802hrhkjxwwwj80zy', + 'http://example.com/cia5rhes802hshkjxcxpfz2zy/cia5rhes802hthkjx0mg13xvr/cia5rhes802huhkjxl8tf2f1j/cia5rhes802hvhkjxkkdxui48', + 'http://example.com/cia5rhes802hwhkjxnt5u3nhm/cia5rhes802hxhkjxbffb2x8l/cia5rhes802hyhkjxd0tm0h6e/cia5rhes802hzhkjxua697jh2', + 'http://example.com/cia5rhes802i0hkjx5thy2y3q/cia5rhes802i1hkjx3jr1y269/cia5rhes802i2hkjxwwksi6eg/cia5rhes802i3hkjxor5nv0z2', + 'http://example.com/cia5rhes802i4hkjx4ttg4je9/cia5rhes802i5hkjxqzq7w677/cia5rhes802i6hkjxeldnbsf2/cia5rhes802i7hkjxk8rmgjfv', + 'http://example.com/cia5rhes802i8hkjx6eb7w4np/cia5rhes802i9hkjxstgvt28t/cia5rhes802iahkjx8b9vwdzr/cia5rhes802ibhkjx1pnrsc7b', + 'http://example.com/cia5rhes802ichkjxvo1nrawf/cia5rhes802idhkjxgivthtjh/cia5rhes802iehkjxx967w9dk/cia5rhes802ifhkjxuu3hsee9', + 'http://example.com/cia5rhes802ighkjxeijczff2/cia5rhes802ihhkjxer0knjjl/cia5rhes802iihkjx116p0tfc/cia5rhes802ijhkjxuomqb7a0', + 'http://example.com/cia5rhes802ikhkjxzw0s6ejs/cia5rhes802ilhkjx1fgypntw/cia5rhes802imhkjx7jreimgw/cia5rhes802inhkjx3shm6234', + 'http://example.com/cia5rhes802iohkjx28sv1ivu/cia5rhes802iphkjxr4p098ji/cia5rhes802iqhkjxdsotusgp/cia5rhes802irhkjx5kudhhd2', + 'http://example.com/cia5rhes802ishkjxixiz6mp1/cia5rhes802ithkjxcagn4wzv/cia5rhes802iuhkjxnulj8edc/cia5rhes802ivhkjxkc73vmwx', + 'http://example.com/cia5rhes802iwhkjx0gdh9w2o/cia5rhes802ixhkjxcl50g4e1/cia5rhes802iyhkjxrptys42g/cia5rhes802izhkjx4w62hqht', + 'http://example.com/cia5rhes802j0hkjx692slfdu/cia5rhes802j1hkjxv3bwwytl/cia5rhes802j2hkjxx3gky18b/cia5rhes802j3hkjx3vsa5jra', + 'http://example.com/cia5rhes802j4hkjx2xe9zlx3/cia5rhes802j5hkjx4f3wi9rl/cia5rhes802j6hkjx2qr5bzrp/cia5rhes802j7hkjxrw3fcfe5', + 'http://example.com/cia5rhes802j8hkjx14k6emm1/cia5rhes802j9hkjxcll7rahj/cia5rhes802jahkjx6dmkabft/cia5rhes802jbhkjxj2d4kvm5', + 'http://example.com/cia5rhes802jchkjxu3olmu84/cia5rhes802jdhkjx1kyxhqd9/cia5rhes802jehkjxmzlxuvus/cia5rhes802jfhkjxt7cvj5h1', + 'http://example.com/cia5rhes802jghkjx80q77jzc/cia5rhes802jhhkjxtj8xxa1e/cia5rhes802jihkjxu4lnwkqf/cia5rhes802jjhkjx33w6a2yi', + 'http://example.com/cia5rhes802jkhkjxefk60o55/cia5rhes802jlhkjx9yuz30ib/cia5rhes802jmhkjxhuhfcbcy/cia5rhes802jnhkjxf6wkt9ht', + 'http://example.com/cia5rhes802johkjxbbd9f8zb/cia5rhes802jphkjxzk5mtk4f/cia5rhes802jqhkjxwb6eerfn/cia5rhes802jrhkjxpuyyqgqw', + 'http://example.com/cia5rhes802jshkjxbgeqep0t/cia5rhes802jthkjxdkbxnh69/cia5rhes802juhkjx03vlhdpu/cia5rhes802jvhkjx8zhdnauu', + 'http://example.com/cia5rhes802jwhkjxkw5bbsew/cia5rhes802jxhkjxgzdwzev2/cia5rhes802jyhkjxeutiz8ot/cia5rhes802jzhkjxeigt9qdf', + 'http://example.com/cia5rhes802k0hkjxvmfunndw/cia5rhes802k1hkjx6j968gws/cia5rhes802k2hkjxjdz6yfk6/cia5rhes802k3hkjxnsoiuwfm', + 'http://example.com/cia5rhes802k4hkjx7fezq8em/cia5rhes802k5hkjxsgmtvhig/cia5rhes802k6hkjx8h5r0ac5/cia5rhes802k7hkjxm0tczqr3', + 'http://example.com/cia5rhes802k8hkjx3ej667en/cia5rhes802k9hkjxeta3mqrs/cia5rhes802kahkjxttm3mtbc/cia5rhes802kbhkjx08tnchxt', + 'http://example.com/cia5rhes802kchkjxs9ys1d1h/cia5rhes802kdhkjxa7zfmkfh/cia5rhes802kehkjx0f1f3s5x/cia5rhes802kfhkjx5bkfptdv', + 'http://example.com/cia5rhes802kghkjxfbx0j2be/cia5rhes802khhkjx2796rmnr/cia5rhes802kihkjxc8qjmfqv/cia5rhes802kjhkjx5l18ngbo', + 'http://example.com/cia5rhes802kkhkjxuvxeycqp/cia5rhes802klhkjxt3xak01c/cia5rhes802kmhkjxsnbinf75/cia5rhes802knhkjxdke5f04u', + 'http://example.com/cia5rhes802kohkjxlr2esas9/cia5rhes802kphkjxoi8bubek/cia5rhes802kqhkjx652tsdtk/cia5rhes802krhkjxx0d9sapx', + 'http://example.com/cia5rhes802kshkjx01h9i4q2/cia5rhes802kthkjxnvp9j4x1/cia5rhes802kuhkjxyfb118if/cia5rhes802kvhkjxcajg7k7x', + 'http://example.com/cia5rhes802kwhkjx1ahqqj6a/cia5rhes802kxhkjx576izsui/cia5rhes802kyhkjxdopj85lq/cia5rhes802kzhkjxrak3td4w', + 'http://example.com/cia5rhes802l0hkjx4oalj3hp/cia5rhes802l1hkjxuiaufryz/cia5rhes802l2hkjx3yx8z13v/cia5rhes802l3hkjxql0nh4mw', + 'http://example.com/cia5rhes802l4hkjxh2yk4att/cia5rhes802l5hkjx1ld7evsc/cia5rhes802l6hkjx64mt6pcs/cia5rhes802l7hkjxoa0hr513', + 'http://example.com/cia5rhes802l8hkjxb8puz3pu/cia5rhes802l9hkjx40l0wzy4/cia5rhes802lahkjxvqaauxku/cia5rhes802lbhkjxxe2r13sb', + 'http://example.com/cia5rhes802lchkjxp07dxy3z/cia5rhes802ldhkjx4p23lqcu/cia5rhes802lehkjxj1swfy96/cia5rhes802lfhkjxeppnm27y', + 'http://example.com/cia5rhes802lghkjxowt32sxr/cia5rhes802lhhkjxr2wyl9ej/cia5rhes802lihkjx62orwsjq/cia5rhes802ljhkjxw99oj10o', + 'http://example.com/cia5rhes802lkhkjxieavj07d/cia5rhes802llhkjxrglllbwb/cia5rhes802lmhkjxjbmalhyj/cia5rhes802lnhkjx54ff2569', + 'http://example.com/cia5rhes802lohkjxgar3wut3/cia5rhes802lphkjx3y6byab9/cia5rhes802lqhkjx2ki1hks2/cia5rhes802lrhkjx867oulq5', + 'http://example.com/cia5rhes802lshkjxb7hkzrqs/cia5rhes802lthkjxfu4yyljq/cia5rhes802luhkjxqswmaz83/cia5rhes802lvhkjxcgjxwpin', + 'http://example.com/cia5rhes802lwhkjxnow6sr6f/cia5rhes802lxhkjxbtxn02ok/cia5rhes802lyhkjxtreu397w/cia5rhes802lzhkjx9fbk1l2s', + 'http://example.com/cia5rhes802m0hkjxuov8bbjf/cia5rhes802m1hkjxfjxcjswu/cia5rhes802m2hkjxnuumriep/cia5rhes802m3hkjxv3abuieh', + 'http://example.com/cia5rhes802m4hkjx7chzbj7m/cia5rhes802m5hkjxamwwacgg/cia5rhes802m6hkjxalw3n1b1/cia5rhes802m7hkjx4oobkiqi', + 'http://example.com/cia5rhes802m8hkjx01qpkvwg/cia5rhes802m9hkjx8s23cjy5/cia5rhes802mahkjx7zklmzeg/cia5rhes802mbhkjxs9htzggq', + 'http://example.com/cia5rhes802mchkjxn4kg4arq/cia5rhes802mdhkjxrlms5rxt/cia5rhes802mehkjx51y6d37q/cia5rhes802mfhkjxgq01e010', + 'http://example.com/cia5rhes802mghkjxcuk2pmky/cia5rhes802mhhkjxcy28ajz9/cia5rhes802mihkjxm3xz72d2/cia5rhes802mjhkjxfecrsmb1', + 'http://example.com/cia5rhes802mkhkjx976oo1q6/cia5rhes802mlhkjxt1d0ks1h/cia5rhes802mmhkjxlya2lnkr/cia5rhes802mnhkjxcjv6cg22', + 'http://example.com/cia5rhes802mohkjxbmd2ljcc/cia5rhes802mphkjxqmpsf43e/cia5rhes802mqhkjx1018sa5h/cia5rhes802mrhkjx83lcx364', + 'http://example.com/cia5rhes802mshkjxx317boaq/cia5rhes802mthkjxvgax0zmu/cia5rhes802muhkjxfgnulq5x/cia5rhes802mvhkjxg9czop5a', + 'http://example.com/cia5rhes802mwhkjx3qru605e/cia5rhes802mxhkjxo14r6mbk/cia5rhes802myhkjxnxvtblhe/cia5rhes802mzhkjxp4fuyyvq', + 'http://example.com/cia5rhes802n0hkjxta6r34nn/cia5rhes802n1hkjxn2des330/cia5rhes802n2hkjxut3wbscg/cia5rhes802n3hkjxi3sjsek8', + 'http://example.com/cia5rhes802n4hkjxlrze879b/cia5rhes802n5hkjxl9d2bptv/cia5rhes802n6hkjxe2pyq523/cia5rhes802n7hkjx3d7uk0va', + 'http://example.com/cia5rhes802n8hkjxsm87le7w/cia5rhes802n9hkjxilk0wcph/cia5rhes802nahkjx2n4ghjd4/cia5rhes802nbhkjx2z0n5kej', + 'http://example.com/cia5rhes802nchkjxjyazwvt2/cia5rhes802ndhkjxg9kfrprk/cia5rhes802nehkjxbgdpif6f/cia5rhes802nfhkjxe0456keb', + 'http://example.com/cia5rhes802nghkjxjhpr22o6/cia5rhes802nhhkjxplhnqcb8/cia5rhes802nihkjxzys0lxo2/cia5rhes802njhkjx2q01z427', + 'http://example.com/cia5rhes802nkhkjx2bh3a4jg/cia5rhes802nlhkjxwr4hs5z6/cia5rhes802nmhkjxuj8y14q2/cia5rhes802nnhkjxuhl3zdhl', + 'http://example.com/cia5rhes802nohkjxsghqd6qb/cia5rhes802nphkjxzwvmt5ut/cia5rhes802nqhkjxr3vatvee/cia5rhes802nrhkjx2bozv5k1', + 'http://example.com/cia5rhes802nshkjx2d7r9wfy/cia5rhes802nthkjxcxj3kn6a/cia5rhes802nuhkjxdria7pkp/cia5rhes802nvhkjx6uliansr', + 'http://example.com/cia5rhes802nwhkjx8nqjhhq1/cia5rhes802nxhkjxc2k2euc9/cia5rhes802nyhkjxdv6dq6vu/cia5rhes802nzhkjxidl9ujw8', + 'http://example.com/cia5rhes802o0hkjxt3hs5pt1/cia5rhes802o1hkjxouvuo74k/cia5rhes802o2hkjx46xz3nds/cia5rhes802o3hkjxrrrkqadg', + 'http://example.com/cia5rhes902o4hkjx2apdepej/cia5rhes902o5hkjxqqttkkkz/cia5rhes902o6hkjxh46l0jeu/cia5rhes902o7hkjxl7h17xdc', + 'http://example.com/cia5rhes902o8hkjxbafzc6v5/cia5rhes902o9hkjxcuowkvn1/cia5rhes902oahkjxasvphtbh/cia5rhes902obhkjxgp6ckpu5', + 'http://example.com/cia5rhes902ochkjxfb99zhss/cia5rhes902odhkjx0idz3cqv/cia5rhes902oehkjxy0f9nkn1/cia5rhes902ofhkjxnhrq2m1r', + 'http://example.com/cia5rhes902oghkjx24kuk19k/cia5rhes902ohhkjx5hx5puqb/cia5rhes902oihkjxcaqprqtz/cia5rhes902ojhkjx3zh6ivhp', + 'http://example.com/cia5rhes902okhkjxuk062elz/cia5rhes902olhkjxpv0ezkgb/cia5rhes902omhkjx6gkm3rj1/cia5rhes902onhkjxmckdzmmf', + 'http://example.com/cia5rhes902oohkjx6667yepw/cia5rhes902ophkjxkhrilcux/cia5rhes902oqhkjxubgywv84/cia5rhes902orhkjxl2z5gfhv', + 'http://example.com/cia5rhes902oshkjxwnznffds/cia5rhes902othkjx2nrd505l/cia5rhes902ouhkjxor8wvi62/cia5rhes902ovhkjxkknnf2c5', + 'http://example.com/cia5rhes902owhkjx0xvzj6j4/cia5rhes902oxhkjxm8wjviav/cia5rhes902oyhkjxd48tw0nv/cia5rhes902ozhkjxy55fth1m', + 'http://example.com/cia5rhes902p0hkjxhf2ln9fg/cia5rhes902p1hkjxn1kh849s/cia5rhes902p2hkjx7w18z1ij/cia5rhes902p3hkjx1iukw4f9', + 'http://example.com/cia5rhes902p4hkjx94e9yno8/cia5rhes902p5hkjxgg7krrow/cia5rhes902p6hkjxs7qbcgio/cia5rhes902p7hkjxjy5ubg21', + 'http://example.com/cia5rhes902p8hkjxc76syimq/cia5rhes902p9hkjxr4crms15/cia5rhes902pahkjxnijggak5/cia5rhes902pbhkjxzj7ajf4p', + 'http://example.com/cia5rhes902pchkjxtq8dybc1/cia5rhes902pdhkjxwqg0v1ob/cia5rhes902pehkjxig150nfx/cia5rhes902pfhkjx4pn0r7va', + 'http://example.com/cia5rhes902pghkjxg86s4zod/cia5rhes902phhkjxc16il6yq/cia5rhes902pihkjx25j53w11/cia5rhes902pjhkjxar484o36', + 'http://example.com/cia5rhes902pkhkjxqjtgnf6o/cia5rhes902plhkjxx22y2p6c/cia5rhes902pmhkjxu72lfdom/cia5rhes902pnhkjxv7bb9e9q', + 'http://example.com/cia5rhes902pohkjxxb029uj1/cia5rhes902pphkjx4ujdzzo5/cia5rhes902pqhkjxx4lnnhw7/cia5rhes902prhkjx6x2u79ck', + 'http://example.com/cia5rhes902pshkjxd1hhakk6/cia5rhes902pthkjxmpu8mcyi/cia5rhes902puhkjxzpcbicof/cia5rhes902pvhkjxij383b25', + 'http://example.com/cia5rhes902pwhkjxiur6rdbh/cia5rhes902pxhkjxyhkhpxrq/cia5rhes902pyhkjx29a11uyj/cia5rhes902pzhkjxf1p8g30r', + 'http://example.com/cia5rhes902q0hkjxotowbqgb/cia5rhes902q1hkjxmb7p5sr6/cia5rhes902q2hkjx378apexd/cia5rhes902q3hkjxjkglr1c4', + 'http://example.com/cia5rhes902q4hkjxxcw4jsq6/cia5rhes902q5hkjxqenj7c97/cia5rhes902q6hkjx2ye8s3q1/cia5rhes902q7hkjxtxm7sdya', + 'http://example.com/cia5rhes902q8hkjxkc9vstb2/cia5rhes902q9hkjxzwok7ng9/cia5rhes902qahkjx8ygp04d1/cia5rhes902qbhkjx4qux7aki', + 'http://example.com/cia5rhes902qchkjx57dfmt8h/cia5rhes902qdhkjx9b0035cy/cia5rhes902qehkjxvebgxts8/cia5rhes902qfhkjxu6yi37mb', + 'http://example.com/cia5rhes902qghkjx6xi3dyjx/cia5rhes902qhhkjx9x0aclfr/cia5rhes902qihkjx7mxvg28t/cia5rhes902qjhkjx9q2wphpa', + 'http://example.com/cia5rhes902qkhkjxu6s6q2q0/cia5rhes902qlhkjxlgcpgxpx/cia5rhes902qmhkjxc1dxbvr1/cia5rhes902qnhkjx6bvhf6hr', + 'http://example.com/cia5rhes902qohkjx1tm0hkvs/cia5rhes902qphkjx3pu22rbr/cia5rhes902qqhkjxdjcth8ug/cia5rhes902qrhkjxwhg6mr88', + 'http://example.com/cia5rhes902qshkjxji4arcck/cia5rhes902qthkjx5t1kjk9o/cia5rhes902quhkjx88zgme2o/cia5rhes902qvhkjxhs22agoc', + 'http://example.com/cia5rhes902qwhkjxkqdfy8em/cia5rhes902qxhkjxo11waca0/cia5rhes902qyhkjxthqzds0b/cia5rhes902qzhkjx890jrftn', + 'http://example.com/cia5rhes902r0hkjx2scv74kv/cia5rhes902r1hkjxhczgr5iw/cia5rhes902r2hkjxd9v3ewx1/cia5rhes902r3hkjxx4tpj5xh', + 'http://example.com/cia5rhes902r4hkjx8217649m/cia5rhes902r5hkjx954lwwvc/cia5rhes902r6hkjxzczxe9o4/cia5rhes902r7hkjxadtvbtm3', + 'http://example.com/cia5rhes902r8hkjx1su72qpn/cia5rhes902r9hkjxexh45oq0/cia5rhes902rahkjxah76ntxr/cia5rhes902rbhkjxnnfojf19', + 'http://example.com/cia5rhes902rchkjxc85n1zzu/cia5rhes902rdhkjxix5w6nkz/cia5rhes902rehkjxogcyyb50/cia5rhes902rfhkjx3r7glwov', + 'http://example.com/cia5rhes902rghkjxtck9dhwc/cia5rhes902rhhkjxru36hy4a/cia5rhes902rihkjxmmyc9tpx/cia5rhes902rjhkjxmbsypxaq', + 'http://example.com/cia5rhes902rkhkjx2fo040pu/cia5rhes902rlhkjxr65jltb9/cia5rhes902rmhkjxnzf86rqg/cia5rhes902rnhkjxca9gnhfv', + 'http://example.com/cia5rhes902rohkjx3t12qfew/cia5rhes902rphkjx0uy0q6x0/cia5rhes902rqhkjxytr1mozv/cia5rhes902rrhkjxti5cpfhq', + 'http://example.com/cia5rhes902rshkjxtzuesbvw/cia5rhes902rthkjx4imx7yq2/cia5rhes902ruhkjxv5rwbdfw/cia5rhes902rvhkjxx9dyruvh', + 'http://example.com/cia5rhes902rwhkjx80skj5fy/cia5rhes902rxhkjxs2roo0or/cia5rhes902ryhkjx0f0egqew/cia5rhes902rzhkjx2qyobgwd', + 'http://example.com/cia5rhes902s0hkjxwzjb0ibj/cia5rhes902s1hkjxthhdzgdb/cia5rhes902s2hkjxmp0am5hc/cia5rhes902s3hkjxou8fe0bw', + 'http://example.com/cia5rhes902s4hkjxy807y0wz/cia5rhes902s5hkjxyi0ucjpj/cia5rhes902s6hkjx57r4913i/cia5rhes902s7hkjx5zyg25co', + 'http://example.com/cia5rhes902s8hkjxtv0y9qsr/cia5rhes902s9hkjxmara3sln/cia5rhes902sahkjx16zbww31/cia5rhes902sbhkjxk3yfnqrf', + 'http://example.com/cia5rhes902schkjxmqs7wb8e/cia5rhes902sdhkjxbzqsikjf/cia5rhes902sehkjxifnkxd42/cia5rhes902sfhkjxeslnix9t', + 'http://example.com/cia5rhes902sghkjx9csqi025/cia5rhes902shhkjx03m41rdk/cia5rhes902sihkjx7o16p436/cia5rhes902sjhkjxuopqyoaf', + 'http://example.com/cia5rhes902skhkjxkj9lox0l/cia5rhes902slhkjx4siwdfz6/cia5rhes902smhkjxkz6smrk5/cia5rhes902snhkjxbydhx9sr', + 'http://example.com/cia5rhes902sohkjxmt7rn0m7/cia5rhes902sphkjxbr1rrero/cia5rhes902sqhkjxgsa5faxo/cia5rhes902srhkjxkaypi7hq', + 'http://example.com/cia5rhes902sshkjxcdabjgaq/cia5rhes902sthkjxdj1l2sdw/cia5rhes902suhkjx4w18whjz/cia5rhes902svhkjx00bsy24i', + 'http://example.com/cia5rhes902swhkjx1sxzd3bs/cia5rhes902sxhkjxwihbb32s/cia5rhes902syhkjxjv82ql1y/cia5rhes902szhkjxhx2p1tjw', + 'http://example.com/cia5rhes902t0hkjxlq1v45l8/cia5rhes902t1hkjxpcb65x6c/cia5rhes902t2hkjxqd79lp9t/cia5rhes902t3hkjxzlu0vgsq', + 'http://example.com/cia5rhes902t4hkjxchoh1xz9/cia5rhes902t5hkjxi7ja8w34/cia5rhes902t6hkjxcibihy5j/cia5rhes902t7hkjxzxhj2llf', + 'http://example.com/cia5rhes902t8hkjxe6kjteus/cia5rhes902t9hkjxct0osy9c/cia5rhes902tahkjxkpn37x26/cia5rhes902tbhkjxs1i3y06r', + 'http://example.com/cia5rhes902tchkjxkijsvrry/cia5rhes902tdhkjx478e7b15/cia5rhes902tehkjxe15r2zp0/cia5rhes902tfhkjx0xdr6u4g', + 'http://example.com/cia5rhes902tghkjxre727axs/cia5rhes902thhkjx8tjhkncn/cia5rhes902tihkjxfwe9moa8/cia5rhes902tjhkjxrw37is68', + 'http://example.com/cia5rhes902tkhkjx1vha7oxy/cia5rhes902tlhkjxorgrss4a/cia5rhes902tmhkjx5v2vjvpc/cia5rhes902tnhkjxe13xjwvn', + 'http://example.com/cia5rhes902tohkjxhh415ghg/cia5rhes902tphkjxewddudgl/cia5rhes902tqhkjxlt904su4/cia5rhes902trhkjxvox3ueb9', + 'http://example.com/cia5rhes902tshkjx565cdwgu/cia5rhes902tthkjx7v8dxnp1/cia5rhes902tuhkjx9lkhhc8x/cia5rhes902tvhkjxet30fwnm', + 'http://example.com/cia5rhes902twhkjx50zbd0gj/cia5rhes902txhkjxcxmjzp6i/cia5rhes902tyhkjx4wwog6sc/cia5rhes902tzhkjxl5k35m8y', + 'http://example.com/cia5rhes902u0hkjxmixf873e/cia5rhes902u1hkjxqkzx249g/cia5rhes902u2hkjxq1h6e73c/cia5rhes902u3hkjxy0raorlv', + 'http://example.com/cia5rhes902u4hkjxp7qu708r/cia5rhes902u5hkjxjq511roe/cia5rhes902u6hkjx24zsjlw7/cia5rhes902u7hkjxxao19ibw', + 'http://example.com/cia5rhes902u8hkjxjj8e4qjy/cia5rhes902u9hkjxnpfmyyee/cia5rhesa02uahkjx2cbqjqlj/cia5rhesa02ubhkjx2sb57ho9', + 'http://example.com/cia5rhesa02uchkjxi7stcunb/cia5rhesa02udhkjxf13m1va9/cia5rhesa02uehkjxbrbvzlts/cia5rhesa02ufhkjxv7c5sg8p', + 'http://example.com/cia5rhesa02ughkjxc4bg17mm/cia5rhesa02uhhkjx1fodi8bu/cia5rhesa02uihkjx1pgynm8w/cia5rhesa02ujhkjx21oibarf', + 'http://example.com/cia5rhesa02ukhkjx9l592wdv/cia5rhesa02ulhkjxvbp05nkt/cia5rhesa02umhkjxosf9qynb/cia5rhesa02unhkjx7bb91ukh', + 'http://example.com/cia5rhesa02uohkjxkss6ccme/cia5rhesa02uphkjxvd93brv7/cia5rhesa02uqhkjxoc61qqcx/cia5rhesa02urhkjxawfbw41u', + 'http://example.com/cia5rhesa02ushkjxgxz51hyw/cia5rhesa02uthkjxewu4vl7k/cia5rhesa02uuhkjxknapklva/cia5rhesa02uvhkjxmdf6weyv', + 'http://example.com/cia5rhesa02uwhkjx9egqxjsi/cia5rhesa02uxhkjxfzhkd5yr/cia5rhesa02uyhkjx8hv3p08k/cia5rhesa02uzhkjx45psji1y', + 'http://example.com/cia5rhesa02v0hkjx4l8lsl3u/cia5rhesa02v1hkjxngdy1ar6/cia5rhesa02v2hkjx6jo5h3qu/cia5rhesa02v3hkjxzbv3dpni', + 'http://example.com/cia5rhesa02v4hkjxtcyq3hrj/cia5rhesa02v5hkjxi1yvcnxy/cia5rhesa02v6hkjxw7v5871t/cia5rhesa02v7hkjxe8a1rpaz', + 'http://example.com/cia5rhesa02v8hkjx11e94e28/cia5rhesa02v9hkjxy79y9wsa/cia5rhesa02vahkjx4x1k6e7p/cia5rhesa02vbhkjx4nugadg0', + 'http://example.com/cia5rhesa02vchkjxyff973f9/cia5rhesa02vdhkjxxeylqp99/cia5rhesa02vehkjxup7kbh2i/cia5rhesa02vfhkjxvik2bnru', + 'http://example.com/cia5rhesa02vghkjxojpmez35/cia5rhesa02vhhkjxrsr1rbtw/cia5rhesa02vihkjxz51r21kh/cia5rhesa02vjhkjxnry87ysd', + 'http://example.com/cia5rhesa02vkhkjxc7kcgnod/cia5rhesa02vlhkjxou8csehx/cia5rhesa02vmhkjx4g46j5vv/cia5rhesa02vnhkjx0xdxordo', + 'http://example.com/cia5rhesa02vohkjxcpd7futv/cia5rhesa02vphkjxjhhvuq13/cia5rhesa02vqhkjx1jx0mwyq/cia5rhesa02vrhkjxatheqhre', + 'http://example.com/cia5rhesa02vshkjxhoxrm7du/cia5rhesa02vthkjxp3j2d7pl/cia5rhesa02vuhkjxfajs3kp2/cia5rhesa02vvhkjx094w7t5z', + 'http://example.com/cia5rhesa02vwhkjx8zsoc546/cia5rhesa02vxhkjxbbwesmgs/cia5rhesa02vyhkjxah7vbsl2/cia5rhesa02vzhkjxccc1osvb', + 'http://example.com/cia5rhesa02w0hkjxilmp1gcj/cia5rhesa02w1hkjxpax3mj4u/cia5rhesa02w2hkjxl4830fix/cia5rhesa02w3hkjxushwofrd', + 'http://example.com/cia5rhesa02w4hkjx0ayq3lna/cia5rhesa02w5hkjxtyfjinxi/cia5rhesa02w6hkjx6v3jk6np/cia5rhesa02w7hkjxb3kzmwfz', + 'http://example.com/cia5rhesa02w8hkjxpfztdog3/cia5rhesa02w9hkjxxu1jj9ro/cia5rhesa02wahkjx9x02t4s6/cia5rhesa02wbhkjxmudm4let', + 'http://example.com/cia5rhesa02wchkjxf8gwzm46/cia5rhesa02wdhkjxnogroqj5/cia5rhesa02wehkjxzcswjm19/cia5rhesa02wfhkjxd7sq70cn', + 'http://example.com/cia5rhesa02wghkjxzl71wo9i/cia5rhesa02whhkjx4qzdc4en/cia5rhesa02wihkjxqcwczavg/cia5rhesa02wjhkjx5v1mo7io', + 'http://example.com/cia5rhesa02wkhkjx17rwy1u4/cia5rhesa02wlhkjxupgzmlhz/cia5rhesa02wmhkjxy4guynpo/cia5rhesa02wnhkjx7hwclzmy', + 'http://example.com/cia5rhesa02wohkjxbhrfwyae/cia5rhesa02wphkjxg54k6a1v/cia5rhesa02wqhkjxxx6ovpcu/cia5rhesa02wrhkjx7chazbg3', + 'http://example.com/cia5rhesa02wshkjxsqxk0429/cia5rhesa02wthkjxjmhikrl3/cia5rhesa02wuhkjxojb6ebr1/cia5rhesa02wvhkjxrcv5ezdg', + 'http://example.com/cia5rhesa02wwhkjx6oc3w8ov/cia5rhesa02wxhkjxm2i72vec/cia5rhesa02wyhkjx3fh9ne9a/cia5rhesa02wzhkjx0971hhm1', + 'http://example.com/cia5rhesa02x0hkjxwenvo26l/cia5rhesa02x1hkjxtfilhs8a/cia5rhesa02x2hkjxpqvnoyqk/cia5rhesa02x3hkjx23vjztdc', + 'http://example.com/cia5rhesa02x4hkjxsgmx5os7/cia5rhesa02x5hkjxgrehs28q/cia5rhesa02x6hkjxvxtnze9l/cia5rhesa02x7hkjx0vlv9z1s', + 'http://example.com/cia5rhesa02x8hkjxge35kywm/cia5rhesa02x9hkjxw2tbefo5/cia5rhesa02xahkjxv137f9qt/cia5rhesa02xbhkjxnz9ep47k', + 'http://example.com/cia5rhesa02xchkjx00anlyr6/cia5rhesa02xdhkjx79zjud7w/cia5rhesa02xehkjxrb6rk7rw/cia5rhesa02xfhkjxphslyr6m', + 'http://example.com/cia5rhesa02xghkjxv656h0en/cia5rhesa02xhhkjxwt9sllti/cia5rhesa02xihkjxbblv9n51/cia5rhesa02xjhkjxoms9ldox', + 'http://example.com/cia5rhesa02xkhkjxb0ljdnru/cia5rhesa02xlhkjxuysb8km6/cia5rhesa02xmhkjx7sdb3ap1/cia5rhesa02xnhkjx556d8gld', + 'http://example.com/cia5rhesa02xohkjxmh07tx4r/cia5rhesa02xphkjxzekp4dcp/cia5rhesa02xqhkjxywdkdqe0/cia5rhesa02xrhkjxn2exhmk3', + 'http://example.com/cia5rhesa02xshkjxot0nxe5o/cia5rhesa02xthkjxxns5dc6l/cia5rhesa02xuhkjxppx37eq1/cia5rhesa02xvhkjxa0n1ft75', + 'http://example.com/cia5rhesa02xwhkjxdlz42y7u/cia5rhesa02xxhkjxyyrh6ehu/cia5rhesa02xyhkjxc2snloyu/cia5rhesa02xzhkjx7hniv0l2', + 'http://example.com/cia5rhesa02y0hkjx1ufllm5d/cia5rhesa02y1hkjxyciovmxi/cia5rhesa02y2hkjxeecnhafz/cia5rhesa02y3hkjxka899vl8', + 'http://example.com/cia5rhesa02y4hkjxas1scma5/cia5rhesa02y5hkjx7hx7cgry/cia5rhesa02y6hkjxt4r854cj/cia5rhesa02y7hkjxl2d58z7z', + 'http://example.com/cia5rhesa02y8hkjx6uvep3v0/cia5rhesa02y9hkjxzkox5acn/cia5rhesa02yahkjxbsttsd42/cia5rhesa02ybhkjx9s0eooqr', + 'http://example.com/cia5rhesa02ychkjxs38kr0ju/cia5rhesa02ydhkjxp7i0dm4v/cia5rhesa02yehkjxytdwg00m/cia5rhesa02yfhkjx6kojbk3h', + 'http://example.com/cia5rhesa02yghkjxu47g5rne/cia5rhesa02yhhkjxp5jiqzck/cia5rhesa02yihkjx9n0k7a86/cia5rhesa02yjhkjxawsmggmg', + 'http://example.com/cia5rhesa02ykhkjxpz5t3big/cia5rhesa02ylhkjxd7mv52ko/cia5rhesa02ymhkjxaq7e3qjp/cia5rhesa02ynhkjxx6n59eel', + 'http://example.com/cia5rhesa02yohkjx151ccq4m/cia5rhesa02yphkjxs6vfdnyb/cia5rhesa02yqhkjxakcfphvj/cia5rhesa02yrhkjxrtgqqlkl', + 'http://example.com/cia5rhesa02yshkjx5nthrj0p/cia5rhesa02ythkjxaoa6xfw1/cia5rhesa02yuhkjxsyk94k5s/cia5rhesa02yvhkjxp1fsbr6q', + 'http://example.com/cia5rhesa02ywhkjxxrjgdzm9/cia5rhesa02yxhkjxmt4liicf/cia5rhesa02yyhkjxqnavwf3w/cia5rhesa02yzhkjxwb6efk2q', + 'http://example.com/cia5rhesa02z0hkjxai4gwv4i/cia5rhesb02z1hkjxotmfrbh1/cia5rhesb02z2hkjxcorstega/cia5rhesb02z3hkjxmm6qrl72', + 'http://example.com/cia5rhesb02z4hkjx119h63fe/cia5rhesb02z5hkjx51zhx95d/cia5rhesb02z6hkjx13iaxgj7/cia5rhesb02z7hkjxnhttadyh', + 'http://example.com/cia5rhesb02z8hkjx7n279k7d/cia5rhesb02z9hkjxtyhtwvh4/cia5rhesb02zahkjxuxc8tnjw/cia5rhesb02zbhkjx9f5w1igd', + 'http://example.com/cia5rhesb02zchkjxu6u03gpq/cia5rhesb02zdhkjxad5fmie8/cia5rhesb02zehkjx82hi1ubw/cia5rhesb02zfhkjxlz014bc9', + 'http://example.com/cia5rhesb02zghkjxpcp41mh4/cia5rhesb02zhhkjxwtmgx1un/cia5rhesb02zihkjxvdlzs9gj/cia5rhesb02zjhkjxxnbuzjtx', + 'http://example.com/cia5rhesb02zkhkjxszpr5g1b/cia5rhesb02zlhkjx5r4u5x2d/cia5rhesb02zmhkjxj9k1c9lb/cia5rhesb02znhkjx76dnsetw', + 'http://example.com/cia5rhesb02zohkjxk9w5hbj0/cia5rhesb02zphkjxsz3yi7na/cia5rhesb02zqhkjxn35x1ss7/cia5rhesb02zrhkjxcmyedvkx', + 'http://example.com/cia5rhesb02zshkjxhuw9xl6g/cia5rhesb02zthkjxnkalu85l/cia5rhesb02zuhkjx0nb3kn0f/cia5rhesb02zvhkjxekzyryxq', + 'http://example.com/cia5rhesb02zwhkjxl2y2pyxt/cia5rhesb02zxhkjxeqoaa6v8/cia5rhesb02zyhkjxu5g8zso5/cia5rhesb02zzhkjx4t9www0y', + 'http://example.com/cia5rhesb0300hkjxdl9hwbgu/cia5rhesb0301hkjxed3e759g/cia5rhesb0302hkjxhsksjcb6/cia5rhesb0303hkjx3fn4nqpg', + 'http://example.com/cia5rhesb0304hkjx0j1w4t7r/cia5rhesb0305hkjx8v918aao/cia5rhesb0306hkjxvdvahvsq/cia5rhesb0307hkjxooa62xp7', + 'http://example.com/cia5rhesb0308hkjx33xkvxg7/cia5rhesb0309hkjxws0twzy4/cia5rhesb030ahkjxtpn8o3r0/cia5rhesb030bhkjxh3wusg8f', + 'http://example.com/cia5rhesb030chkjxt4rz9ksr/cia5rhesb030dhkjxklvupy26/cia5rhesb030ehkjxxf7wxi4e/cia5rhesb030fhkjxa11i0b7u', + 'http://example.com/cia5rhesb030ghkjxc1beup56/cia5rhesb030hhkjxxcgs2kpz/cia5rhesb030ihkjxc7a9g482/cia5rhesb030jhkjxjb51smqy', + 'http://example.com/cia5rhesb030khkjxu6a7pi1g/cia5rhesb030lhkjxo0qkosjn/cia5rhesb030mhkjxucjtxcjj/cia5rhesb030nhkjxbn92q469', + 'http://example.com/cia5rhesb030ohkjxalklya9k/cia5rhesb030phkjx2s6kj633/cia5rhesb030qhkjx5dus8zhl/cia5rhesb030rhkjxexszt1qv', + 'http://example.com/cia5rhesb030shkjxs325mfs5/cia5rhesb030thkjxhj36rw16/cia5rhesb030uhkjxzs6na9ek/cia5rhesb030vhkjxcahu5xdq', + 'http://example.com/cia5rhesb030whkjxbq6vx29f/cia5rhesb030xhkjxzcefm4gv/cia5rhesb030yhkjxcx0o6obs/cia5rhesb030zhkjxy6a654xp', + 'http://example.com/cia5rhesb0310hkjx74o7x2ol/cia5rhesb0311hkjxbaa60v2j/cia5rhesb0312hkjxws59xi68/cia5rhesb0313hkjxe024wl25', + 'http://example.com/cia5rhesb0314hkjxsmo6r7qy/cia5rhesb0315hkjxyo4vsu02/cia5rhesb0316hkjxq7bv4sl0/cia5rhesb0317hkjxnu8xcpds', + 'http://example.com/cia5rhesb0318hkjxfllzbwdp/cia5rhesb0319hkjx9jpu3qeb/cia5rhesb031ahkjx1yw2rxmr/cia5rhesb031bhkjx0kh6iq8e', + 'http://example.com/cia5rhesb031chkjxdjrdwexa/cia5rhesb031dhkjxpdkelmj0/cia5rhesb031ehkjxo0q4659i/cia5rhesb031fhkjxqj6po4aw', + 'http://example.com/cia5rhesb031ghkjxydbw4cnp/cia5rhesb031hhkjx7ak9k5ib/cia5rhesb031ihkjxshjg8guf/cia5rhesb031jhkjxgc7isxom', + 'http://example.com/cia5rhesb031khkjxilu3xhrq/cia5rhesb031lhkjx7qa2tmqv/cia5rhesb031mhkjx2gqx175d/cia5rhesb031nhkjxcclzxfj5', + 'http://example.com/cia5rhesb031ohkjxnqa5u0yu/cia5rhesb031phkjxvj65rgc0/cia5rhesb031qhkjxps94ct2m/cia5rhesb031rhkjx13vf7hqf', + 'http://example.com/cia5rhesb031shkjx4c0rxkqe/cia5rhesb031thkjx2f1rlhtc/cia5rhesb031uhkjxc6mmo6r9/cia5rhesb031vhkjxmkdbf7tz', + 'http://example.com/cia5rhesb031whkjx4wrcwah4/cia5rhesb031xhkjxjy564fgv/cia5rhesb031yhkjxel00ycv5/cia5rhesb031zhkjxwv42qge9', + 'http://example.com/cia5rhesb0320hkjx579rdytw/cia5rhesb0321hkjxqwktfaa3/cia5rhesb0322hkjx5uguvgm4/cia5rhesb0323hkjxod265vm7', + 'http://example.com/cia5rhesb0324hkjxo91kfm12/cia5rhesb0325hkjx7eeoo34p/cia5rhesb0326hkjxkcbju4fy/cia5rhesb0327hkjx9rgv9jej', + 'http://example.com/cia5rhesb0328hkjxu29htifz/cia5rhesb0329hkjx833v3icl/cia5rhesb032ahkjxp93q4nqo/cia5rhesb032bhkjx4tktxa61', + 'http://example.com/cia5rhesb032chkjxli18annx/cia5rhesb032dhkjxoin4rpsb/cia5rhesb032ehkjxkezkkq9n/cia5rhesb032fhkjxxq4syq15', + 'http://example.com/cia5rhesb032ghkjxjr48ia8g/cia5rhesb032hhkjxaz6zhm4c/cia5rhesb032ihkjxriefifyj/cia5rhesb032jhkjxt06hn2ix', + 'http://example.com/cia5rhesb032khkjxl0o2c8hq/cia5rhesb032lhkjxvlfg1dcu/cia5rhesb032mhkjxa6neghbc/cia5rhesb032nhkjxomcdlu3w', + 'http://example.com/cia5rhesb032ohkjxnrefhx6j/cia5rhesb032phkjx05xbd8mi/cia5rhesb032qhkjx22ncbb1j/cia5rhesb032rhkjx8mqw8vvb', + 'http://example.com/cia5rhesb032shkjxzw7wur7z/cia5rhesb032thkjxdybqu2ix/cia5rhesb032uhkjxjqrudsu0/cia5rhesb032vhkjx60p88zgu', + 'http://example.com/cia5rhesb032whkjxsj2cgd7r/cia5rhesb032xhkjxjv4oyt79/cia5rhesb032yhkjxlzlkj3x2/cia5rhesb032zhkjxhkvllyb6', + 'http://example.com/cia5rhesb0330hkjxhduykvat/cia5rhesb0331hkjxqg1x6769/cia5rhesb0332hkjx8scwhj5n/cia5rhesb0333hkjxous8ibmx', +]; diff --git a/dom/cache/test/mochitest/mochitest.ini b/dom/cache/test/mochitest/mochitest.ini index 07fa4f8229..3cfe425a5c 100644 --- a/dom/cache/test/mochitest/mochitest.ini +++ b/dom/cache/test/mochitest/mochitest.ini @@ -20,6 +20,8 @@ support-files = test_cache_requestCache.js test_cache_delete.js test_cache_put_reorder.js + test_cache_https.js + large_url_list.js [test_cache.html] [test_cache_add.html] @@ -33,3 +35,7 @@ support-files = [test_cache_requestCache.html] [test_cache_delete.html] [test_cache_put_reorder.html] +[test_cache_https.html] + skip-if = buildapp == 'b2g' # bug 1162353 +[test_cache_restart.html] +[test_cache_shrink.html] diff --git a/dom/cache/test/mochitest/test_cache_add.js b/dom/cache/test/mochitest/test_cache_add.js index 63a6242262..db65c7487f 100644 --- a/dom/cache/test/mochitest/test_cache_add.js +++ b/dom/cache/test/mochitest/test_cache_add.js @@ -10,10 +10,10 @@ caches.open(name).then(function(openCache) { cache = openCache; return cache.add('ftp://example.com/invalid' + context); }).catch(function (err) { - is(err.name, 'NetworkError', 'add() should throw NetworkError for invalid scheme'); + is(err.name, 'TypeError', 'add() should throw TypeError for invalid scheme'); return cache.addAll(['http://example.com/valid' + context, 'ftp://example.com/invalid' + context]); }).catch(function (err) { - is(err.name, 'NetworkError', 'addAll() should throw NetworkError for invalid scheme'); + is(err.name, 'TypeError', 'addAll() should throw TypeError for invalid scheme'); var promiseList = urlList.map(function(url) { return cache.match(url); }); diff --git a/dom/cache/test/mochitest/test_cache_https.html b/dom/cache/test/mochitest/test_cache_https.html new file mode 100644 index 0000000000..8ec509f0e8 --- /dev/null +++ b/dom/cache/test/mochitest/test_cache_https.html @@ -0,0 +1,20 @@ + + + + + Validate Interfaces Exposed to Workers + + + + + + + + + diff --git a/dom/cache/test/mochitest/test_cache_https.js b/dom/cache/test/mochitest/test_cache_https.js new file mode 100644 index 0000000000..1c1cfffebd --- /dev/null +++ b/dom/cache/test/mochitest/test_cache_https.js @@ -0,0 +1,23 @@ +var cache = null; +var name = 'https_' + context; +var urlBase = 'https://example.com/tests/dom/cache/test/mochitest'; +var url1 = urlBase + '/test_cache.js'; +var url2 = urlBase + '/test_cache_add.js'; + +caches.open(name).then(function(c) { + cache = c; + return cache.addAll([new Request(url1, { mode: 'no-cors' }), + new Request(url2, { mode: 'no-cors' })]); +}).then(function() { + return cache.delete(url1); +}).then(function(result) { + ok(result, 'Cache entry should be deleted'); + return cache.delete(url2); +}).then(function(result) { + ok(result, 'Cache entry should be deleted'); + cache = null; + return caches.delete(name); +}).then(function(result) { + ok(result, 'Cache should be deleted'); + testDone(); +}); diff --git a/dom/cache/test/mochitest/test_cache_matchAll_request.js b/dom/cache/test/mochitest/test_cache_matchAll_request.js index 7c3aa95521..0cc7298589 100644 --- a/dom/cache/test/mochitest/test_cache_matchAll_request.js +++ b/dom/cache/test/mochitest/test_cache_matchAll_request.js @@ -74,6 +74,31 @@ function testRequest(request1, request2, request3, unknownRequest, }).then(function(r) { is(r.length, 1, "Should only find 1 item"); return checkResponse(r[0], response1, response1Text); + }).then(function() { + return c.matchAll(new Request(request1, {method: "HEAD"})); + }).then(function(r) { + is(r.length, 1, "Should only find 1 item"); + return checkResponse(r[0], response1, ""); + }).then(function() { + return c.matchAll(new Request(request1, {method: "HEAD"}), {ignoreMethod: true}); + }).then(function(r) { + is(r.length, 1, "Should only find 1 item"); + return checkResponse(r[0], response1, response1Text); + }).then(function() { + return Promise.all( + ["POST", "PUT", "DELETE", "OPTIONS"] + .map(function(method) { + var req = new Request(request1, {method: method}); + return c.matchAll(req) + .then(function(r) { + is(r.length, 0, "Searching for a request with a non-GET/HEAD method should not succeed"); + return c.matchAll(req, {ignoreMethod: true}); + }).then(function(r) { + is(r.length, 1, "Should only find 1 item"); + return checkResponse(r[0], response1, response1Text); + }); + }) + ); }).then(function() { return c.matchAll(requestWithDifferentFragment); }).then(function(r) { diff --git a/dom/cache/test/mochitest/test_cache_match_request.js b/dom/cache/test/mochitest/test_cache_match_request.js index 128d6384cc..85e67500ff 100644 --- a/dom/cache/test/mochitest/test_cache_match_request.js +++ b/dom/cache/test/mochitest/test_cache_match_request.js @@ -6,7 +6,10 @@ var c; var responseText; var name = "match-request" + context; -function checkResponse(r) { +function checkResponse(r, expectedBody) { + if (expectedBody === undefined) { + expectedBody = responseText; + } ok(r !== response, "The objects should not be the same"); is(r.url, response.url.replace("#fragment", ""), "The URLs should be the same"); @@ -17,7 +20,7 @@ function checkResponse(r) { "Both responses should have the same status text"); return r.text().then(function(text) { // Avoid dumping out the large response text to the log if they're equal. - if (text !== responseText) { + if (text !== expectedBody) { is(text, responseText, "The response body should be correct"); } }); @@ -60,6 +63,28 @@ function testRequest(request, unknownRequest, requestWithAlternateQueryString, return c.match(request); }).then(function(r) { return checkResponse(r); + }).then(function() { + return c.match(new Request(request, {method: "HEAD"})); + }).then(function(r) { + return checkResponse(r, ''); + }).then(function() { + return c.match(new Request(request, {method: "HEAD"}), {ignoreMethod: true}); + }).then(function(r) { + return checkResponse(r); + }).then(function() { + return Promise.all( + ["POST", "PUT", "DELETE", "OPTIONS"] + .map(function(method) { + var req = new Request(request, {method: method}); + return c.match(req) + .then(function(r) { + is(typeof r, "undefined", "Searching for a request with a non-GET/HEAD method should not succeed"); + return c.match(req, {ignoreMethod: true}); + }).then(function(r) { + return checkResponse(r); + }); + }) + ); }).then(function() { return caches.match(request); }).then(function(r) { diff --git a/dom/cache/test/mochitest/test_cache_shrink.html b/dom/cache/test/mochitest/test_cache_shrink.html new file mode 100644 index 0000000000..56b3d8d3e3 --- /dev/null +++ b/dom/cache/test/mochitest/test_cache_shrink.html @@ -0,0 +1,131 @@ + + + + + Test Cache with QuotaManager Restart + + + + + + + + diff --git a/dom/fetch/ChannelInfo.cpp b/dom/fetch/ChannelInfo.cpp new file mode 100644 index 0000000000..91f64402d7 --- /dev/null +++ b/dom/fetch/ChannelInfo.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 "mozilla/dom/ChannelInfo.h" +#include "nsCOMPtr.h" +#include "nsIChannel.h" +#include "nsIHttpChannel.h" +#include "nsSerializationHelper.h" +#include "mozilla/net/HttpBaseChannel.h" +#include "mozilla/ipc/ChannelInfo.h" +#include "nsIJARChannel.h" +#include "nsJARChannel.h" + +using namespace mozilla; +using namespace mozilla::dom; + +void +ChannelInfo::InitFromChannel(nsIChannel* aChannel) +{ + MOZ_ASSERT(!mInited, "Cannot initialize the object twice"); + + nsCOMPtr securityInfo; + aChannel->GetSecurityInfo(getter_AddRefs(securityInfo)); + if (securityInfo) { + SetSecurityInfo(securityInfo); + } + + mInited = true; +} + +void +ChannelInfo::InitFromIPCChannelInfo(const ipc::IPCChannelInfo& aChannelInfo) +{ + MOZ_ASSERT(!mInited, "Cannot initialize the object twice"); + + mSecurityInfo = aChannelInfo.securityInfo(); + + mInited = true; +} + +void +ChannelInfo::SetSecurityInfo(nsISupports* aSecurityInfo) +{ + MOZ_ASSERT(mSecurityInfo.IsEmpty(), "security info should only be set once"); + nsCOMPtr serializable = do_QueryInterface(aSecurityInfo); + if (!serializable) { + NS_WARNING("A non-serializable object was passed to InternalResponse::SetSecurityInfo"); + return; + } + NS_SerializeToString(serializable, mSecurityInfo); +} + +nsresult +ChannelInfo::ResurrectInfoOnChannel(nsIChannel* aChannel) +{ + MOZ_ASSERT(mInited); + + if (!mSecurityInfo.IsEmpty()) { + nsCOMPtr infoObj; + nsresult rv = NS_DeserializeObject(mSecurityInfo, getter_AddRefs(infoObj)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + nsCOMPtr httpChannel = + do_QueryInterface(aChannel); + if (httpChannel) { + net::HttpBaseChannel* httpBaseChannel = + static_cast(httpChannel.get()); + rv = httpBaseChannel->OverrideSecurityInfo(infoObj); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + nsCOMPtr jarChannel = + do_QueryInterface(aChannel); + if (NS_WARN_IF(!jarChannel)) { + return NS_ERROR_FAILURE; + } + static_cast(jarChannel.get())-> + OverrideSecurityInfo(infoObj); + } + } + + return NS_OK; +} + +ipc::IPCChannelInfo +ChannelInfo::AsIPCChannelInfo() const +{ + // This may be called when mInited is false, for example if we try to store + // a synthesized Response object into the Cache. Uninitialized and empty + // ChannelInfo objects are indistinguishable at the IPC level, so this is + // fine. + + IPCChannelInfo ipcInfo; + + ipcInfo.securityInfo() = mSecurityInfo; + + return ipcInfo; +} diff --git a/dom/fetch/ChannelInfo.h b/dom/fetch/ChannelInfo.h new file mode 100644 index 0000000000..f365fdf0fc --- /dev/null +++ b/dom/fetch/ChannelInfo.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_ChannelInfo_h +#define mozilla_dom_ChannelInfo_h + +#include "nsString.h" + +class nsIChannel; + +namespace mozilla { +namespace ipc { +class IPCChannelInfo; +} // namespace ipc + +namespace dom { + +// This class represents the information related to a Response that we +// retrieve from the corresponding channel that is used to perform the fetch. +// +// When adding new members to this object, the following code needs to be +// updated: +// * IPCChannelInfo +// * InitFromChannel and InitFromIPCChannelInfo members +// * ResurrectInfoOnChannel member +// * AsIPCChannelInfo member +// * constructors and assignment operators for this class. +// * DOM Cache schema code (in dom/cache/DBSchema.cpp) to ensure that the newly +// added member is saved into the DB and loaded from it properly. +// +// Care must be taken when initializing this object, or when calling +// ResurrectInfoOnChannel(). This object cannot be initialized twice, and +// ResurrectInfoOnChannel() cannot be called on it before it has been +// initialized. There are assertions ensuring these invariants. +class ChannelInfo final +{ +public: + typedef mozilla::ipc::IPCChannelInfo IPCChannelInfo; + + ChannelInfo() + : mInited(false) + { + } + + ChannelInfo(const ChannelInfo& aRHS) + : mSecurityInfo(aRHS.mSecurityInfo) + , mInited(aRHS.mInited) + { + } + + ChannelInfo& + operator=(const ChannelInfo& aRHS) + { + mSecurityInfo = aRHS.mSecurityInfo; + mInited = aRHS.mInited; + return *this; + } + + void InitFromChannel(nsIChannel* aChannel); + void InitFromIPCChannelInfo(const IPCChannelInfo& aChannelInfo); + + // This restores every possible information stored from a previous channel + // object on a new one. + nsresult ResurrectInfoOnChannel(nsIChannel* aChannel); + + bool IsInitialized() const + { + return mInited; + } + + IPCChannelInfo AsIPCChannelInfo() const; + +private: + void SetSecurityInfo(nsISupports* aSecurityInfo); + +private: + nsCString mSecurityInfo; + bool mInited; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ChannelInfo_h diff --git a/dom/fetch/ChannelInfo.ipdlh b/dom/fetch/ChannelInfo.ipdlh new file mode 100644 index 0000000000..605009e120 --- /dev/null +++ b/dom/fetch/ChannelInfo.ipdlh @@ -0,0 +1,14 @@ +/* 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/. */ + +namespace mozilla { +namespace ipc { + +struct IPCChannelInfo +{ + nsCString securityInfo; +}; + +} // namespace ipc +} // namespace mozilla diff --git a/dom/fetch/FetchDriver.cpp b/dom/fetch/FetchDriver.cpp index 2043009803..2676e9d425 100644 --- a/dom/fetch/FetchDriver.cpp +++ b/dom/fetch/FetchDriver.cpp @@ -704,12 +704,8 @@ FetchDriver::OnStartRequest(nsIRequest* aRequest, } response->SetBody(pipeInputStream); - nsCOMPtr securityInfo; nsCOMPtr channel = do_QueryInterface(aRequest); - rv = channel->GetSecurityInfo(getter_AddRefs(securityInfo)); - if (securityInfo) { - response->SetSecurityInfo(securityInfo); - } + response->InitChannelInfo(channel); // Resolves fetch() promise which may trigger code running in a worker. Make // sure the Response is fully initialized before calling this. diff --git a/dom/fetch/InternalResponse.cpp b/dom/fetch/InternalResponse.cpp index 6ee065c7bc..2791a013e2 100644 --- a/dom/fetch/InternalResponse.cpp +++ b/dom/fetch/InternalResponse.cpp @@ -8,7 +8,6 @@ #include "mozilla/dom/InternalHeaders.h" #include "nsStreamUtils.h" -#include "nsSerializationHelper.h" namespace mozilla { namespace dom { @@ -75,24 +74,5 @@ InternalResponse::CORSResponse() return cors.forget(); } -void -InternalResponse::SetSecurityInfo(nsISupports* aSecurityInfo) -{ - MOZ_ASSERT(mSecurityInfo.IsEmpty(), "security info should only be set once"); - nsCOMPtr serializable = do_QueryInterface(aSecurityInfo); - if (!serializable) { - NS_WARNING("A non-serializable object was passed to InternalResponse::SetSecurityInfo"); - return; - } - NS_SerializeToString(serializable, mSecurityInfo); -} - -void -InternalResponse::SetSecurityInfo(const nsCString& aSecurityInfo) -{ - MOZ_ASSERT(mSecurityInfo.IsEmpty(), "security info should only be set once"); - mSecurityInfo = aSecurityInfo; -} - } // namespace dom } // namespace mozilla diff --git a/dom/fetch/InternalResponse.h b/dom/fetch/InternalResponse.h index 0d5203da8a..9b88de778c 100644 --- a/dom/fetch/InternalResponse.h +++ b/dom/fetch/InternalResponse.h @@ -11,6 +11,7 @@ #include "nsISupportsImpl.h" #include "mozilla/dom/ResponseBinding.h" +#include "mozilla/dom/ChannelInfo.h" namespace mozilla { namespace dom { @@ -48,7 +49,7 @@ public: response->mTerminationReason = mTerminationReason; response->mURL = mURL; response->mFinalURL = mFinalURL; - response->mSecurityInfo = mSecurityInfo; + response->mChannelInfo = mChannelInfo; response->mWrappedResponse = this; return response.forget(); } @@ -156,17 +157,29 @@ public: mBody = aBody; } - const nsCString& - GetSecurityInfo() const + void + InitChannelInfo(nsIChannel* aChannel) { - return mSecurityInfo; + mChannelInfo.InitFromChannel(aChannel); } void - SetSecurityInfo(nsISupports* aSecurityInfo); + InitChannelInfo(const mozilla::ipc::IPCChannelInfo& aChannelInfo) + { + mChannelInfo.InitFromIPCChannelInfo(aChannelInfo); + } void - SetSecurityInfo(const nsCString& aSecurityInfo); + InitChannelInfo(const ChannelInfo& aChannelInfo) + { + mChannelInfo = aChannelInfo; + } + + const ChannelInfo& + GetChannelInfo() const + { + return mChannelInfo; + } private: ~InternalResponse() @@ -185,7 +198,7 @@ private: copy->mTerminationReason = mTerminationReason; copy->mURL = mURL; copy->mFinalURL = mFinalURL; - copy->mSecurityInfo = mSecurityInfo; + copy->mChannelInfo = mChannelInfo; return copy.forget(); } @@ -197,7 +210,7 @@ private: const nsCString mStatusText; nsRefPtr mHeaders; nsCOMPtr mBody; - nsCString mSecurityInfo; + ChannelInfo mChannelInfo; // For filtered responses. // Cache, and SW interception should always serialize/access the underlying diff --git a/dom/fetch/Response.h b/dom/fetch/Response.h index 87dc9b5b4c..5692d70eae 100644 --- a/dom/fetch/Response.h +++ b/dom/fetch/Response.h @@ -13,13 +13,13 @@ #include "mozilla/dom/Fetch.h" #include "mozilla/dom/ResponseBinding.h" +#include "InternalHeaders.h" #include "InternalResponse.h" namespace mozilla { namespace dom { class Headers; -class InternalHeaders; class Response final : public nsISupports , public FetchBody @@ -78,10 +78,16 @@ public: return mInternalResponse->Headers(); } - const nsCString& - GetSecurityInfo() const + void + InitChannelInfo(nsIChannel* aChannel) { - return mInternalResponse->GetSecurityInfo(); + mInternalResponse->InitChannelInfo(aChannel); + } + + const ChannelInfo& + GetChannelInfo() const + { + return mInternalResponse->GetChannelInfo(); } Headers* Headers_(); diff --git a/dom/fetch/moz.build b/dom/fetch/moz.build index 316f84b1f1..20133500dd 100644 --- a/dom/fetch/moz.build +++ b/dom/fetch/moz.build @@ -5,6 +5,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. EXPORTS.mozilla.dom += [ + 'ChannelInfo.h', 'Fetch.h', 'FetchDriver.h', 'Headers.h', @@ -16,6 +17,7 @@ EXPORTS.mozilla.dom += [ ] UNIFIED_SOURCES += [ + 'ChannelInfo.cpp', 'Fetch.cpp', 'FetchDriver.cpp', 'Headers.cpp', @@ -26,11 +28,23 @@ UNIFIED_SOURCES += [ 'Response.cpp', ] +IPDL_SOURCES += [ + 'ChannelInfo.ipdlh', +] + LOCAL_INCLUDES += [ '../workers', + # For nsJARChannel.h + '/modules/libjar', + # For HttpBaseChannel.h dependencies + '/netwerk/base', # For nsDataHandler.h '/netwerk/protocol/data', + # For HttpBaseChannel.h + '/netwerk/protocol/http', ] FAIL_ON_WARNINGS = True FINAL_LIBRARY = 'xul' + +include('/ipc/chromium/chromium-config.mozbuild') diff --git a/dom/promise/Promise.cpp b/dom/promise/Promise.cpp index 771ba518ea..60737dd9bd 100644 --- a/dom/promise/Promise.cpp +++ b/dom/promise/Promise.cpp @@ -955,6 +955,26 @@ NS_IMPL_CYCLE_COLLECTION(AllResolveHandler, mCountdownHolder) /* static */ already_AddRefed Promise::All(const GlobalObject& aGlobal, const Sequence& aIterable, ErrorResult& aRv) +{ + JSContext* cx = aGlobal.Context(); + + nsTArray> promiseList; + + for (uint32_t i = 0; i < aIterable.Length(); ++i) { + JS::Rooted value(cx, aIterable.ElementAt(i)); + nsRefPtr nextPromise = Promise::Resolve(aGlobal, value, aRv); + + MOZ_ASSERT(!aRv.Failed()); + + promiseList.AppendElement(Move(nextPromise)); + } + + return Promise::All(aGlobal, promiseList, aRv); +} + +/* static */ already_AddRefed +Promise::All(const GlobalObject& aGlobal, + const nsTArray>& aPromiseList, ErrorResult& aRv) { nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); @@ -965,7 +985,7 @@ Promise::All(const GlobalObject& aGlobal, JSContext* cx = aGlobal.Context(); - if (aIterable.Length() == 0) { + if (aPromiseList.IsEmpty()) { JS::Rooted empty(cx, JS_NewArrayObject(cx, 0)); if (!empty) { aRv.Throw(NS_ERROR_OUT_OF_MEMORY); @@ -982,7 +1002,7 @@ Promise::All(const GlobalObject& aGlobal, return nullptr; } nsRefPtr holder = - new CountdownHolder(aGlobal, promise, aIterable.Length()); + new CountdownHolder(aGlobal, promise, aPromiseList.Length()); JS::Rooted obj(cx, JS::CurrentGlobalOrNull(cx)); if (!obj) { @@ -992,20 +1012,16 @@ Promise::All(const GlobalObject& aGlobal, nsRefPtr rejectCb = new RejectPromiseCallback(promise, obj); - for (uint32_t i = 0; i < aIterable.Length(); ++i) { - JS::Rooted value(cx, aIterable.ElementAt(i)); - nsRefPtr nextPromise = Promise::Resolve(aGlobal, value, aRv); - - MOZ_ASSERT(!aRv.Failed()); - + for (uint32_t i = 0; i < aPromiseList.Length(); ++i) { nsRefPtr resolveHandler = new AllResolveHandler(holder, i); nsRefPtr resolveCb = new NativePromiseCallback(resolveHandler, Resolved); + // Every promise gets its own resolve callback, which will set the right // index in the array to the resolution value. - nextPromise->AppendCallbacks(resolveCb, rejectCb); + aPromiseList[i]->AppendCallbacks(resolveCb, rejectCb); } return promise.forget(); diff --git a/dom/promise/Promise.h b/dom/promise/Promise.h index cf876645c5..860e99c1cf 100644 --- a/dom/promise/Promise.h +++ b/dom/promise/Promise.h @@ -187,6 +187,10 @@ public: All(const GlobalObject& aGlobal, const Sequence& aIterable, ErrorResult& aRv); + static already_AddRefed + All(const GlobalObject& aGlobal, + const nsTArray>& aPromiseList, ErrorResult& aRv); + static already_AddRefed Race(const GlobalObject& aGlobal, const Sequence& aIterable, ErrorResult& aRv); diff --git a/dom/webidl/Cache.webidl b/dom/webidl/Cache.webidl index 099701d2bd..7df69a8f6e 100644 --- a/dom/webidl/Cache.webidl +++ b/dom/webidl/Cache.webidl @@ -33,7 +33,6 @@ dictionary CacheQueryOptions { boolean ignoreSearch = false; boolean ignoreMethod = false; boolean ignoreVary = false; -boolean prefixMatch = false; DOMString cacheName; }; diff --git a/dom/workers/ScriptLoader.cpp b/dom/workers/ScriptLoader.cpp index 7387ac330a..435910c567 100644 --- a/dom/workers/ScriptLoader.cpp +++ b/dom/workers/ScriptLoader.cpp @@ -14,7 +14,6 @@ #include "nsIIOService.h" #include "nsIProtocolHandler.h" #include "nsIScriptSecurityManager.h" -#include "nsISerializable.h" #include "nsIStreamLoader.h" #include "nsIStreamListenerTee.h" #include "nsIThreadRetargetableRequest.h" @@ -422,7 +421,7 @@ private: bool mFailed; nsCOMPtr mPump; nsCOMPtr mBaseURI; - nsCString mSecurityInfo; + ChannelInfo mChannelInfo; }; NS_IMPL_ISUPPORTS(CacheScriptLoader, nsIStreamLoaderObserver) @@ -589,19 +588,9 @@ private: new InternalResponse(200, NS_LITERAL_CSTRING("OK")); ir->SetBody(mReader); - // Set the security info of the channel on the response so that it's + // Set the channel info of the channel on the response so that it's // saved in the cache. - nsCOMPtr infoObj; - channel->GetSecurityInfo(getter_AddRefs(infoObj)); - if (infoObj) { - nsCOMPtr serializable = do_QueryInterface(infoObj); - if (serializable) { - ir->SetSecurityInfo(serializable); - MOZ_ASSERT(!ir->GetSecurityInfo().IsEmpty()); - } else { - NS_WARNING("A non-serializable object was obtained from nsIChannel::GetSecurityInfo()!"); - } - } + ir->InitChannelInfo(channel); nsRefPtr response = new Response(mCacheCreator->Global(), ir); @@ -983,18 +972,9 @@ private: // Take care of the base URI first. mWorkerPrivate->SetBaseURI(finalURI); - // Store the security info if needed. + // Store the channel info if needed. if (mWorkerPrivate->IsServiceWorker()) { - nsCOMPtr infoObj; - channel->GetSecurityInfo(getter_AddRefs(infoObj)); - if (infoObj) { - nsCOMPtr serializable = do_QueryInterface(infoObj); - if (serializable) { - mWorkerPrivate->SetSecurityInfo(serializable); - } else { - NS_WARNING("A non-serializable object was obtained from nsIChannel::GetSecurityInfo()!"); - } - } + mWorkerPrivate->InitChannelInfo(channel); } // Now to figure out which principal to give this worker. @@ -1065,7 +1045,8 @@ private: void DataReceivedFromCache(uint32_t aIndex, const uint8_t* aString, - uint32_t aStringLen, const nsCString& aSecurityInfo) + uint32_t aStringLen, + const ChannelInfo& aChannelInfo) { AssertIsOnMainThread(); MOZ_ASSERT(aIndex < mLoadInfos.Length()); @@ -1093,7 +1074,7 @@ private: MOZ_ASSERT(principal); nsILoadGroup* loadGroup = mWorkerPrivate->GetLoadGroup(); MOZ_ASSERT(loadGroup); - mWorkerPrivate->SetSecurityInfo(aSecurityInfo); + mWorkerPrivate->InitChannelInfo(aChannelInfo); // Needed to initialize the principal info. This is fine because // the cache principal cannot change, unlike the channel principal. mWorkerPrivate->SetPrincipal(principal, loadGroup); @@ -1447,11 +1428,11 @@ CacheScriptLoader::ResolvedCallback(JSContext* aCx, nsCOMPtr inputStream; response->GetBody(getter_AddRefs(inputStream)); - mSecurityInfo = response->GetSecurityInfo(); + mChannelInfo = response->GetChannelInfo(); if (!inputStream) { mLoadInfo.mCacheStatus = ScriptLoadInfo::Cached; - mRunnable->DataReceivedFromCache(mIndex, (uint8_t*)"", 0, mSecurityInfo); + mRunnable->DataReceivedFromCache(mIndex, (uint8_t*)"", 0, mChannelInfo); return; } @@ -1507,7 +1488,7 @@ CacheScriptLoader::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aCont mLoadInfo.mCacheStatus = ScriptLoadInfo::Cached; - mRunnable->DataReceivedFromCache(mIndex, aString, aStringLen, mSecurityInfo); + mRunnable->DataReceivedFromCache(mIndex, aString, aStringLen, mChannelInfo); return NS_OK; } diff --git a/dom/workers/ServiceWorkerEvents.cpp b/dom/workers/ServiceWorkerEvents.cpp index 4c8d05e232..2d1f6013e7 100644 --- a/dom/workers/ServiceWorkerEvents.cpp +++ b/dom/workers/ServiceWorkerEvents.cpp @@ -98,14 +98,14 @@ class FinishResponse final : public nsRunnable { nsMainThreadPtrHandle mChannel; nsRefPtr mInternalResponse; - nsCString mWorkerSecurityInfo; + ChannelInfo mWorkerChannelInfo; public: FinishResponse(nsMainThreadPtrHandle& aChannel, InternalResponse* aInternalResponse, - const nsCString& aWorkerSecurityInfo) + const ChannelInfo& aWorkerChannelInfo) : mChannel(aChannel) , mInternalResponse(aInternalResponse) - , mWorkerSecurityInfo(aWorkerSecurityInfo) + , mWorkerChannelInfo(aWorkerChannelInfo) { } @@ -114,19 +114,17 @@ public: { AssertIsOnMainThread(); - nsCOMPtr infoObj; - nsAutoCString securityInfo(mInternalResponse->GetSecurityInfo()); - if (securityInfo.IsEmpty()) { + ChannelInfo channelInfo; + if (mInternalResponse->GetChannelInfo().IsInitialized()) { + channelInfo = mInternalResponse->GetChannelInfo(); + } else { // We are dealing with a synthesized response here, so fall back to the - // security info for the worker script. - securityInfo = mWorkerSecurityInfo; + // channel info for the worker script. + channelInfo = mWorkerChannelInfo; } - nsresult rv = NS_DeserializeObject(securityInfo, getter_AddRefs(infoObj)); - if (NS_SUCCEEDED(rv)) { - rv = mChannel->SetSecurityInfo(infoObj); - if (NS_WARN_IF(NS_FAILED(rv))) { - return rv; - } + nsresult rv = mChannel->SetChannelInfo(&channelInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; } mChannel->SynthesizeStatus(mInternalResponse->GetStatus(), mInternalResponse->GetStatusText()); @@ -169,14 +167,14 @@ struct RespondWithClosure { nsMainThreadPtrHandle mInterceptedChannel; nsRefPtr mInternalResponse; - nsCString mWorkerSecurityInfo; + ChannelInfo mWorkerChannelInfo; RespondWithClosure(nsMainThreadPtrHandle& aChannel, InternalResponse* aInternalResponse, - const nsCString& aWorkerSecurityInfo) + const ChannelInfo& aWorkerChannelInfo) : mInterceptedChannel(aChannel) , mInternalResponse(aInternalResponse) - , mWorkerSecurityInfo(aWorkerSecurityInfo) + , mWorkerChannelInfo(aWorkerChannelInfo) { } }; @@ -188,7 +186,7 @@ void RespondWithCopyComplete(void* aClosure, nsresult aStatus) if (NS_SUCCEEDED(aStatus)) { event = new FinishResponse(data->mInterceptedChannel, data->mInternalResponse, - data->mWorkerSecurityInfo); + data->mWorkerChannelInfo); } else { event = new CancelChannelRunnable(data->mInterceptedChannel); } @@ -254,7 +252,7 @@ RespondWithHandler::ResolvedCallback(JSContext* aCx, JS::Handle aValu worker->AssertIsOnWorkerThread(); nsAutoPtr closure( - new RespondWithClosure(mInterceptedChannel, ir, worker->GetSecurityInfo())); + new RespondWithClosure(mInterceptedChannel, ir, worker->GetChannelInfo())); nsCOMPtr body; response->GetBody(getter_AddRefs(body)); // Errors and redirects may not have a body. diff --git a/dom/workers/ServiceWorkerManager.cpp b/dom/workers/ServiceWorkerManager.cpp index f96875b4dd..2c171a902c 100644 --- a/dom/workers/ServiceWorkerManager.cpp +++ b/dom/workers/ServiceWorkerManager.cpp @@ -16,6 +16,7 @@ #include "nsIHttpChannel.h" #include "nsIHttpChannelInternal.h" #include "nsIHttpHeaderVisitor.h" +#include "nsIJARChannel.h" #include "nsINetworkInterceptController.h" #include "nsIMutableArray.h" #include "nsIUploadChannel2.h" @@ -3210,60 +3211,70 @@ public: rv = uri->GetSpec(mSpec); NS_ENSURE_SUCCESS(rv, rv); - nsCOMPtr httpChannel = do_QueryInterface(channel); - NS_ENSURE_TRUE(httpChannel, NS_ERROR_NOT_AVAILABLE); - - rv = httpChannel->GetRequestMethod(mMethod); - NS_ENSURE_SUCCESS(rv, rv); - uint32_t loadFlags; rv = channel->GetLoadFlags(&loadFlags); NS_ENSURE_SUCCESS(rv, rv); - nsCOMPtr internalChannel = do_QueryInterface(httpChannel); - NS_ENSURE_TRUE(internalChannel, NS_ERROR_NOT_AVAILABLE); - - uint32_t mode; - internalChannel->GetCorsMode(&mode); - switch (mode) { - case nsIHttpChannelInternal::CORS_MODE_SAME_ORIGIN: - mRequestMode = RequestMode::Same_origin; - break; - case nsIHttpChannelInternal::CORS_MODE_NO_CORS: - mRequestMode = RequestMode::No_cors; - break; - case nsIHttpChannelInternal::CORS_MODE_CORS: - case nsIHttpChannelInternal::CORS_MODE_CORS_WITH_FORCED_PREFLIGHT: - mRequestMode = RequestMode::Cors; - break; - default: - MOZ_CRASH("Unexpected CORS mode"); - } - - if (loadFlags & nsIRequest::LOAD_ANONYMOUS) { - mRequestCredentials = RequestCredentials::Omit; - } else { - bool includeCrossOrigin; - internalChannel->GetCorsIncludeCredentials(&includeCrossOrigin); - if (includeCrossOrigin) { - mRequestCredentials = RequestCredentials::Include; - } - } - - rv = httpChannel->VisitRequestHeaders(this); - NS_ENSURE_SUCCESS(rv, rv); - nsCOMPtr loadInfo; rv = channel->GetLoadInfo(getter_AddRefs(loadInfo)); NS_ENSURE_SUCCESS(rv, rv); mContentPolicyType = loadInfo->InternalContentPolicyType(); - nsCOMPtr uploadChannel = do_QueryInterface(httpChannel); - if (uploadChannel) { - MOZ_ASSERT(!mUploadStream); - rv = uploadChannel->CloneUploadStream(getter_AddRefs(mUploadStream)); + nsCOMPtr httpChannel = do_QueryInterface(channel); + if (httpChannel) { + rv = httpChannel->GetRequestMethod(mMethod); NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr internalChannel = do_QueryInterface(httpChannel); + NS_ENSURE_TRUE(internalChannel, NS_ERROR_NOT_AVAILABLE); + + uint32_t mode; + internalChannel->GetCorsMode(&mode); + switch (mode) { + case nsIHttpChannelInternal::CORS_MODE_SAME_ORIGIN: + mRequestMode = RequestMode::Same_origin; + break; + case nsIHttpChannelInternal::CORS_MODE_NO_CORS: + mRequestMode = RequestMode::No_cors; + break; + case nsIHttpChannelInternal::CORS_MODE_CORS: + case nsIHttpChannelInternal::CORS_MODE_CORS_WITH_FORCED_PREFLIGHT: + mRequestMode = RequestMode::Cors; + break; + default: + MOZ_CRASH("Unexpected CORS mode"); + } + + if (loadFlags & nsIRequest::LOAD_ANONYMOUS) { + mRequestCredentials = RequestCredentials::Omit; + } else { + bool includeCrossOrigin; + internalChannel->GetCorsIncludeCredentials(&includeCrossOrigin); + if (includeCrossOrigin) { + mRequestCredentials = RequestCredentials::Include; + } + } + + rv = httpChannel->VisitRequestHeaders(this); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr uploadChannel = do_QueryInterface(httpChannel); + if (uploadChannel) { + MOZ_ASSERT(!mUploadStream); + rv = uploadChannel->CloneUploadStream(getter_AddRefs(mUploadStream)); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + nsCOMPtr jarChannel = do_QueryInterface(channel); + // If it is not an HTTP channel it must be a JAR one. + NS_ENSURE_TRUE(jarChannel, NS_ERROR_NOT_AVAILABLE); + + mMethod = "GET"; + + if (loadFlags & nsIRequest::LOAD_ANONYMOUS) { + mRequestCredentials = RequestCredentials::Omit; + } } return NS_OK; diff --git a/dom/workers/ServiceWorkerScriptCache.cpp b/dom/workers/ServiceWorkerScriptCache.cpp index b5b60fcd07..9fd11cd521 100644 --- a/dom/workers/ServiceWorkerScriptCache.cpp +++ b/dom/workers/ServiceWorkerScriptCache.cpp @@ -10,7 +10,6 @@ #include "mozilla/dom/cache/CacheStorage.h" #include "mozilla/dom/cache/Cache.h" #include "nsIThreadRetargetableRequest.h" -#include "nsSerializationHelper.h" #include "nsIPrincipal.h" #include "Workers.h" @@ -441,9 +440,9 @@ public: } void - SetSecurityInfo(nsISerializable* aSecurityInfo) + InitChannelInfo(nsIChannel* aChannel) { - NS_SerializeToString(aSecurityInfo, mSecurityInfo); + mChannelInfo.InitFromChannel(aChannel); } private: @@ -540,7 +539,7 @@ private: new InternalResponse(200, NS_LITERAL_CSTRING("OK")); ir->SetBody(body); - ir->SetSecurityInfo(mSecurityInfo); + ir->InitChannelInfo(mChannelInfo); nsRefPtr response = new Response(aCache->GetGlobalObject(), ir); @@ -571,7 +570,7 @@ private: // Only used if the network script has changed and needs to be cached. nsString mNewCacheName; - nsCString mSecurityInfo; + ChannelInfo mChannelInfo; nsCString mMaxScope; @@ -600,16 +599,7 @@ CompareNetwork::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext) MOZ_ASSERT(channel == mChannel); #endif - nsCOMPtr infoObj; - mChannel->GetSecurityInfo(getter_AddRefs(infoObj)); - if (infoObj) { - nsCOMPtr serializable = do_QueryInterface(infoObj); - if (serializable) { - mManager->SetSecurityInfo(serializable); - } else { - NS_WARNING("A non-serializable object was obtained from nsIChannel::GetSecurityInfo()!"); - } - } + mManager->InitChannelInfo(mChannel); return NS_OK; } diff --git a/dom/workers/WorkerPrivate.cpp b/dom/workers/WorkerPrivate.cpp index d7fd5268e9..92b8f273cd 100644 --- a/dom/workers/WorkerPrivate.cpp +++ b/dom/workers/WorkerPrivate.cpp @@ -32,7 +32,6 @@ #include "nsIXPConnect.h" #include "nsPerformance.h" #include "nsPIDOMWindow.h" -#include "nsSerializationHelper.h" #include #include "jsfriendapi.h" @@ -4041,17 +4040,6 @@ WorkerPrivateParent::SetPrincipal(nsIPrincipal* aPrincipal, PrincipalToPrincipalInfo(aPrincipal, mLoadInfo.mPrincipalInfo))); } -template -void -WorkerPrivateParent::SetSecurityInfo(nsISerializable* aSerializable) -{ - MOZ_ASSERT(IsServiceWorker()); - AssertIsOnMainThread(); - nsAutoCString securityInfo; - NS_SerializeToString(aSerializable, securityInfo); - SetSecurityInfo(securityInfo); -} - template JSContext* WorkerPrivateParent::ParentJSContext() const diff --git a/dom/workers/WorkerPrivate.h b/dom/workers/WorkerPrivate.h index 44b19a8999..6f5365557d 100644 --- a/dom/workers/WorkerPrivate.h +++ b/dom/workers/WorkerPrivate.h @@ -495,24 +495,34 @@ public: return mLoadInfo.mServiceWorkerCacheName; } - const nsCString& - GetSecurityInfo() const + const ChannelInfo& + GetChannelInfo() const { MOZ_ASSERT(IsServiceWorker()); - return mLoadInfo.mSecurityInfo; + return mLoadInfo.mChannelInfo; } void - SetSecurityInfo(const nsCString& aSecurityInfo) + SetChannelInfo(const ChannelInfo& aChannelInfo) { MOZ_ASSERT(IsServiceWorker()); AssertIsOnMainThread(); - MOZ_ASSERT(mLoadInfo.mSecurityInfo.IsEmpty()); - mLoadInfo.mSecurityInfo = aSecurityInfo; + MOZ_ASSERT(!mLoadInfo.mChannelInfo.IsInitialized()); + MOZ_ASSERT(aChannelInfo.IsInitialized()); + mLoadInfo.mChannelInfo = aChannelInfo; } void - SetSecurityInfo(nsISerializable* aSerializable); + 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 diff --git a/dom/workers/Workers.h b/dom/workers/Workers.h index 9e0d1ae8cd..7b992d3007 100644 --- a/dom/workers/Workers.h +++ b/dom/workers/Workers.h @@ -20,6 +20,7 @@ #include "nsILoadContext.h" #include "nsIWeakReferenceUtils.h" #include "nsIInterfaceRequestor.h" +#include "mozilla/dom/ChannelInfo.h" #define BEGIN_WORKERS_NAMESPACE \ namespace mozilla { namespace dom { namespace workers { @@ -244,7 +245,7 @@ struct WorkerLoadInfo nsString mServiceWorkerCacheName; - nsCString mSecurityInfo; + ChannelInfo mChannelInfo; uint64_t mWindowID; uint64_t mServiceWorkerID; diff --git a/extensions/auth/nsAuth.h b/extensions/auth/nsAuth.h index 839d980a77..1d60de84c9 100644 --- a/extensions/auth/nsAuth.h +++ b/extensions/auth/nsAuth.h @@ -14,7 +14,6 @@ enum pType { #include "mozilla/Logging.h" -#if defined( PR_LOGGING ) // // in order to do logging, the following environment variables need to be set: // @@ -24,8 +23,5 @@ enum pType { extern PRLogModuleInfo* gNegotiateLog; #define LOG(args) PR_LOG(gNegotiateLog, PR_LOG_DEBUG, args) -#else -#define LOG(args) -#endif #endif /* !defined( nsAuth_h__ ) */ diff --git a/extensions/auth/nsAuthFactory.cpp b/extensions/auth/nsAuthFactory.cpp index eb600c1ce5..0712d896cf 100644 --- a/extensions/auth/nsAuthFactory.cpp +++ b/extensions/auth/nsAuthFactory.cpp @@ -220,7 +220,6 @@ static const mozilla::Module::ContractIDEntry kAuthContracts[] = { }; //----------------------------------------------------------------------------- -#if defined( PR_LOGGING ) PRLogModuleInfo *gNegotiateLog; // setup nspr logging ... @@ -230,9 +229,6 @@ InitNegotiateAuth() gNegotiateLog = PR_NewLogModule("negotiateauth"); return NS_OK; } -#else -#define InitNegotiateAuth nullptr -#endif static void DestroyNegotiateAuth() diff --git a/extensions/auth/nsAuthGSSAPI.cpp b/extensions/auth/nsAuthGSSAPI.cpp index 20df685fd9..31a5259fde 100644 --- a/extensions/auth/nsAuthGSSAPI.cpp +++ b/extensions/auth/nsAuthGSSAPI.cpp @@ -226,8 +226,6 @@ gssInit() return NS_OK; } -#if defined( PR_LOGGING ) - // Generate proper GSSAPI error messages from the major and // minor status codes. void @@ -269,12 +267,6 @@ LogGssError(OM_uint32 maj_stat, OM_uint32 min_stat, const char *prefix) LOG(("%s\n", errorStr.get())); } -#else /* PR_LOGGING */ - -#define LogGssError(x,y,z) - -#endif /* PR_LOGGING */ - //----------------------------------------------------------------------------- nsAuthGSSAPI::nsAuthGSSAPI(pType package) diff --git a/extensions/auth/nsAuthSSPI.cpp b/extensions/auth/nsAuthSSPI.cpp index d37a919404..677a4773af 100644 --- a/extensions/auth/nsAuthSSPI.cpp +++ b/extensions/auth/nsAuthSSPI.cpp @@ -467,12 +467,11 @@ nsAuthSSPI::GetNextToken(const void *inToken, &ignored); if (rc == SEC_I_CONTINUE_NEEDED || rc == SEC_E_OK) { -#ifdef PR_LOGGING if (rc == SEC_E_OK) LOG(("InitializeSecurityContext: succeeded.\n")); else LOG(("InitializeSecurityContext: continue.\n")); -#endif + if (sspi_cbt) free(sspi_cbt); diff --git a/extensions/gio/nsGIOProtocolHandler.cpp b/extensions/gio/nsGIOProtocolHandler.cpp index d066e01ca4..1867e9c55e 100644 --- a/extensions/gio/nsGIOProtocolHandler.cpp +++ b/extensions/gio/nsGIOProtocolHandler.cpp @@ -28,12 +28,8 @@ //----------------------------------------------------------------------------- // NSPR_LOG_MODULES=gio:5 -#ifdef PR_LOGGING static PRLogModuleInfo *sGIOLog; #define LOG(args) PR_LOG(sGIOLog, PR_LOG_DEBUG, args) -#else -#define LOG(args) -#endif //----------------------------------------------------------------------------- @@ -906,9 +902,7 @@ NS_IMPL_ISUPPORTS(nsGIOProtocolHandler, nsIProtocolHandler, nsIObserver) nsresult nsGIOProtocolHandler::Init() { -#ifdef PR_LOGGING sGIOLog = PR_NewLogModule("gio"); -#endif nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); if (prefs) diff --git a/gfx/thebes/gfxFcPlatformFontList.cpp b/gfx/thebes/gfxFcPlatformFontList.cpp index c9112c3494..7dc36fcacd 100644 --- a/gfx/thebes/gfxFcPlatformFontList.cpp +++ b/gfx/thebes/gfxFcPlatformFontList.cpp @@ -36,8 +36,6 @@ using namespace mozilla::unicode; #define PRINTING_FC_PROPERTY "gfx.printing" -#ifdef PR_LOGGING - #define LOG_FONTLIST(args) PR_LOG(gfxPlatform::GetLog(eGfxLog_fontlist), \ PR_LOG_DEBUG, args) #define LOG_FONTLIST_ENABLED() PR_LOG_TEST( \ @@ -47,8 +45,6 @@ using namespace mozilla::unicode; gfxPlatform::GetLog(eGfxLog_cmapdata), \ PR_LOG_DEBUG) -#endif - static const FcChar8* ToFcChar8Ptr(const char* aStr) { @@ -387,7 +383,6 @@ gfxFontconfigFontEntry::ReadCMAP(FontInfoData *aFontInfoData) mCharacterMap = new gfxCharacterMap(); } -#ifdef PR_LOGGING LOG_FONTLIST(("(fontlist-cmap) name: %s, size: %d hash: %8.8x%s\n", NS_ConvertUTF16toUTF8(mName).get(), charmap->SizeOfIncludingThis(moz_malloc_size_of), @@ -398,7 +393,6 @@ gfxFontconfigFontEntry::ReadCMAP(FontInfoData *aFontInfoData) NS_ConvertUTF16toUTF8(mName).get()); charmap->Dump(prefix, eGfxLog_cmapdata); } -#endif return rv; } @@ -850,7 +844,6 @@ gfxFontconfigFontFamily::FindStyleVariations(FontInfoData *aFontInfoData) fp->AddFullname(fontEntry, fullname); } -#ifdef PR_LOGGING if (LOG_FONTLIST_ENABLED()) { LOG_FONTLIST(("(fontlist) added (%s) to family (%s)" " with style: %s weight: %d stretch: %d" @@ -862,7 +855,6 @@ gfxFontconfigFontFamily::FindStyleVariations(FontInfoData *aFontInfoData) NS_ConvertUTF16toUTF8(psname).get(), NS_ConvertUTF16toUTF8(fullname).get())); } -#endif } mFaceNamesInitialized = true; mFontPatterns.Clear(); diff --git a/gfx/thebes/gfxGDIFontList.cpp b/gfx/thebes/gfxGDIFontList.cpp index 6f0ac90530..31727f51a9 100644 --- a/gfx/thebes/gfxGDIFontList.cpp +++ b/gfx/thebes/gfxGDIFontList.cpp @@ -527,12 +527,10 @@ GDIFontFamily::FindStyleVariations(FontInfoData *aFontInfoData) EnumFontFamiliesExW(hdc, &logFont, (FONTENUMPROCW)GDIFontFamily::FamilyAddStylesProc, (LPARAM)this, 0); -#ifdef PR_LOGGING if (LOG_FONTLIST_ENABLED() && mAvailableFonts.Length() == 0) { LOG_FONTLIST(("(fontlist) no styles available in family \"%s\"", NS_ConvertUTF16toUTF8(mName).get())); } -#endif ReleaseDC(nullptr, hdc); diff --git a/js/ipc/WrapperAnswer.cpp b/js/ipc/WrapperAnswer.cpp index bdf6b80f2b..4c6611353d 100644 --- a/js/ipc/WrapperAnswer.cpp +++ b/js/ipc/WrapperAnswer.cpp @@ -220,14 +220,15 @@ WrapperAnswer::RecvDelete(const ObjectId &objId, const JSIDVariant &idVar, Retur } bool -WrapperAnswer::RecvHas(const ObjectId& objId, const JSIDVariant& idVar, ReturnStatus* rs, bool* bp) +WrapperAnswer::RecvHas(const ObjectId& objId, const JSIDVariant& idVar, ReturnStatus* rs, + bool* foundp) { AutoJSAPI jsapi; if (NS_WARN_IF(!jsapi.Init(scopeForTargetObjects()))) return false; jsapi.TakeOwnershipOfErrorReporting(); JSContext* cx = jsapi.cx(); - *bp = false; + *foundp = false; RootedObject obj(cx, findObjectById(cx, objId)); if (!obj) @@ -239,24 +240,21 @@ WrapperAnswer::RecvHas(const ObjectId& objId, const JSIDVariant& idVar, ReturnSt if (!fromJSIDVariant(cx, idVar, &id)) return fail(jsapi, rs); - bool found; - if (!JS_HasPropertyById(cx, obj, id, &found)) + if (!JS_HasPropertyById(cx, obj, id, foundp)) return fail(jsapi, rs); - *bp = !!found; - return ok(rs); } bool WrapperAnswer::RecvHasOwn(const ObjectId& objId, const JSIDVariant& idVar, ReturnStatus* rs, - bool* bp) + bool* foundp) { AutoJSAPI jsapi; if (NS_WARN_IF(!jsapi.Init(scopeForTargetObjects()))) return false; jsapi.TakeOwnershipOfErrorReporting(); JSContext* cx = jsapi.cx(); - *bp = false; + *foundp = false; RootedObject obj(cx, findObjectById(cx, objId)); if (!obj) @@ -268,11 +266,8 @@ WrapperAnswer::RecvHasOwn(const ObjectId& objId, const JSIDVariant& idVar, Retur if (!fromJSIDVariant(cx, idVar, &id)) return fail(jsapi, rs); - Rooted desc(cx); - if (!JS_GetPropertyDescriptorById(cx, obj, id, &desc)) + if (!JS_HasOwnPropertyById(cx, obj, id, foundp)) return fail(jsapi, rs); - *bp = (desc.object() == obj); - return ok(rs); } diff --git a/js/ipc/WrapperAnswer.h b/js/ipc/WrapperAnswer.h index 1c1ce965de..f8f734aef4 100644 --- a/js/ipc/WrapperAnswer.h +++ b/js/ipc/WrapperAnswer.h @@ -21,43 +21,43 @@ namespace jsipc { class WrapperAnswer : public virtual JavaScriptShared { public: - explicit WrapperAnswer(JSRuntime *rt) : JavaScriptShared(rt) {} + explicit WrapperAnswer(JSRuntime* rt) : JavaScriptShared(rt) {} - bool RecvPreventExtensions(const ObjectId &objId, ReturnStatus *rs); - bool RecvGetPropertyDescriptor(const ObjectId &objId, const JSIDVariant &id, - ReturnStatus *rs, - PPropertyDescriptor *out); + bool RecvPreventExtensions(const ObjectId& objId, ReturnStatus* rs); + bool RecvGetPropertyDescriptor(const ObjectId& objId, const JSIDVariant& id, + ReturnStatus* rs, + PPropertyDescriptor* out); bool RecvGetOwnPropertyDescriptor(const ObjectId& objId, const JSIDVariant& id, ReturnStatus* rs, PPropertyDescriptor* out); bool RecvDefineProperty(const ObjectId& objId, const JSIDVariant& id, - const PPropertyDescriptor &flags, ReturnStatus *rs); - bool RecvDelete(const ObjectId &objId, const JSIDVariant &id, ReturnStatus *rs); + const PPropertyDescriptor& flags, ReturnStatus* rs); + bool RecvDelete(const ObjectId& objId, const JSIDVariant& id, ReturnStatus* rs); bool RecvHas(const ObjectId& objId, const JSIDVariant& id, - ReturnStatus* rs, bool* bp); + ReturnStatus* rs, bool* foundp); bool RecvHasOwn(const ObjectId& objId, const JSIDVariant& id, - ReturnStatus* rs, bool* bp); - bool RecvGet(const ObjectId &objId, const ObjectVariant &receiverVar, - const JSIDVariant &id, - ReturnStatus *rs, JSVariant *result); - bool RecvSet(const ObjectId &objId, const JSIDVariant &id, const JSVariant &value, - const JSVariant &receiverVar, ReturnStatus *rs); + ReturnStatus* rs, bool* foundp); + bool RecvGet(const ObjectId& objId, const ObjectVariant& receiverVar, + const JSIDVariant& id, + ReturnStatus* rs, JSVariant* result); + bool RecvSet(const ObjectId& objId, const JSIDVariant& id, const JSVariant& value, + const JSVariant& receiverVar, ReturnStatus* rs); - bool RecvIsExtensible(const ObjectId &objId, ReturnStatus *rs, - bool *result); + bool RecvIsExtensible(const ObjectId& objId, ReturnStatus* rs, + bool* result); bool RecvCallOrConstruct(const ObjectId& objId, InfallibleTArray&& argv, const bool& construct, ReturnStatus* rs, JSVariant* result, nsTArray* outparams); bool RecvHasInstance(const ObjectId& objId, const JSVariant& v, ReturnStatus* rs, bool* bp); - bool RecvObjectClassIs(const ObjectId &objId, const uint32_t &classValue, - bool *result); - bool RecvClassName(const ObjectId &objId, nsString *result); - bool RecvGetPrototype(const ObjectId &objId, ReturnStatus *rs, ObjectOrNullVariant *result); - bool RecvRegExpToShared(const ObjectId &objId, ReturnStatus *rs, nsString *source, uint32_t *flags); + bool RecvObjectClassIs(const ObjectId& objId, const uint32_t& classValue, + bool* result); + bool RecvClassName(const ObjectId& objId, nsString* result); + bool RecvGetPrototype(const ObjectId& objId, ReturnStatus* rs, ObjectOrNullVariant* result); + bool RecvRegExpToShared(const ObjectId& objId, ReturnStatus* rs, nsString* source, uint32_t* flags); - bool RecvGetPropertyKeys(const ObjectId &objId, const uint32_t &flags, + bool RecvGetPropertyKeys(const ObjectId& objId, const uint32_t& flags, ReturnStatus* rs, nsTArray* ids); bool RecvInstanceOf(const ObjectId& objId, const JSIID& iid, ReturnStatus* rs, bool* instanceof); diff --git a/js/src/frontend/BytecodeEmitter.cpp b/js/src/frontend/BytecodeEmitter.cpp index 40e83de809..bed4775491 100644 --- a/js/src/frontend/BytecodeEmitter.cpp +++ b/js/src/frontend/BytecodeEmitter.cpp @@ -930,7 +930,7 @@ BytecodeEmitter::enterNestedScope(StmtInfoBCE* stmt, ObjectBox* objbox, StmtType pushStatement(stmt, stmtType, offset()); scopeObj->initEnclosingNestedScope(enclosingStaticScope()); - stmtStack.linkAsInnermostScopal(stmt, *scopeObj); + stmtStack.linkAsInnermostScopeStmt(stmt, *scopeObj); MOZ_ASSERT(stmt->linksScope()); stmt->isBlockScope = (stmtType == StmtType::BLOCK); diff --git a/js/src/frontend/BytecodeEmitter.h b/js/src/frontend/BytecodeEmitter.h index f6178e0487..c757ad7f5d 100644 --- a/js/src/frontend/BytecodeEmitter.h +++ b/js/src/frontend/BytecodeEmitter.h @@ -220,7 +220,7 @@ struct BytecodeEmitter bool updateLocalsToFrameSlots(); StmtInfoBCE* innermostStmt() const { return stmtStack.innermost(); } - StmtInfoBCE* innermostScopeStmt() const { return stmtStack.innermostScopal(); } + StmtInfoBCE* innermostScopeStmt() const { return stmtStack.innermostScopeStmt(); } bool isAliasedName(ParseNode* pn); diff --git a/js/src/frontend/Parser.h b/js/src/frontend/Parser.h index d5cbc5fa65..8e1bb4288d 100644 --- a/js/src/frontend/Parser.h +++ b/js/src/frontend/Parser.h @@ -287,7 +287,7 @@ struct ParseContext : public GenericParseContext unsigned blockid() { return stmtStack.innermost() ? stmtStack.innermost()->blockid : bodyid; } StmtInfoPC* innermostStmt() const { return stmtStack.innermost(); } - StmtInfoPC* innermostScopeStmt() const { return stmtStack.innermostScopal(); } + StmtInfoPC* innermostScopeStmt() const { return stmtStack.innermostScopeStmt(); } // True if we are at the topmost level of a entire script or function body. // For example, while parsing this code we would encounter f1 and f2 at @@ -459,8 +459,8 @@ class Parser : private JS::AutoGCRooter, public StrictModeGetter * Allocate a new parsed object or function container from * cx->tempLifoAlloc. */ - ObjectBox *newObjectBox(JSObject *obj); - FunctionBox *newFunctionBox(Node fn, JSFunction *fun, ParseContext *pc, + ObjectBox* newObjectBox(JSObject* obj); + FunctionBox* newFunctionBox(Node fn, JSFunction* fun, ParseContext* pc, Directives directives, GeneratorKind generatorKind); /* diff --git a/js/src/frontend/SharedContext.h b/js/src/frontend/SharedContext.h index ca82fe371d..d6d756f343 100644 --- a/js/src/frontend/SharedContext.h +++ b/js/src/frontend/SharedContext.h @@ -583,7 +583,7 @@ class MOZ_STACK_CLASS StmtInfoStack { } StmtInfo* innermost() const { return innermostStmt_; } - StmtInfo* innermostScopal() const { return innermostScopeStmt_; } + StmtInfo* innermostScopeStmt() const { return innermostScopeStmt_; } void push(StmtInfo* stmt, StmtType type) { stmt->type = type; @@ -598,7 +598,7 @@ class MOZ_STACK_CLASS StmtInfoStack void pushNestedScope(StmtInfo* stmt, StmtType type, NestedScopeObject& staticScope) { push(stmt, type); - linkAsInnermostScopal(stmt, staticScope); + linkAsInnermostScopeStmt(stmt, staticScope); } void pop() { @@ -608,8 +608,8 @@ class MOZ_STACK_CLASS StmtInfoStack innermostScopeStmt_ = stmt->enclosingScope; } - void linkAsInnermostScopal(StmtInfo* stmt, NestedScopeObject& staticScope) { - MOZ_ASSERT(stmt != innermostScopal()); + void linkAsInnermostScopeStmt(StmtInfo* stmt, NestedScopeObject& staticScope) { + MOZ_ASSERT(stmt != innermostScopeStmt_); MOZ_ASSERT(!stmt->enclosingScope); stmt->enclosingScope = innermostScopeStmt_; innermostScopeStmt_ = stmt; @@ -620,7 +620,7 @@ class MOZ_STACK_CLASS StmtInfoStack MOZ_ASSERT(!innermostStmt_->isBlockScope); MOZ_ASSERT(innermostStmt_->canBeBlockScope()); innermostStmt_->isBlockScope = true; - linkAsInnermostScopal(innermostStmt_, blockObj); + linkAsInnermostScopeStmt(innermostStmt_, blockObj); } }; diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index 1ad33b0ec3..8fca73b81e 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -2861,6 +2861,26 @@ JS_GetOwnPropertyDescriptor(JSContext* cx, HandleObject obj, const char* name, return JS_GetOwnPropertyDescriptorById(cx, obj, id, desc); } +JS_PUBLIC_API(bool) +JS_HasOwnPropertyById(JSContext* cx, HandleObject obj, HandleId id, bool* foundp) +{ + AssertHeapIsIdle(cx); + CHECK_REQUEST(cx); + assertSameCompartment(cx, obj, id); + + return HasOwnProperty(cx, obj, id, foundp); +} + +JS_PUBLIC_API(bool) +JS_HasOwnProperty(JSContext* cx, HandleObject obj, const char* name, bool* foundp) +{ + JSAtom* atom = Atomize(cx, name, strlen(name)); + if (!atom) + return false; + RootedId id(cx, AtomToId(atom)); + return JS_HasOwnPropertyById(cx, obj, id, foundp); +} + JS_PUBLIC_API(bool) JS_GetPropertyDescriptorById(JSContext* cx, HandleObject obj, HandleId id, MutableHandle desc) diff --git a/js/src/jsapi.h b/js/src/jsapi.h index afac05cfdb..8b5e074a8b 100644 --- a/js/src/jsapi.h +++ b/js/src/jsapi.h @@ -2884,34 +2884,34 @@ JS_DefinePropertyById(JSContext *cx, JS::HandleObject obj, JS::HandleId id, JS::Handle desc); extern JS_PUBLIC_API(JSObject *) -JS_DefineObject(JSContext *cx, JS::HandleObject obj, const char *name, - const JSClass *clasp = nullptr, unsigned attrs = 0); +JS_DefineObject(JSContext* cx, JS::HandleObject obj, const char* name, + const JSClass* clasp = nullptr, unsigned attrs = 0); extern JS_PUBLIC_API(bool) -JS_DefineConstDoubles(JSContext *cx, JS::HandleObject obj, const JSConstDoubleSpec *cds); +JS_DefineConstDoubles(JSContext* cx, JS::HandleObject obj, const JSConstDoubleSpec* cds); extern JS_PUBLIC_API(bool) -JS_DefineConstIntegers(JSContext *cx, JS::HandleObject obj, const JSConstIntegerSpec *cis); +JS_DefineConstIntegers(JSContext* cx, JS::HandleObject obj, const JSConstIntegerSpec* cis); extern JS_PUBLIC_API(bool) -JS_DefineProperties(JSContext *cx, JS::HandleObject obj, const JSPropertySpec *ps); +JS_DefineProperties(JSContext* cx, JS::HandleObject obj, const JSPropertySpec* ps); /* * */ extern JS_PUBLIC_API(bool) -JS_AlreadyHasOwnProperty(JSContext *cx, JS::HandleObject obj, const char *name, - bool *foundp); +JS_AlreadyHasOwnProperty(JSContext* cx, JS::HandleObject obj, const char* name, + bool* foundp); extern JS_PUBLIC_API(bool) -JS_AlreadyHasOwnPropertyById(JSContext *cx, JS::HandleObject obj, JS::HandleId id, - bool *foundp); +JS_AlreadyHasOwnPropertyById(JSContext* cx, JS::HandleObject obj, JS::HandleId id, + bool* foundp); extern JS_PUBLIC_API(bool) -JS_HasProperty(JSContext *cx, JS::HandleObject obj, const char *name, bool *foundp); +JS_HasProperty(JSContext* cx, JS::HandleObject obj, const char* name, bool* foundp); extern JS_PUBLIC_API(bool) -JS_HasPropertyById(JSContext *cx, JS::HandleObject obj, JS::HandleId id, bool *foundp); +JS_HasPropertyById(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* foundp); extern JS_PUBLIC_API(bool) @@ -2923,9 +2923,15 @@ JS_GetOwnPropertyDescriptor(JSContext* cx, JS::HandleObject obj, const char* nam JS::MutableHandle desc); extern JS_PUBLIC_API(bool) -JS_GetOwnUCPropertyDescriptor(JSContext *cx, JS::HandleObject obj, const char16_t *name, +JS_GetOwnUCPropertyDescriptor(JSContext* cx, JS::HandleObject obj, const char16_t* name, JS::MutableHandle desc); +extern JS_PUBLIC_API(bool) +JS_HasOwnPropertyById(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* foundp); + +extern JS_PUBLIC_API(bool) +JS_HasOwnProperty(JSContext* cx, JS::HandleObject obj, const char* name, bool* foundp); + /* * Like JS_GetOwnPropertyDescriptorById but will return a property on * an object on the prototype chain (returned in desc->obj). If desc->obj is null, @@ -2956,21 +2962,21 @@ extern JS_PUBLIC_API(bool) JS_SetPropertyById(JSContext* cx, JS::HandleObject obj, JS::HandleId id, JS::HandleValue v); extern JS_PUBLIC_API(bool) -JS_ForwardSetPropertyTo(JSContext *cx, JS::HandleObject obj, JS::HandleId id, JS::HandleValue v, +JS_ForwardSetPropertyTo(JSContext* cx, JS::HandleObject obj, JS::HandleId id, JS::HandleValue v, JS::HandleValue receiver, JS::ObjectOpResult &result); extern JS_PUBLIC_API(bool) JS_DeleteProperty(JSContext* cx, JS::HandleObject obj, const char* name); extern JS_PUBLIC_API(bool) -JS_DeleteProperty(JSContext *cx, JS::HandleObject obj, const char *name, +JS_DeleteProperty(JSContext* cx, JS::HandleObject obj, const char* name, JS::ObjectOpResult &result); extern JS_PUBLIC_API(bool) JS_DeletePropertyById(JSContext* cx, JS::HandleObject obj, jsid id); extern JS_PUBLIC_API(bool) -JS_DeletePropertyById(JSContext *cx, JS::HandleObject obj, JS::HandleId id, +JS_DeletePropertyById(JSContext* cx, JS::HandleObject obj, JS::HandleId id, JS::ObjectOpResult &result); extern JS_PUBLIC_API(bool) @@ -3004,12 +3010,12 @@ JS_DefineUCProperty(JSContext* cx, JS::HandleObject obj, const char16_t* name, s JSNative getter = nullptr, JSNative setter = nullptr); extern JS_PUBLIC_API(bool) -JS_DefineUCProperty(JSContext *cx, JS::HandleObject obj, const char16_t *name, size_t namelen, +JS_DefineUCProperty(JSContext* cx, JS::HandleObject obj, const char16_t* name, size_t namelen, JS::Handle desc, JS::ObjectOpResult &result); extern JS_PUBLIC_API(bool) -JS_DefineUCProperty(JSContext *cx, JS::HandleObject obj, const char16_t *name, size_t namelen, +JS_DefineUCProperty(JSContext* cx, JS::HandleObject obj, const char16_t* name, size_t namelen, JS::Handle desc); extern JS_PUBLIC_API(bool) @@ -3032,7 +3038,7 @@ JS_SetUCProperty(JSContext* cx, JS::HandleObject obj, JS::HandleValue v); extern JS_PUBLIC_API(bool) -JS_DeleteUCProperty(JSContext *cx, JS::HandleObject obj, const char16_t *name, size_t namelen, +JS_DeleteUCProperty(JSContext* cx, JS::HandleObject obj, const char16_t* name, size_t namelen, JS::ObjectOpResult &result); extern JS_PUBLIC_API(JSObject*) @@ -3109,16 +3115,16 @@ extern JS_PUBLIC_API(bool) JS_SetElement(JSContext* cx, JS::HandleObject obj, uint32_t index, int32_t v); extern JS_PUBLIC_API(bool) -JS_SetElement(JSContext *cx, JS::HandleObject obj, uint32_t index, uint32_t v); +JS_SetElement(JSContext* cx, JS::HandleObject obj, uint32_t index, uint32_t v); extern JS_PUBLIC_API(bool) -JS_SetElement(JSContext *cx, JS::HandleObject obj, uint32_t index, double v); +JS_SetElement(JSContext* cx, JS::HandleObject obj, uint32_t index, double v); extern JS_PUBLIC_API(bool) -JS_DeleteElement(JSContext *cx, JS::HandleObject obj, uint32_t index); +JS_DeleteElement(JSContext* cx, JS::HandleObject obj, uint32_t index); extern JS_PUBLIC_API(bool) -JS_DeleteElement(JSContext *cx, JS::HandleObject obj, uint32_t index, JS::ObjectOpResult &result); +JS_DeleteElement(JSContext* cx, JS::HandleObject obj, uint32_t index, JS::ObjectOpResult &result); /* * Assign 'undefined' to all of the object's non-reserved slots. Note: this is diff --git a/js/src/vm/ScopeObject-inl.h b/js/src/vm/ScopeObject-inl.h index 1432bbf329..1111e3dea8 100644 --- a/js/src/vm/ScopeObject-inl.h +++ b/js/src/vm/ScopeObject-inl.h @@ -193,7 +193,7 @@ StaticScopeIter::fun() const } /* namespace js */ -inline JSObject * +inline JSObject* JSObject::enclosingScope() { if (is()) diff --git a/js/xpconnect/loader/mozJSComponentLoader.cpp b/js/xpconnect/loader/mozJSComponentLoader.cpp index 495b15f02b..5273490e21 100644 --- a/js/xpconnect/loader/mozJSComponentLoader.cpp +++ b/js/xpconnect/loader/mozJSComponentLoader.cpp @@ -69,10 +69,8 @@ static const char kJSCachePrefix[] = "jsloader"; #define XPC_SERIALIZATION_BUFFER_SIZE (64 * 1024) #define XPC_DESERIALIZATION_BUFFER_SIZE (12 * 8192) -#ifdef PR_LOGGING // NSPR_LOG_MODULES=JSComponentLoader:5 static PRLogModuleInfo* gJSCLLog; -#endif #define LOG(args) PR_LOG(gJSCLLog, PR_LOG_DEBUG, args) @@ -199,11 +197,9 @@ mozJSComponentLoader::mozJSComponentLoader() { MOZ_ASSERT(!sSelf, "mozJSComponentLoader should be a singleton"); -#ifdef PR_LOGGING if (!gJSCLLog) { gJSCLLog = PR_NewLogModule("JSComponentLoader"); } -#endif sSelf = this; } diff --git a/js/xpconnect/src/nsXPConnect.cpp b/js/xpconnect/src/nsXPConnect.cpp index e2afe21136..a5042de08a 100644 --- a/js/xpconnect/src/nsXPConnect.cpp +++ b/js/xpconnect/src/nsXPConnect.cpp @@ -214,9 +214,7 @@ xpc::ErrorReport::Init(JSErrorReport* aReport, const char* aFallbackMessage, mIsMuted = aReport->isMuted; } -#ifdef PR_LOGGING static PRLogModuleInfo* gJSDiagnostics; -#endif void xpc::ErrorReport::LogToConsole() @@ -241,7 +239,6 @@ xpc::ErrorReport::LogToConsole() fflush(stderr); } -#ifdef PR_LOGGING // Log to the PR Log Module. if (!gJSDiagnostics) gJSDiagnostics = PR_NewLogModule("JSDiagnostics"); @@ -251,7 +248,6 @@ xpc::ErrorReport::LogToConsole() ("file %s, line %u\n%s", NS_LossyConvertUTF16toASCII(mFileName).get(), mLineNumber, NS_LossyConvertUTF16toASCII(mErrorMsg).get())); } -#endif // Log to the console. We do this last so that we can simply return if // there's no console service without affecting the other reporting diff --git a/layout/base/AccessibleCaretEventHub.cpp b/layout/base/AccessibleCaretEventHub.cpp index 11b97d7acb..48264f9946 100644 --- a/layout/base/AccessibleCaretEventHub.cpp +++ b/layout/base/AccessibleCaretEventHub.cpp @@ -21,8 +21,6 @@ namespace mozilla { -#ifdef PR_LOGGING - #undef AC_LOG #define AC_LOG(message, ...) \ AC_LOG_BASE("AccessibleCaretEventHub (%p): " message, this, ##__VA_ARGS__); @@ -31,8 +29,6 @@ namespace mozilla { #define AC_LOGV(message, ...) \ AC_LOGV_BASE("AccessibleCaretEventHub (%p): " message, this, ##__VA_ARGS__); -#endif // #ifdef PR_LOGGING - NS_IMPL_ISUPPORTS(AccessibleCaretEventHub, nsIReflowObserver, nsIScrollObserver, nsISelectionListener, nsISupportsWeakReference); diff --git a/layout/base/AccessibleCaretManager.cpp b/layout/base/AccessibleCaretManager.cpp index 88b3d7bb27..9b818f7458 100644 --- a/layout/base/AccessibleCaretManager.cpp +++ b/layout/base/AccessibleCaretManager.cpp @@ -21,8 +21,6 @@ namespace mozilla { -#ifdef PR_LOGGING - #undef AC_LOG #define AC_LOG(message, ...) \ AC_LOG_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__); @@ -31,8 +29,6 @@ namespace mozilla { #define AC_LOGV(message, ...) \ AC_LOGV_BASE("AccessibleCaretManager (%p): " message, this, ##__VA_ARGS__); -#endif // #ifdef PR_LOGGING - using namespace dom; using Appearance = AccessibleCaret::Appearance; using PositionChangedResult = AccessibleCaret::PositionChangedResult; diff --git a/modules/libjar/InterceptedJARChannel.cpp b/modules/libjar/InterceptedJARChannel.cpp new file mode 100644 index 0000000000..3f17f288ed --- /dev/null +++ b/modules/libjar/InterceptedJARChannel.cpp @@ -0,0 +1,127 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set expandtab ts=2 sw=2 sts=2 cin: */ +/* 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 "InterceptedJARChannel.h" +#include "nsIPipe.h" +#include "mozilla/dom/ChannelInfo.h" + +using namespace mozilla::net; + +NS_IMPL_ISUPPORTS(InterceptedJARChannel, nsIInterceptedChannel) + +InterceptedJARChannel::InterceptedJARChannel(nsJARChannel* aChannel, + nsINetworkInterceptController* aController, + bool aIsNavigation) +: mController(aController) +, mChannel(aChannel) +, mIsNavigation(aIsNavigation) +{ +} + +NS_IMETHODIMP +InterceptedJARChannel::GetResponseBody(nsIOutputStream** aStream) +{ + NS_IF_ADDREF(*aStream = mResponseBody); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedJARChannel::GetIsNavigation(bool* aIsNavigation) +{ + *aIsNavigation = mIsNavigation; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedJARChannel::GetChannel(nsIChannel** aChannel) +{ + NS_IF_ADDREF(*aChannel = mChannel); + return NS_OK; +} + +NS_IMETHODIMP +InterceptedJARChannel::ResetInterception() +{ + if (!mChannel) { + return NS_ERROR_NOT_AVAILABLE; + } + + mResponseBody = nullptr; + mSynthesizedInput = nullptr; + + mChannel->ResetInterception(); + mChannel = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedJARChannel::SynthesizeStatus(uint16_t aStatus, + const nsACString& aReason) +{ + return NS_OK; +} + +NS_IMETHODIMP +InterceptedJARChannel::SynthesizeHeader(const nsACString& aName, + const nsACString& aValue) +{ + return NS_OK; +} + +NS_IMETHODIMP +InterceptedJARChannel::FinishSynthesizedResponse() +{ + if (NS_WARN_IF(!mChannel)) { + return NS_ERROR_NOT_AVAILABLE; + } + + mChannel->OverrideWithSynthesizedResponse(mSynthesizedInput); + + mResponseBody = nullptr; + mChannel = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedJARChannel::Cancel() +{ + if (!mChannel) { + return NS_ERROR_FAILURE; + } + + nsresult rv = mChannel->Cancel(NS_BINDING_ABORTED); + NS_ENSURE_SUCCESS(rv, rv); + mResponseBody = nullptr; + mChannel = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +InterceptedJARChannel::SetChannelInfo(mozilla::dom::ChannelInfo* aChannelInfo) +{ + if (!mChannel) { + return NS_ERROR_FAILURE; + } + + return aChannelInfo->ResurrectInfoOnChannel(mChannel); +} + +void +InterceptedJARChannel::NotifyController() +{ + nsresult rv = NS_NewPipe(getter_AddRefs(mSynthesizedInput), + getter_AddRefs(mResponseBody), + 0, UINT32_MAX, true, true); + NS_ENSURE_SUCCESS_VOID(rv); + + rv = mController->ChannelIntercepted(this); + if (NS_WARN_IF(NS_FAILED(rv))) { + rv = ResetInterception(); + NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), + "Failed to resume intercepted network request"); + } + mController = nullptr; +} diff --git a/modules/libjar/InterceptedJARChannel.h b/modules/libjar/InterceptedJARChannel.h new file mode 100644 index 0000000000..cd48dd2348 --- /dev/null +++ b/modules/libjar/InterceptedJARChannel.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set expandtab ts=2 sw=2 sts=2 cin: */ +/* 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 InterceptedJARChannel_h +#define InterceptedJARChannel_h + +#include "nsJAR.h" +#include "nsJARChannel.h" +#include "nsIInputStream.h" +#include "nsIInputStreamPump.h" +#include "nsINetworkInterceptController.h" +#include "nsIOutputStream.h" +#include "mozilla/RefPtr.h" + +#include "mozilla/Maybe.h" + +class nsIStreamListener; +class nsJARChannel; + +namespace mozilla { +namespace net { + +// An object representing a channel that has been intercepted. This avoids +// complicating the actual channel implementation with the details of +// synthesizing responses. +class InterceptedJARChannel : public nsIInterceptedChannel +{ + // The interception controller to notify about the successful channel + // interception. + nsCOMPtr mController; + + // The actual channel being intercepted. + nsRefPtr mChannel; + + // Reader-side of the synthesized response body. + nsCOMPtr mSynthesizedInput; + + // The stream to write the body of the synthesized response. + nsCOMPtr mResponseBody; + + // Wether this intercepted channel was performing a navigation. + bool mIsNavigation; + + virtual ~InterceptedJARChannel() {}; +public: + InterceptedJARChannel(nsJARChannel* aChannel, + nsINetworkInterceptController* aController, + bool aIsNavigation); + + NS_DECL_ISUPPORTS + NS_DECL_NSIINTERCEPTEDCHANNEL + + void NotifyController(); +}; + +} // namespace net +} // namespace mozilla + +#endif // InterceptedJARChannel_h diff --git a/modules/libjar/moz.build b/modules/libjar/moz.build index d02c680829..858bbe30a7 100644 --- a/modules/libjar/moz.build +++ b/modules/libjar/moz.build @@ -23,12 +23,14 @@ XPIDL_SOURCES += [ XPIDL_MODULE = 'jar' EXPORTS += [ + 'InterceptedJARChannel.h', 'nsJARURI.h', 'nsZipArchive.h', 'zipstruct.h', ] UNIFIED_SOURCES += [ + 'InterceptedJARChannel.cpp', 'nsJARProtocolHandler.cpp', 'nsJARURI.cpp', ] diff --git a/modules/libjar/nsJARChannel.cpp b/modules/libjar/nsJARChannel.cpp index e363403706..4f8aff2e4f 100644 --- a/modules/libjar/nsJARChannel.cpp +++ b/modules/libjar/nsJARChannel.cpp @@ -24,6 +24,9 @@ #include "mozilla/net/RemoteOpenFileChild.h" #include "nsITabChild.h" #include "private/pprio.h" +#include "nsINetworkInterceptController.h" +#include "InterceptedJARChannel.h" +#include "nsInputStreamPump.h" using namespace mozilla; using namespace mozilla::net; @@ -43,14 +46,11 @@ static NS_DEFINE_CID(kZipReaderCID, NS_ZIPREADER_CID); #undef LOG #endif -#if defined(PR_LOGGING) // // set NSPR_LOG_MODULES=nsJarProtocol:5 // static PRLogModuleInfo *gJarProtocolLog = nullptr; -#endif -// If you ever want to define PR_FORCE_LOGGING in this file, see bug 545995 #define LOG(args) PR_LOG(gJarProtocolLog, PR_LOG_DEBUG, args) #define LOG_ENABLED() PR_LOG_TEST(gJarProtocolLog, 4) @@ -202,11 +202,10 @@ nsJARChannel::nsJARChannel() , mIsUnsafe(true) , mOpeningRemote(false) , mEnsureChildFd(false) + , mSynthesizedStreamLength(0) { -#if defined(PR_LOGGING) if (!gJarProtocolLog) gJarProtocolLog = PR_NewLogModule("nsJarProtocol"); -#endif // hold an owning reference to the jar handler NS_ADDREF(gJarHandler); @@ -239,7 +238,7 @@ NS_IMPL_ISUPPORTS_INHERITED(nsJARChannel, nsIThreadRetargetableStreamListener, nsIJARChannel) -nsresult +nsresult nsJARChannel::Init(nsIURI *uri) { nsresult rv; @@ -263,9 +262,7 @@ nsJARChannel::Init(nsIURI *uri) return NS_ERROR_INVALID_ARG; } -#if defined(PR_LOGGING) mJarURI->GetSpec(mSpec); -#endif return rv; } @@ -537,6 +534,8 @@ nsJARChannel::GetStatus(nsresult *status) { if (mPump && NS_SUCCEEDED(mStatus)) mPump->GetStatus(status); + else if (mSynthesizedResponsePump && NS_SUCCEEDED(mStatus)) + mSynthesizedResponsePump->GetStatus(status); else *status = mStatus; return NS_OK; @@ -548,6 +547,8 @@ nsJARChannel::Cancel(nsresult status) mStatus = status; if (mPump) return mPump->Cancel(status); + if (mSynthesizedResponsePump) + return mSynthesizedResponsePump->Cancel(status); NS_ASSERTION(!mIsPending, "need to implement cancel when downloading"); return NS_OK; @@ -558,6 +559,8 @@ nsJARChannel::Suspend() { if (mPump) return mPump->Suspend(); + if (mSynthesizedResponsePump) + return mSynthesizedResponsePump->Suspend(); NS_ASSERTION(!mIsPending, "need to implement suspend when downloading"); return NS_OK; @@ -568,6 +571,8 @@ nsJARChannel::Resume() { if (mPump) return mPump->Resume(); + if (mSynthesizedResponsePump) + return mSynthesizedResponsePump->Resume(); NS_ASSERTION(!mIsPending, "need to implement resume when downloading"); return NS_OK; @@ -677,7 +682,20 @@ nsJARChannel::SetNotificationCallbacks(nsIInterfaceRequestor *aCallbacks) return NS_OK; } -NS_IMETHODIMP +nsresult +nsJARChannel::OverrideSecurityInfo(nsISupports* aSecurityInfo) +{ + MOZ_RELEASE_ASSERT(!mSecurityInfo, + "This can only be called when we don't have a security info object already"); + MOZ_RELEASE_ASSERT(aSecurityInfo, + "This can only be called with a valid security info object"); + MOZ_RELEASE_ASSERT(ShouldIntercept(), + "This can only be called on channels that can be intercepted"); + mSecurityInfo = aSecurityInfo; + return NS_OK; +} + +NS_IMETHODIMP nsJARChannel::GetSecurityInfo(nsISupports **aSecurityInfo) { NS_PRECONDITION(aSecurityInfo, "Null out param"); @@ -843,6 +861,66 @@ nsJARChannel::Open(nsIInputStream **stream) return NS_OK; } +bool +nsJARChannel::ShouldIntercept() +{ + LOG(("nsJARChannel::ShouldIntercept [this=%x]\n", this)); + // We only intercept app:// requests + if (!mAppURI) { + return false; + } + + nsCOMPtr controller; + NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, + NS_GET_IID(nsINetworkInterceptController), + getter_AddRefs(controller)); + bool shouldIntercept = false; + if (controller) { + bool isNavigation = mLoadFlags & LOAD_DOCUMENT_URI; + nsresult rv = controller->ShouldPrepareForIntercept(mAppURI, + isNavigation, + &shouldIntercept); + NS_ENSURE_SUCCESS(rv, false); + } + + return shouldIntercept; +} + +void nsJARChannel::ResetInterception() +{ + LOG(("nsJARChannel::ResetInterception [this=%x]\n", this)); + + // Continue with the origin request. + nsresult rv = ContinueAsyncOpen(); + NS_ENSURE_SUCCESS_VOID(rv); +} + +void +nsJARChannel::OverrideWithSynthesizedResponse(nsIInputStream* aSynthesizedInput) +{ + // In our current implementation, the FetchEvent handler will copy the + // response stream completely into the pipe backing the input stream so we + // can treat the available as the length of the stream. + uint64_t available; + nsresult rv = aSynthesizedInput->Available(&available); + if (NS_WARN_IF(NS_FAILED(rv))) { + mSynthesizedStreamLength = -1; + } else { + mSynthesizedStreamLength = int64_t(available); + } + + rv = nsInputStreamPump::Create(getter_AddRefs(mSynthesizedResponsePump), + aSynthesizedInput, + int64_t(-1), int64_t(-1), 0, 0, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + aSynthesizedInput->Close(); + return; + } + + rv = mSynthesizedResponsePump->AsyncRead(this, nullptr); + NS_ENSURE_SUCCESS_VOID(rv); +} + NS_IMETHODIMP nsJARChannel::AsyncOpen(nsIStreamListener *listener, nsISupports *ctx) { @@ -858,18 +936,40 @@ nsJARChannel::AsyncOpen(nsIStreamListener *listener, nsISupports *ctx) // Initialize mProgressSink NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, mProgressSink); - nsresult rv = LookupFile(true); - if (NS_FAILED(rv)) - return rv; - - // These variables must only be set if we're going to trigger an - // OnStartRequest, either from AsyncRead or OnDownloadComplete. - // - // That means: Do not add early return statements beyond this point! mListener = listener; mListenerContext = ctx; mIsPending = true; + // Check if this channel should intercept the network request and prepare + // for a possible synthesized response instead. + if (ShouldIntercept()) { + nsCOMPtr controller; + NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, + NS_GET_IID(nsINetworkInterceptController), + getter_AddRefs(controller)); + + bool isNavigation = mLoadFlags & LOAD_DOCUMENT_URI; + nsRefPtr intercepted = + new InterceptedJARChannel(this, controller, isNavigation); + intercepted->NotifyController(); + return NS_OK; + } + + return ContinueAsyncOpen(); +} + +nsresult +nsJARChannel::ContinueAsyncOpen() +{ + LOG(("nsJARChannel::ContinueAsyncOpen [this=%x]\n", this)); + nsresult rv = LookupFile(true); + if (NS_FAILED(rv)) { + mIsPending = false; + mListenerContext = nullptr; + mListener = nullptr; + return rv; + } + nsCOMPtr channel; if (!mJarFile) { @@ -1182,9 +1282,7 @@ nsJARChannel::OnDataAvailable(nsIRequest *req, nsISupports *ctx, nsIInputStream *stream, uint64_t offset, uint32_t count) { -#if defined(PR_LOGGING) LOG(("nsJARChannel::OnDataAvailable [this=%x %s]\n", this, mSpec.get())); -#endif nsresult rv; diff --git a/modules/libjar/nsJARChannel.h b/modules/libjar/nsJARChannel.h index 6a2941d231..d4866a34f8 100644 --- a/modules/libjar/nsJARChannel.h +++ b/modules/libjar/nsJARChannel.h @@ -10,6 +10,7 @@ #include "nsIJARChannel.h" #include "nsIJARURI.h" #include "nsIInputStreamPump.h" +#include "InterceptedJARChannel.h" #include "nsIInterfaceRequestor.h" #include "nsIProgressEventSink.h" #include "nsIStreamListener.h" @@ -27,6 +28,13 @@ #include "mozilla/Logging.h" class nsJARInputThunk; +class nsInputStreamPump; + +namespace mozilla { +namespace net { + class InterceptedJARChannel; +} // namespace net +} // namespace mozilla //----------------------------------------------------------------------------- @@ -53,6 +61,8 @@ public: nsresult Init(nsIURI *uri); + nsresult OverrideSecurityInfo(nsISupports* aSecurityInfo); + private: virtual ~nsJARChannel(); @@ -69,9 +79,18 @@ private: mozilla::net::MemoryDownloader::Data aData) override; -#if defined(PR_LOGGING) + // Returns true if this channel should intercept the network request and + // prepare for a possible synthesized response instead. + bool ShouldIntercept(); + nsresult ContinueAsyncOpen(); + // Discard the prior interception and continue with the original network + // request. + void ResetInterception(); + // Override this channel's pending response with a synthesized one. The + // content will be asynchronously read from the pump. + void OverrideWithSynthesizedResponse(nsIInputStream* aSynthesizedInput); + nsCString mSpec; -#endif bool mOpened; @@ -109,6 +128,11 @@ private: nsCOMPtr mJarBaseURI; nsCString mJarEntry; nsCString mInnerJarEntry; + + nsRefPtr mSynthesizedResponsePump; + int64_t mSynthesizedStreamLength; + + friend class mozilla::net::InterceptedJARChannel; }; #endif // nsJARChannel_h__ diff --git a/netwerk/base/moz.build b/netwerk/base/moz.build index 691da44e64..ab518be901 100644 --- a/netwerk/base/moz.build +++ b/netwerk/base/moz.build @@ -145,6 +145,7 @@ EXPORTS += [ 'nsASocketHandler.h', 'nsAsyncRedirectVerifyHelper.h', 'nsFileStreams.h', + 'nsInputStreamPump.h', 'nsMIMEInputStream.h', 'nsNetUtil.h', 'nsReadLine.h', diff --git a/netwerk/base/nsINetworkInterceptController.idl b/netwerk/base/nsINetworkInterceptController.idl index 6295fc38c2..a816931ae4 100644 --- a/netwerk/base/nsINetworkInterceptController.idl +++ b/netwerk/base/nsINetworkInterceptController.idl @@ -9,6 +9,16 @@ interface nsIChannel; interface nsIOutputStream; interface nsIURI; +%{C++ +namespace mozilla { +namespace dom { +class ChannelInfo; +} +} +%} + +[ptr] native ChannelInfo(mozilla::dom::ChannelInfo); + /** * Interface to allow implementors of nsINetworkInterceptController to control the behaviour * of intercepted channels without tying implementation details of the interception to @@ -16,7 +26,7 @@ interface nsIURI; * which do not implement nsIChannel. */ -[scriptable, uuid(2fc1170c-4f9d-4c9e-8e5d-2d351dbe03f2)] +[scriptable, uuid(f2c07a6b-366d-4ef4-85ab-a77f4bcb1646)] interface nsIInterceptedChannel : nsISupports { /** @@ -67,9 +77,10 @@ interface nsIInterceptedChannel : nsISupports readonly attribute bool isNavigation; /** - * This method allows to override the security info for the channel. + * This method allows to override the channel info for the channel. */ - void setSecurityInfo(in nsISupports securityInfo); + [noscript] + void setChannelInfo(in ChannelInfo channelInfo); }; /** diff --git a/netwerk/protocol/http/InterceptedChannel.cpp b/netwerk/protocol/http/InterceptedChannel.cpp index 212b7324fa..2518fcb9c6 100644 --- a/netwerk/protocol/http/InterceptedChannel.cpp +++ b/netwerk/protocol/http/InterceptedChannel.cpp @@ -13,6 +13,7 @@ #include "nsHttpChannel.h" #include "HttpChannelChild.h" #include "nsHttpResponseHead.h" +#include "mozilla/dom/ChannelInfo.h" namespace mozilla { namespace net { @@ -233,9 +234,13 @@ InterceptedChannelChrome::Cancel() } NS_IMETHODIMP -InterceptedChannelChrome::SetSecurityInfo(nsISupports* aSecurityInfo) +InterceptedChannelChrome::SetChannelInfo(dom::ChannelInfo* aChannelInfo) { - return mChannel->OverrideSecurityInfo(aSecurityInfo); + if (!mChannel) { + return NS_ERROR_FAILURE; + } + + return aChannelInfo->ResurrectInfoOnChannel(mChannel); } InterceptedChannelContent::InterceptedChannelContent(HttpChannelChild* aChannel, @@ -336,9 +341,13 @@ InterceptedChannelContent::Cancel() } NS_IMETHODIMP -InterceptedChannelContent::SetSecurityInfo(nsISupports* aSecurityInfo) +InterceptedChannelContent::SetChannelInfo(dom::ChannelInfo* aChannelInfo) { - return mChannel->OverrideSecurityInfo(aSecurityInfo); + if (!mChannel) { + return NS_ERROR_FAILURE; + } + + return aChannelInfo->ResurrectInfoOnChannel(mChannel); } } // namespace net diff --git a/netwerk/protocol/http/InterceptedChannel.h b/netwerk/protocol/http/InterceptedChannel.h index b009b8c143..941fededd1 100644 --- a/netwerk/protocol/http/InterceptedChannel.h +++ b/netwerk/protocol/http/InterceptedChannel.h @@ -82,7 +82,7 @@ public: NS_IMETHOD SynthesizeStatus(uint16_t aStatus, const nsACString& aReason) override; NS_IMETHOD SynthesizeHeader(const nsACString& aName, const nsACString& aValue) override; NS_IMETHOD Cancel() override; - NS_IMETHOD SetSecurityInfo(nsISupports* aSecurityInfo) override; + NS_IMETHOD SetChannelInfo(mozilla::dom::ChannelInfo* aChannelInfo) override; virtual void NotifyController() override; }; @@ -109,7 +109,7 @@ public: NS_IMETHOD SynthesizeStatus(uint16_t aStatus, const nsACString& aReason) override; NS_IMETHOD SynthesizeHeader(const nsACString& aName, const nsACString& aValue) override; NS_IMETHOD Cancel() override; - NS_IMETHOD SetSecurityInfo(nsISupports* aSecurityInfo) override; + NS_IMETHOD SetChannelInfo(mozilla::dom::ChannelInfo* aChannelInfo) override; virtual void NotifyController() override; }; diff --git a/netwerk/protocol/http/TunnelUtils.h b/netwerk/protocol/http/TunnelUtils.h index b3decc860b..ec058dee95 100644 --- a/netwerk/protocol/http/TunnelUtils.h +++ b/netwerk/protocol/http/TunnelUtils.h @@ -15,6 +15,7 @@ #include "nsITimer.h" #include "NullHttpTransaction.h" #include "mozilla/TimeStamp.h" +#include "prio.h" // a TLSFilterTransaction wraps another nsAHttpTransaction but // applies a encode/decode filter of TLS onto the ReadSegments diff --git a/netwerk/protocol/http/nsHttpConnectionInfo.h b/netwerk/protocol/http/nsHttpConnectionInfo.h index d6a96f3f5a..7b15fb0288 100644 --- a/netwerk/protocol/http/nsHttpConnectionInfo.h +++ b/netwerk/protocol/http/nsHttpConnectionInfo.h @@ -11,6 +11,7 @@ #include "nsProxyInfo.h" #include "nsCOMPtr.h" #include "nsStringFwd.h" +#include "mozilla/Logging.h" extern PRLogModuleInfo *gHttpLog; diff --git a/parser/htmlparser/nsExpatDriver.cpp b/parser/htmlparser/nsExpatDriver.cpp index 1448045388..11d5125305 100644 --- a/parser/htmlparser/nsExpatDriver.cpp +++ b/parser/htmlparser/nsExpatDriver.cpp @@ -34,7 +34,6 @@ static const char16_t kUTF16[] = { 'U', 'T', 'F', '-', '1', '6', '\0' }; -#ifdef PR_LOGGING static PRLogModuleInfo * GetExpatDriverLog() { @@ -43,7 +42,6 @@ GetExpatDriverLog() sLog = PR_NewLogModule("expatdriver"); return sLog; } -#endif /***************************** EXPAT CALL BACKS ******************************/ // The callback handlers that get called from the expat parser. @@ -1096,7 +1094,6 @@ nsExpatDriver::ConsumeToken(nsScanner& aScanner, bool& aFlushTokens) buffer = nullptr; length = 0; -#if defined(PR_LOGGING) || defined (DEBUG) if (blocked) { PR_LOG(GetExpatDriverLog(), PR_LOG_DEBUG, ("Resuming Expat, will parse data remaining in Expat's " @@ -1113,7 +1110,6 @@ nsExpatDriver::ConsumeToken(nsScanner& aScanner, bool& aFlushTokens) NS_ConvertUTF16toUTF8(currentExpatPosition.get(), mExpatBuffered).get())); } -#endif } else { buffer = start.get(); diff --git a/storage/StorageBaseStatementInternal.h b/storage/StorageBaseStatementInternal.h index 20b289fc1b..5005300859 100644 --- a/storage/StorageBaseStatementInternal.h +++ b/storage/StorageBaseStatementInternal.h @@ -321,6 +321,20 @@ NS_DEFINE_STATIC_IID_ACCESSOR(StorageBaseStatementInternal, const uint8_t *aValue, \ uint32_t aValueSize), \ (aWhere, aValue, aValueSize)) \ + BIND_GEN_IMPL(_class, _optionalGuard, \ + StringAsBlob, \ + (const nsACString &aWhere, \ + const nsAString& aValue), \ + (uint32_t aWhere, \ + const nsAString& aValue), \ + (aWhere, aValue)) \ + BIND_GEN_IMPL(_class, _optionalGuard, \ + UTF8StringAsBlob, \ + (const nsACString &aWhere, \ + const nsACString& aValue), \ + (uint32_t aWhere, \ + const nsACString& aValue), \ + (aWhere, aValue)) \ BIND_GEN_IMPL(_class, _optionalGuard, \ AdoptedBlob, \ (const nsACString &aWhere, \ diff --git a/storage/mozIStorageBaseStatement.idl b/storage/mozIStorageBaseStatement.idl index 439d3e886a..52cd305001 100644 --- a/storage/mozIStorageBaseStatement.idl +++ b/storage/mozIStorageBaseStatement.idl @@ -19,7 +19,7 @@ interface mozIStorageBindingParamsArray; * (mozIStorageStatement) that can be used for both synchronous and asynchronous * purposes. */ -[scriptable, uuid(5d34f333-ed3f-4aa2-ba51-f2a8b0cfa33a)] +[scriptable, uuid(16ca67aa-1325-43e2-aac7-859afd1590b2)] interface mozIStorageBaseStatement : mozIStorageBindingParams { /** * Finalizes a statement so you can successfully close a database connection. @@ -67,6 +67,12 @@ interface mozIStorageBaseStatement : mozIStorageBindingParams { in unsigned long aParamIndex, [array,const,size_is(aValueSize)] in octet aValue, in unsigned long aValueSize); + [deprecated] void bindStringAsBlobParameter( + in unsigned long aParamIndex, + in AString aValue); + [deprecated] void bindUTF8StringAsBlobParameter( + in unsigned long aParamIndex, + in AUTF8String aValue); [deprecated] void bindAdoptedBlobParameter( in unsigned long aParamIndex, [array,size_is(aValueSize)] in octet aValue, diff --git a/storage/mozIStorageBindingParams.idl b/storage/mozIStorageBindingParams.idl index 3e98a996c7..81d9e6efb3 100644 --- a/storage/mozIStorageBindingParams.idl +++ b/storage/mozIStorageBindingParams.idl @@ -8,7 +8,7 @@ interface nsIVariant; -[scriptable, uuid(7d8763ad-79d9-4674-ada1-37fd702af68c)] +[scriptable, uuid(2d09f42f-966e-4663-b4b3-b0c8676bf2bf)] interface mozIStorageBindingParams : nsISupports { /** * Binds aValue to the parameter with the name aName. @@ -34,6 +34,11 @@ interface mozIStorageBindingParams : nsISupports { void bindBlobByName(in AUTF8String aName, [array, const, size_is(aValueSize)] in octet aValue, in unsigned long aValueSize); + + // Convenience routines for storing strings as blobs. + void bindStringAsBlobByName(in AUTF8String aName, in AString aValue); + void bindUTF8StringAsBlobByName(in AUTF8String aName, in AUTF8String aValue); + // The function adopts the storage for the provided blob. After calling // this function, mozStorage will ensure that NS_Free is called on the // underlying pointer. @@ -66,6 +71,11 @@ interface mozIStorageBindingParams : nsISupports { void bindBlobByIndex(in unsigned long aIndex, [array, const, size_is(aValueSize)] in octet aValue, in unsigned long aValueSize); + + // Convenience routines for storing strings as blobs. + void bindStringAsBlobByIndex(in unsigned long aIndex, in AString aValue); + void bindUTF8StringAsBlobByIndex(in unsigned long aIndex, in AUTF8String aValue); + // The function adopts the storage for the provided blob. After calling // this function, mozStorage will ensure that NS_Free is called on the // underlying pointer. diff --git a/storage/mozIStorageStatement.idl b/storage/mozIStorageStatement.idl index 329ce280dc..28abc59791 100644 --- a/storage/mozIStorageStatement.idl +++ b/storage/mozIStorageStatement.idl @@ -15,7 +15,7 @@ * A SQL statement that can be used for both synchronous and asynchronous * purposes. */ -[scriptable, uuid(b3c4476e-c490-4e3b-9db1-e2d3a6f0287c)] +[scriptable, uuid(5f567c35-6c32-4140-828c-683ea49cfd3a)] interface mozIStorageStatement : mozIStorageBaseStatement { /** * Create a clone of this statement, by initializing a new statement @@ -207,6 +207,29 @@ interface mozIStorageStatement : mozIStorageBaseStatement { * The contents of the BLOB. This will be NULL if aDataSize == 0. */ void getBlob(in unsigned long aIndex, out unsigned long aDataSize, [array,size_is(aDataSize)] out octet aData); + + /** + * Retrieve the contents of a Blob column from the current result row as a + * string. + * + * @param aIndex + * 0-based colummn index. + * @return The value for the result Blob column interpreted as a String. + * No encoding conversion is performed. + */ + AString getBlobAsString(in unsigned long aIndex); + + /** + * Retrieve the contents of a Blob column from the current result row as a + * UTF8 string. + * + * @param aIndex + * 0-based colummn index. + * @return The value for the result Blob column interpreted as a UTF8 String. + * No encoding conversion is performed. + */ + AUTF8String getBlobAsUTF8String(in unsigned long aIndex); + /** * Check whether the given column in the current result row is NULL. * diff --git a/storage/mozIStorageValueArray.idl b/storage/mozIStorageValueArray.idl index fd1de2a9d4..3dbf75285e 100644 --- a/storage/mozIStorageValueArray.idl +++ b/storage/mozIStorageValueArray.idl @@ -14,7 +14,7 @@ * mozIStorageValueArray wraps an array of SQL values, such as a single database * row. */ -[scriptable, uuid(07b5b93e-113c-4150-863c-d247b003a55d)] +[scriptable, uuid(6e6306f4-ffa7-40f5-96ca-36159ce8f431)] interface mozIStorageValueArray : nsISupports { /** * These type values are returned by getTypeOfIndex @@ -62,6 +62,8 @@ interface mozIStorageValueArray : nsISupports { // data will be NULL if dataSize = 0 void getBlob(in unsigned long aIndex, out unsigned long aDataSize, [array,size_is(aDataSize)] out octet aData); + AString getBlobAsString(in unsigned long aIndex); + AUTF8String getBlobAsUTF8String(in unsigned long aIndex); boolean getIsNull(in unsigned long aIndex); /** diff --git a/storage/mozStorageArgValueArray.cpp b/storage/mozStorageArgValueArray.cpp index 019788512e..3b9daab2fd 100644 --- a/storage/mozStorageArgValueArray.cpp +++ b/storage/mozStorageArgValueArray.cpp @@ -152,6 +152,18 @@ ArgValueArray::GetBlob(uint32_t aIndex, return NS_OK; } +NS_IMETHODIMP +ArgValueArray::GetBlobAsString(uint32_t aIndex, nsAString& aValue) +{ + return DoGetBlobAsString(this, aIndex, aValue); +} + +NS_IMETHODIMP +ArgValueArray::GetBlobAsUTF8String(uint32_t aIndex, nsACString& aValue) +{ + return DoGetBlobAsString(this, aIndex, aValue); +} + NS_IMETHODIMP ArgValueArray::GetIsNull(uint32_t aIndex, bool *_isNull) diff --git a/storage/mozStorageBindingParams.cpp b/storage/mozStorageBindingParams.cpp index d8c2fded5f..359bdd8b5a 100644 --- a/storage/mozStorageBindingParams.cpp +++ b/storage/mozStorageBindingParams.cpp @@ -358,6 +358,20 @@ BindingParams::BindBlobByName(const nsACString &aName, return BindByName(aName, value); } +NS_IMETHODIMP +BindingParams::BindStringAsBlobByName(const nsACString& aName, + const nsAString& aValue) +{ + return DoBindStringAsBlobByName(this, aName, aValue); +} + +NS_IMETHODIMP +BindingParams::BindUTF8StringAsBlobByName(const nsACString& aName, + const nsACString& aValue) +{ + return DoBindStringAsBlobByName(this, aName, aValue); +} + NS_IMETHODIMP BindingParams::BindAdoptedBlobByName(const nsACString &aName, @@ -493,6 +507,19 @@ BindingParams::BindBlobByIndex(uint32_t aIndex, return BindByIndex(aIndex, value); } +NS_IMETHODIMP +BindingParams::BindStringAsBlobByIndex(uint32_t aIndex, const nsAString& aValue) +{ + return DoBindStringAsBlobByIndex(this, aIndex, aValue); +} + +NS_IMETHODIMP +BindingParams::BindUTF8StringAsBlobByIndex(uint32_t aIndex, + const nsACString& aValue) +{ + return DoBindStringAsBlobByIndex(this, aIndex, aValue); +} + NS_IMETHODIMP BindingParams::BindAdoptedBlobByIndex(uint32_t aIndex, uint8_t *aValue, diff --git a/storage/mozStoragePrivateHelpers.h b/storage/mozStoragePrivateHelpers.h index 718ad402c1..e29188ce58 100644 --- a/storage/mozStoragePrivateHelpers.h +++ b/storage/mozStoragePrivateHelpers.h @@ -88,6 +88,54 @@ already_AddRefed newCompletionEvent( mozIStorageCompletionCallback *aCallback ); +/** + * Utility method to get a Blob as a string value. The string expects + * the interface exposed by nsAString/nsACString/etc. + */ +template +nsresult +DoGetBlobAsString(T* aThis, uint32_t aIndex, V& aValue) +{ + typedef typename V::char_type char_type; + + uint32_t size; + char_type* blob; + nsresult rv = + aThis->GetBlob(aIndex, &size, reinterpret_cast(&blob)); + NS_ENSURE_SUCCESS(rv, rv); + + aValue.Adopt(blob, size / sizeof(char_type)); + return NS_OK; +} + +/** + * Utility method to bind a string value as a Blob. The string expects + * the interface exposed by nsAString/nsACString/etc. + */ +template +nsresult +DoBindStringAsBlobByName(T* aThis, const nsACString& aName, const V& aValue) +{ + typedef typename V::char_type char_type; + return aThis->BindBlobByName(aName, + reinterpret_cast(aValue.BeginReading()), + aValue.Length() * sizeof(char_type)); +} + +/** + * Utility method to bind a string value as a Blob. The string expects + * the interface exposed by nsAString/nsACString/etc. + */ +template +nsresult +DoBindStringAsBlobByIndex(T* aThis, uint32_t aIndex, const V& aValue) +{ + typedef typename V::char_type char_type; + return aThis->BindBlobByIndex(aIndex, + reinterpret_cast(aValue.BeginReading()), + aValue.Length() * sizeof(char_type)); +} + } // namespace storage } // namespace mozilla diff --git a/storage/mozStorageRow.cpp b/storage/mozStorageRow.cpp index 9f28151e09..7bcac4c301 100644 --- a/storage/mozStorageRow.cpp +++ b/storage/mozStorageRow.cpp @@ -194,6 +194,18 @@ Row::GetBlob(uint32_t aIndex, reinterpret_cast(_blob)); } +NS_IMETHODIMP +Row::GetBlobAsString(uint32_t aIndex, nsAString& aValue) +{ + return DoGetBlobAsString(this, aIndex, aValue); +} + +NS_IMETHODIMP +Row::GetBlobAsUTF8String(uint32_t aIndex, nsACString& aValue) +{ + return DoGetBlobAsString(this, aIndex, aValue); +} + NS_IMETHODIMP Row::GetIsNull(uint32_t aIndex, bool *_isNull) diff --git a/storage/mozStorageStatement.cpp b/storage/mozStorageStatement.cpp index bec88abf21..3c01a6933b 100644 --- a/storage/mozStorageStatement.cpp +++ b/storage/mozStorageStatement.cpp @@ -828,6 +828,18 @@ Statement::GetBlob(uint32_t aIndex, return NS_OK; } +NS_IMETHODIMP +Statement::GetBlobAsString(uint32_t aIndex, nsAString& aValue) +{ + return DoGetBlobAsString(this, aIndex, aValue); +} + +NS_IMETHODIMP +Statement::GetBlobAsUTF8String(uint32_t aIndex, nsACString& aValue) +{ + return DoGetBlobAsString(this, aIndex, aValue); +} + NS_IMETHODIMP Statement::GetSharedUTF8String(uint32_t aIndex, uint32_t *_length, diff --git a/testing/marionette/client/marionette/runner/base.py b/testing/marionette/client/marionette/runner/base.py index 2dbcd3a14d..9690104967 100644 --- a/testing/marionette/client/marionette/runner/base.py +++ b/testing/marionette/client/marionette/runner/base.py @@ -556,10 +556,10 @@ class BaseMarionetteTestRunner(object): # In the event we're gathering debug without starting a session, skip marionette commands if marionette.session is not None: try: - marionette.set_context(marionette.CONTEXT_CHROME) - rv['screenshot'] = marionette.screenshot() - marionette.set_context(marionette.CONTEXT_CONTENT) - rv['source'] = marionette.page_source + with marionette.using_context(marionette.CONTEXT_CHROME): + rv['screenshot'] = marionette.screenshot() + with marionette.using_context(marionette.CONTEXT_CONTENT): + rv['source'] = marionette.page_source except: logger = get_default_logger() logger.warning('Failed to gather test failure debug.', exc_info=True) diff --git a/testing/marionette/client/marionette/tests/unit/test_click.py b/testing/marionette/client/marionette/tests/unit/test_click.py index 0e16a68731..14b779654f 100644 --- a/testing/marionette/client/marionette/tests/unit/test_click.py +++ b/testing/marionette/client/marionette/tests/unit/test_click.py @@ -32,72 +32,3 @@ class TestClick(MarionetteTestCase): with self.assertRaises(ElementNotVisibleException): self.marionette.find_element(By.ID, 'child').click() - -class TestClickAction(MarionetteTestCase): - - def setUp(self): - MarionetteTestCase.setUp(self) - if self.marionette.session_capabilities['platformName'] == 'DARWIN': - self.mod_key = Keys.META - else: - self.mod_key = Keys.CONTROL - self.action = Actions(self.marionette) - - def test_click_action(self): - test_html = self.marionette.absolute_url("test.html") - self.marionette.navigate(test_html) - link = self.marionette.find_element(By.ID, "mozLink") - self.action.click(link).perform() - self.assertEqual("Clicked", self.marionette.execute_script("return document.getElementById('mozLink').innerHTML;")) - - def test_clicking_element_out_of_view_succeeds(self): - # The action based click doesn't check for visibility. - test_html = self.marionette.absolute_url('hidden.html') - self.marionette.navigate(test_html) - el = self.marionette.find_element(By.ID, 'child') - self.action.click(el).perform() - - def test_double_click_action(self): - test_html = self.marionette.absolute_url("javascriptPage.html") - self.marionette.navigate(test_html) - el = self.marionette.find_element(By.ID, 'displayed') - # The first click just brings the element into view so text selection - # works as expected. (A different test page could be used to isolate - # this element and make sure it's always in view) - el.click() - self.action.double_click(el).perform() - el.send_keys(self.mod_key + 'c') - rel = self.marionette.find_element("id", "keyReporter") - rel.send_keys(self.mod_key + 'v') - self.assertEqual(rel.get_attribute('value'), 'Displayed') - - def test_context_click_action(self): - test_html = self.marionette.absolute_url("javascriptPage.html") - self.marionette.navigate(test_html) - click_el = self.marionette.find_element(By.ID, 'resultContainer') - - def context_menu_state(): - with self.marionette.using_context('chrome'): - cm_el = self.marionette.find_element(By.ID, 'contentAreaContextMenu') - return cm_el.get_attribute('state') - - self.assertEqual('closed', context_menu_state()) - self.action.context_click(click_el).perform() - self.wait_for_condition(lambda _: context_menu_state() == 'open') - - with self.marionette.using_context('chrome'): - (self.marionette.find_element(By.ID, 'main-window') - .send_keys(Keys.ESCAPE)) - self.wait_for_condition(lambda _: context_menu_state() == 'closed') - - def test_middle_click_action(self): - test_html = self.marionette.absolute_url("clicks.html") - self.marionette.navigate(test_html) - - self.marionette.find_element(By.ID, "addbuttonlistener").click() - - el = self.marionette.find_element(By.ID, "showbutton") - self.action.middle_click(el).perform() - - self.wait_for_condition( - lambda _: el.get_attribute('innerHTML') == '1') diff --git a/testing/marionette/client/marionette/tests/unit/test_execute_script.py b/testing/marionette/client/marionette/tests/unit/test_execute_script.py index 78b81d7935..42ac086751 100644 --- a/testing/marionette/client/marionette/tests/unit/test_execute_script.py +++ b/testing/marionette/client/marionette/tests/unit/test_execute_script.py @@ -4,15 +4,17 @@ import urllib -from marionette_driver.by import By -from marionette_driver.errors import JavascriptException +from marionette_driver import By, errors from marionette import MarionetteTestCase + def inline(doc): return "data:text/html;charset=utf-8,%s" % urllib.quote(doc) + elements = inline("

foo

bar

") + class TestExecuteContent(MarionetteTestCase): def test_stack_trace(self): try: @@ -21,7 +23,7 @@ class TestExecuteContent(MarionetteTestCase): return b; """) self.assertFalse(True) - except JavascriptException, inst: + except errors.JavascriptException as inst: self.assertTrue('return b' in inst.stacktrace) def test_execute_simple(self): @@ -34,11 +36,11 @@ class TestExecuteContent(MarionetteTestCase): self.assertEqual(self.marionette.execute_script("1;"), None) def test_execute_js_exception(self): - self.assertRaises(JavascriptException, + self.assertRaises(errors.JavascriptException, self.marionette.execute_script, "return foo(bar);") def test_execute_permission(self): - self.assertRaises(JavascriptException, + self.assertRaises(errors.JavascriptException, self.marionette.execute_script, """ let prefs = Components.classes["@mozilla.org/preferences-service;1"] @@ -96,6 +98,7 @@ let prefs = Components.classes["@mozilla.org/preferences-service;1"] [None]) self.assertIs(result, None) + class TestExecuteChrome(TestExecuteContent): def setUp(self): super(TestExecuteChrome, self).setUp() @@ -121,3 +124,10 @@ class TestExecuteChrome(TestExecuteContent): actual = self.marionette.execute_script( "return document.querySelectorAll('textbox')") self.assertEqual(expected, actual) + + def test_async_script_timeout(self): + with self.assertRaises(errors.ScriptTimeoutException): + self.marionette.execute_async_script(""" + var cb = arguments[arguments.length - 1]; + setTimeout(function() { cb() }, 250); + """, script_timeout=100) diff --git a/testing/marionette/client/marionette/tests/unit/test_mouse_action.py b/testing/marionette/client/marionette/tests/unit/test_mouse_action.py new file mode 100644 index 0000000000..5d7d807123 --- /dev/null +++ b/testing/marionette/client/marionette/tests/unit/test_mouse_action.py @@ -0,0 +1,117 @@ +# 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/. + +from marionette import MarionetteTestCase +from marionette_driver.marionette import Actions +from marionette_driver.keys import Keys +from marionette_driver.by import By + +class TestMouseAction(MarionetteTestCase): + + def setUp(self): + MarionetteTestCase.setUp(self) + if self.marionette.session_capabilities['platformName'] == 'DARWIN': + self.mod_key = Keys.META + else: + self.mod_key = Keys.CONTROL + self.action = Actions(self.marionette) + + def test_click_action(self): + test_html = self.marionette.absolute_url("test.html") + self.marionette.navigate(test_html) + link = self.marionette.find_element(By.ID, "mozLink") + self.action.click(link).perform() + self.assertEqual("Clicked", self.marionette.execute_script("return document.getElementById('mozLink').innerHTML;")) + + def test_clicking_element_out_of_view_succeeds(self): + # The action based click doesn't check for visibility. + test_html = self.marionette.absolute_url('hidden.html') + self.marionette.navigate(test_html) + el = self.marionette.find_element(By.ID, 'child') + self.action.click(el).perform() + + def test_double_click_action(self): + test_html = self.marionette.absolute_url("javascriptPage.html") + self.marionette.navigate(test_html) + el = self.marionette.find_element(By.ID, 'displayed') + # The first click just brings the element into view so text selection + # works as expected. (A different test page could be used to isolate + # this element and make sure it's always in view) + el.click() + self.action.double_click(el).perform() + el.send_keys(self.mod_key + 'c') + rel = self.marionette.find_element("id", "keyReporter") + rel.send_keys(self.mod_key + 'v') + self.assertEqual(rel.get_attribute('value'), 'Displayed') + + def test_context_click_action(self): + test_html = self.marionette.absolute_url("javascriptPage.html") + self.marionette.navigate(test_html) + click_el = self.marionette.find_element(By.ID, 'resultContainer') + + def context_menu_state(): + with self.marionette.using_context('chrome'): + cm_el = self.marionette.find_element(By.ID, 'contentAreaContextMenu') + return cm_el.get_attribute('state') + + self.assertEqual('closed', context_menu_state()) + self.action.context_click(click_el).perform() + self.wait_for_condition(lambda _: context_menu_state() == 'open') + + with self.marionette.using_context('chrome'): + (self.marionette.find_element(By.ID, 'main-window') + .send_keys(Keys.ESCAPE)) + self.wait_for_condition(lambda _: context_menu_state() == 'closed') + + def test_middle_click_action(self): + test_html = self.marionette.absolute_url("clicks.html") + self.marionette.navigate(test_html) + + self.marionette.find_element(By.ID, "addbuttonlistener").click() + + el = self.marionette.find_element(By.ID, "showbutton") + self.action.middle_click(el).perform() + + self.wait_for_condition( + lambda _: el.get_attribute('innerHTML') == '1') + + def test_chrome_click(self): + self.marionette.navigate("about:blank") + data_uri = "data:text/html," + with self.marionette.using_context('chrome'): + urlbar = self.marionette.find_element(By.ID, "urlbar") + urlbar.send_keys(data_uri) + go_button = self.marionette.find_element(By.ID, "urlbar-go-button") + self.action.click(go_button).perform() + self.wait_for_condition(lambda mn: mn.get_url() == data_uri) + + def test_chrome_double_click(self): + self.marionette.navigate("about:blank") + test_word = "quux" + with self.marionette.using_context('chrome'): + urlbar = self.marionette.find_element(By.ID, "urlbar") + self.assertEqual(urlbar.get_attribute('value'), '') + + urlbar.send_keys(test_word) + self.assertEqual(urlbar.get_attribute('value'), test_word) + (self.action.double_click(urlbar).perform() + .key_down(self.mod_key) + .key_down('x').perform()) + self.assertEqual(urlbar.get_attribute('value'), '') + + def test_chrome_context_click_action(self): + self.marionette.set_context('chrome') + def context_menu_state(): + cm_el = self.marionette.find_element(By.ID, 'tabContextMenu') + return cm_el.get_attribute('state') + + currtab = self.marionette.execute_script("return gBrowser.selectedTab") + self.assertEqual('closed', context_menu_state()) + self.action.context_click(currtab).perform() + self.wait_for_condition(lambda _: context_menu_state() == 'open') + + (self.marionette.find_element(By.ID, 'main-window') + .send_keys(Keys.ESCAPE)) + + self.wait_for_condition(lambda _: context_menu_state() == 'closed') diff --git a/testing/marionette/client/marionette/tests/unit/test_teardown_context_preserved.py b/testing/marionette/client/marionette/tests/unit/test_teardown_context_preserved.py new file mode 100644 index 0000000000..344d5f6aee --- /dev/null +++ b/testing/marionette/client/marionette/tests/unit/test_teardown_context_preserved.py @@ -0,0 +1,21 @@ +# 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/. + +from marionette import MarionetteTestCase, SkipTest + + +class TestTearDownContext(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + + def tearDown(self): + self.assertEqual(self.get_context(), self.marionette.CONTEXT_CHROME) + MarionetteTestCase.tearDown(self) + + def get_context(self): + return self.marionette._send_message('getContext', 'value') + + def test_skipped_teardown_ok(self): + raise SkipTest("This should leave our teardown method in chrome context") diff --git a/testing/marionette/client/marionette/tests/unit/unit-tests.ini b/testing/marionette/client/marionette/tests/unit/unit-tests.ini index bba42eb0be..44087f14b3 100644 --- a/testing/marionette/client/marionette/tests/unit/unit-tests.ini +++ b/testing/marionette/client/marionette/tests/unit/unit-tests.ini @@ -149,3 +149,6 @@ skip-if = os == "linux" # Bug 1085717 [test_modal_dialogs.py] b2g = false [test_key_actions.py] +[test_mouse_action.py] +b2g = false +[test_teardown_context_preserved.py] diff --git a/testing/marionette/command.js b/testing/marionette/command.js new file mode 100644 index 0000000000..6b66ccf351 --- /dev/null +++ b/testing/marionette/command.js @@ -0,0 +1,168 @@ +/* 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 {utils: Cu} = Components; + +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); + +Cu.import("chrome://marionette/content/error.js"); + +this.EXPORTED_SYMBOLS = ["CommandProcessor", "Response"]; +const logger = Log.repository.getLogger("Marionette"); + +/** + * Represents the response returned from the remote end after execution + * of its corresponding command. + * + * The Response is a mutable object passed to each command for + * modification through the available setters. The response is sent + * implicitly by CommandProcessor when a command is finished executing, + * and any modifications made subsequent to this will have no effect. + * + * @param {number} cmdId + * UUID tied to the corresponding command request this is + * a response for. + * @param {function(number)} okHandler + * Callback function called on successful responses with no body. + * @param {function(Object, number)} respHandler + * Callback function called on successful responses with body. + * @param {Object=} msg + * A message to populate the response, containing the properties + * "sessionId", "status", and "value". + * @param {function(Map)=} sanitizer + * Run before sending message. + */ +this.Response = function(cmdId, okHandler, respHandler, msg, sanitizer) { + const removeEmpty = function(map) { + let rv = {}; + for (let [key, value] of map) { + if (typeof value == "undefined") { + value = null; + } + rv[key] = value; + } + return rv; + }; + + this.id = cmdId; + this.ok = true; + this.okHandler = okHandler; + this.respHandler = respHandler; + this.sanitizer = sanitizer || removeEmpty; + + this.data = new Map([ + ["sessionId", msg.sessionId ? msg.sessionId : null], + ["status", msg.status ? msg.status : 0 /* success */], + ["value", msg.value ? msg.value : undefined], + ]); +}; + +Response.prototype = { + get name() { return this.data.get("name"); }, + set name(n) { this.data.set("name", n); }, + get sessionId() { return this.data.get("sessionId"); }, + set sessionId(id) { this.data.set("sessionId", id); }, + get status() { return this.data.get("status"); }, + set status(ns) { this.data.set("status", ns); }, + get value() { return this.data.get("value"); }, + set value(val) { + this.data.set("value", val); + this.ok = false; + } +}; + +Response.prototype.send = function() { + if (this.sent) { + logger.warn("Skipped sending response to command ID " + + this.id + " because response has already been sent"); + return; + } + + if (this.ok) { + this.okHandler(this.id); + } else { + let rawData = this.sanitizer(this.data); + this.respHandler(rawData, this.id); + } +}; + +/** + * @param {(Error|Object)} err + * The error to send, either an instance of the Error prototype, + * or an object with the properties "message", "code", and "stack". + */ +Response.prototype.sendError = function(err) { + this.status = "code" in err ? err.code : new UnknownError().code; + this.value = error.toJSON(err); + this.send(); + + // propagate errors that are implementation problems + if (!error.isWebDriverError(err)) { + throw err; + } +}; + +/** + * The command processor receives messages on execute(payload, …) + * from the dispatcher, processes them, and wraps the functions that + * it executes from the WebDriver implementation, driver. + * + * @param {GeckoDriver} driver + * Reference to the driver implementation. + */ +this.CommandProcessor = function(driver) { + this.driver = driver; +}; + +/** + * Executes a WebDriver command based on the received payload, + * which is expected to be an object with a "parameters" property + * that is a simple key/value collection of arguments. + * + * The respHandler function will be called with the JSON object to + * send back to the client. + * + * The cmdId is the UUID tied to this request that prevents + * the dispatcher from sending responses in the wrong order. + * + * @param {Object} payload + * Message as received from client. + * @param {function(number)} okHandler + * Callback function called on successful responses with no body. + * @param {function(Object, number)} respHandler + * Callback function called on successful responses with body. + * @param {number} cmdId + * The unique identifier for the command to execute. + */ +CommandProcessor.prototype.execute = function(payload, okHandler, respHandler, cmdId) { + let cmd = payload; + let resp = new Response( + cmdId, okHandler, respHandler, {sessionId: this.driver.sessionId}); + let sendResponse = resp.send.bind(resp); + let sendError = resp.sendError.bind(resp); + + // Ideally handlers shouldn't have to care about the command ID, + // but some methods (newSession, executeScript, et al.) have not + // yet been converted to use the new form of request dispatching. + cmd.id = cmdId; + + // For as long as the old form of request dispatching is in use, + // we need to tell ListenerProxy what the current command ID is + // so that individual commands in driver.js can define it explicitly. + this.driver.listener.curCmdId = cmd.id; + + let req = Task.spawn(function*() { + let fn = this.driver.commands[cmd.name]; + if (typeof fn == "undefined") { + throw new UnknownCommandError(cmd.name); + } + + yield fn.bind(this.driver)(cmd, resp); + }.bind(this)); + + req.then(sendResponse, sendError).catch(error.report); +}; diff --git a/testing/marionette/components/marionettecomponent.js b/testing/marionette/components/marionettecomponent.js index a4f5cc4ca1..ddae3a1e72 100644 --- a/testing/marionette/components/marionettecomponent.js +++ b/testing/marionette/components/marionettecomponent.js @@ -2,37 +2,34 @@ * 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.CC = Components.Constructor; -this.Cc = Components.classes; -this.Ci = Components.interfaces; -this.Cu = Components.utils; -this.Cr = Components.results; +"use strict"; + +const {Constructor: CC, interfaces: Ci, utils: Cu} = Components; const MARIONETTE_CONTRACTID = "@mozilla.org/marionette;1"; const MARIONETTE_CID = Components.ID("{786a1369-dca5-4adc-8486-33d23c88010a}"); -const MARIONETTE_ENABLED_PREF = 'marionette.defaultPrefs.enabled'; -const MARIONETTE_FORCELOCAL_PREF = 'marionette.force-local'; -const MARIONETTE_LOG_PREF = 'marionette.logging'; -this.ServerSocket = CC("@mozilla.org/network/server-socket;1", - "nsIServerSocket", - "initSpecialConnection"); +const DEFAULT_PORT = 2828; +const ENABLED_PREF = "marionette.defaultPrefs.enabled"; +const PORT_PREF = "marionette.defaultPrefs.port"; +const FORCELOCAL_PREF = "marionette.force-local"; +const LOG_PREF = "marionette.logging"; + +const ServerSocket = CC("@mozilla.org/network/server-socket;1", + "nsIServerSocket", + "initSpecialConnection"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); Cu.import("resource://gre/modules/Log.jsm"); -let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader); - function MarionetteComponent() { - this._loaded = false; + this.loaded_ = false; this.observerService = Services.obs; - // set up the logger this.logger = Log.repository.getLogger("Marionette"); - this.logger.level = Log.Level["Trace"]; + this.logger.level = Log.Level.Trace; let dumper = false; #ifdef DEBUG dumper = true; @@ -41,12 +38,11 @@ function MarionetteComponent() { dumper = true; #endif try { - if (dumper || Services.prefs.getBoolPref(MARIONETTE_LOG_PREF)) { + if (dumper || Services.prefs.getBoolPref(LOG_PREF)) { let formatter = new Log.BasicFormatter(); this.logger.addAppender(new Log.DumpAppender(formatter)); } - } - catch(e) {} + } catch(e) {} } MarionetteComponent.prototype = { @@ -54,134 +50,141 @@ MarionetteComponent.prototype = { classID: MARIONETTE_CID, contractID: MARIONETTE_CONTRACTID, QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler, Ci.nsIObserver]), - _xpcom_categories: [{category: "command-line-handler", entry: "b-marionette"}, - {category: "profile-after-change", service: true}], - appName: Services.appinfo.name, + _xpcom_categories: [ + {category: "command-line-handler", entry: "b-marionette"}, + {category: "profile-after-change", service: true} + ], enabled: false, finalUiStartup: false, - _marionetteServer: null, + server: null +}; - onSocketAccepted: function mc_onSocketAccepted(aSocket, aTransport) { - this.logger.info("onSocketAccepted for Marionette dummy socket"); - }, +MarionetteComponent.prototype.onSocketAccepted = function( + socket, transport) { + this.logger.info("onSocketAccepted for Marionette dummy socket"); +}; - onStopListening: function mc_onStopListening(aSocket, status) { - this.logger.info("onStopListening for Marionette dummy socket, code " + status); - aSocket.close(); - }, +MarionetteComponent.prototype.onStopListening = function(socket, status) { + this.logger.info(`onStopListening for Marionette dummy socket, code ${status}`); + socket.close(); +}; - // Check cmdLine argument for --marionette - handle: function mc_handle(cmdLine) { - // If the CLI is there then lets do work otherwise nothing to see - if (cmdLine.handleFlag("marionette", false)) { - this.enabled = true; - this.logger.info("marionette enabled via command-line"); - this.init(); - } - }, +/** Check cmdLine argument for {@code --marionette}. */ +MarionetteComponent.prototype.handle = function(cmdLine) { + // if the CLI is there then lets do work otherwise nothing to see + if (cmdLine.handleFlag("marionette", false)) { + this.enabled = true; + this.logger.info("Marionette enabled via command-line flag"); + this.init(); + } +}; - observe: function mc_observe(aSubject, aTopic, aData) { - switch (aTopic) { - case "profile-after-change": - // Using final-ui-startup as the xpcom category doesn't seem to work, - // so we wait for that by adding an observer here. - this.observerService.addObserver(this, "final-ui-startup", false); +MarionetteComponent.prototype.observe = function(subj, topic, data) { + switch (topic) { + case "profile-after-change": + // Using final-ui-startup as the xpcom category doesn't seem to work, + // so we wait for that by adding an observer here. + this.observerService.addObserver(this, "final-ui-startup", false); #ifdef ENABLE_MARIONETTE - let enabledPref = false; - try { - enabledPref = Services.prefs.getBoolPref(MARIONETTE_ENABLED_PREF); - } catch(e) {} - if (enabledPref) { - this.enabled = true; - this.logger.info("marionette enabled via build flag and pref"); - - // We want to suppress the modal dialog that's shown - // when starting up in safe-mode to enable testing. - if (Services.appinfo.inSafeMode) { - this.observerService.addObserver(this, "domwindowopened", false); - } + try { + this.enabled = Services.prefs.getBoolPref(ENABLED_PREF); + } catch(e) {} + if (this.enabled) { + this.logger.info("Marionette enabled via build flag and pref"); + + // We want to suppress the modal dialog that's shown + // when starting up in safe-mode to enable testing. + if (Services.appinfo.inSafeMode) { + this.observerService.addObserver(this, "domwindowopened", false); } + } #endif - break; - case "final-ui-startup": - this.finalUiStartup = true; - this.observerService.removeObserver(this, aTopic); - this.observerService.addObserver(this, "xpcom-shutdown", false); - this.init(); - break; - case "domwindowopened": - this.observerService.removeObserver(this, aTopic); - this._suppressSafeModeDialog(aSubject); - break; - case "xpcom-shutdown": - this.observerService.removeObserver(this, "xpcom-shutdown"); - this.uninit(); - break; + break; + + case "final-ui-startup": + this.finalUiStartup = true; + this.observerService.removeObserver(this, topic); + this.observerService.addObserver(this, "xpcom-shutdown", false); + this.init(); + break; + + case "domwindowopened": + this.observerService.removeObserver(this, topic); + this.suppressSafeModeDialog_(subj); + break; + + case "xpcom-shutdown": + this.observerService.removeObserver(this, "xpcom-shutdown"); + this.uninit(); + break; + } +}; + +MarionetteComponent.prototype.suppressSafeModeDialog_ = function(win) { + // Wait for the modal dialog to finish loading. + win.addEventListener("load", function onload() { + win.removeEventListener("load", onload); + + if (win.document.getElementById("safeModeDialog")) { + // Accept the dialog to start in safe-mode + win.setTimeout(() => { + win.document.documentElement.getButton("accept").click(); + }); } - }, + }); +}; - _suppressSafeModeDialog: function mc_suppressSafeModeDialog(aWindow) { - // Wait for the modal dialog to finish loading. - aWindow.addEventListener("load", function onLoad() { - aWindow.removeEventListener("load", onLoad); +MarionetteComponent.prototype.init = function() { + if (this.loaded_ || !this.enabled || !this.finalUiStartup) { + return; + } - if (aWindow.document.getElementById("safeModeDialog")) { - aWindow.setTimeout(() => { - // Accept the dialog to start in safe-mode. - aWindow.document.documentElement.getButton("accept").click(); - }); - } - }); - }, + this.loaded_ = true; - init: function mc_init() { - if (!this._loaded && this.enabled && this.finalUiStartup) { - this._loaded = true; + let forceLocal = Services.appinfo.name == "B2G" ? false : true; + try { + forceLocal = Services.prefs.getBoolPref(FORCELOCAL_PREF); + } catch (e) {} + Services.prefs.setBoolPref(FORCELOCAL_PREF, forceLocal); - let marionette_forcelocal = this.appName == 'B2G' ? false : true; - try { - marionette_forcelocal = Services.prefs.getBoolPref(MARIONETTE_FORCELOCAL_PREF); - } - catch(e) {} - Services.prefs.setBoolPref(MARIONETTE_FORCELOCAL_PREF, marionette_forcelocal); + if (!forceLocal) { + // See bug 800138. Because the first socket that opens with + // force-local=false fails, we open a dummy socket that will fail. + // keepWhenOffline=true so that it still work when offline (local). + // This allows the following attempt by Marionette to open a socket + // to succeed. + let insaneSacrificialGoat = + new ServerSocket(666, Ci.nsIServerSocket.KeepWhenOffline, 4); + insaneSacrificialGoat.asyncListen(this); + } - if (!marionette_forcelocal) { - // See bug 800138. Because the first socket that opens with - // force-local=false fails, we open a dummy socket that will fail. - // keepWhenOffline=true so that it still work when offline (local). - // This allows the following attempt by Marionette to open a socket - // to succeed. - let insaneSacrificialGoat = new ServerSocket(666, Ci.nsIServerSocket.KeepWhenOffline, 4); - insaneSacrificialGoat.asyncListen(this); - } + let port = DEFAULT_PORT; + try { + port = Services.prefs.getIntPref(PORT_PREF); + } catch (e) {} - let port; - try { - port = Services.prefs.getIntPref('marionette.defaultPrefs.port'); - } - catch(e) { - port = 2828; - } - try { - loader.loadSubScript("chrome://marionette/content/marionette-server.js"); - let forceLocal = Services.prefs.getBoolPref(MARIONETTE_FORCELOCAL_PREF); - this._marionetteServer = new MarionetteServer(port, forceLocal); - this.logger.info("Marionette server ready"); - } - catch(e) { - this.logger.error('exception: ' + e.name + ', ' + e.message + ': ' + - e.fileName + " :: " + e.lineNumber); - } + let s; + try { + Cu.import("chrome://marionette/content/server.js"); + s = new MarionetteServer(port, forceLocal); + s.start(); + this.logger.info(`Listening on port ${s.port}`); + } catch (e) { + this.logger.error(`Error on starting server: ${e}`); + dump(e.toString() + "\n" + e.stack + "\n"); + } finally { + if (s) { + this.server = s; } - }, - - uninit: function mc_uninit() { - if (this._marionetteServer) { - this._marionetteServer.closeListener(); - } - this._loaded = false; - }, + } +}; +MarionetteComponent.prototype.uninit = function() { + if (!this.loaded_) { + return; + } + this.server.stop(); + this.loaded_ = false; }; this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MarionetteComponent]); diff --git a/testing/marionette/dispatcher.js b/testing/marionette/dispatcher.js new file mode 100644 index 0000000000..50b85b6272 --- /dev/null +++ b/testing/marionette/dispatcher.js @@ -0,0 +1,281 @@ +/* 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 {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); + +Cu.import("chrome://marionette/content/command.js"); +Cu.import("chrome://marionette/content/emulator.js"); +Cu.import("chrome://marionette/content/error.js"); +Cu.import("chrome://marionette/content/driver.js"); + +this.EXPORTED_SYMBOLS = ["Dispatcher"]; + +const logger = Log.repository.getLogger("Marionette"); +const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); + +/** + * Manages a Marionette connection, and dispatches packets received to + * their correct destinations. + * + * @param {number} connId + * Unique identifier of the connection this dispatcher should handle. + * @param {DebuggerTransport} transport + * Debugger transport connection to the client. + * @param {function(Emulator): GeckoDriver} driverFactory + * A factory function that takes an Emulator as argument and produces + * a GeckoDriver. + */ +this.Dispatcher = function(connId, transport, driverFactory) { + this.id = connId; + this.conn = transport; + + // Marionette uses a protocol based on the debugger server, which + // requires passing back actor ID's with responses. Unlike the debugger + // server, we don't actually have multiple actors, so just use a dummy + // value of "0". + this.actorId = "0"; + + // callback for when connection is closed + this.onclose = null; + + // transport hooks are Dispatcher.prototype.onPacket + // and Dispatcher.prototype.onClosed + this.conn.hooks = this; + + this.emulator = new Emulator(msg => this.sendResponse(msg, -1)); + this.driver = driverFactory(this.emulator); + this.commandProcessor = new CommandProcessor(this.driver); +}; + +/** + * Debugger transport callback that dispatches the request. + * Request handlers defined in this.requests take presedence + * over those defined in this.driver.commands. + */ +Dispatcher.prototype.onPacket = function(packet) { + logger.debug(`${this.id} -> ${packet.toSource()}`); + + if (this.requests && this.requests[packet.name]) { + this.requests[packet.name].bind(this)(packet); + } else { + let id = this.beginNewCommand(); + let ok = this.sendOk.bind(this); + let send = this.send.bind(this); + this.commandProcessor.execute(packet, ok, send, id); + } +}; + +/** + * Debugger transport callback that cleans up + * after a connection is closed. + */ +Dispatcher.prototype.onClosed = function(status) { + this.driver.sessionTearDown(); + if (this.onclose) { + this.onclose(this); + } +}; + +// Dispatcher specific command handlers: + +Dispatcher.prototype.getMarionetteID = function() { + let id = this.beginNewCommand(); + this.sendResponse({from: "root", id: this.actorId}, id); +}; + +Dispatcher.prototype.emulatorCmdResult = function(msg) { + switch (this.driver.context) { + case Context.CONTENT: + this.driver.sendAsync("emulatorCmdResult", msg); + break; + case Context.CHROME: + let cb = this.emulator.popCallback(msg.id); + if (!cb) { + return; + } + cb.result(msg); + break; + } +}; + +/** + * Quits Firefox with the provided flags and tears down the current + * session. + */ +Dispatcher.prototype.quitApplication = function(msg) { + let id = this.beginNewCommand(); + + if (this.driver.appName != "Firefox") { + this.sendError({ + "message": "In app initiated quit only supported on Firefox", + "status": 500 + }, id); + return; + } + + let flags = Ci.nsIAppStartup.eAttemptQuit; + for (let k of msg.parameters.flags) { + flags |= Ci.nsIAppStartup[k]; + } + + this.driver.sessionTearDown(); + Services.startup.quit(flags); +}; + +// Convenience methods: + +Dispatcher.prototype.sayHello = function() { + let id = this.beginNewCommand(); + let yo = {from: "root", applicationType: "gecko", traits: []}; + this.sendResponse(yo, id); +}; + +Dispatcher.prototype.sendOk = function(cmdId) { + this.sendResponse({from: this.actorId, ok: true}, cmdId); +}; + +Dispatcher.prototype.sendError = function(err, cmdId) { + let packet = { + from: this.actorId, + status: err.status, + sessionId: this.driver.sessionId, + error: err + }; + this.sendResponse(packet, cmdId); +}; + +/** + * Marshals and sends message to either client or emulator based on the + * provided {@code cmdId}. + * + * This routine produces a Marionette protocol packet, which is different + * to a WebDriver protocol response in that it contains an extra key + * {@code from} for the debugger transport actor ID. It also replaces the + * key {@code value} with {@code error} when {@code msg.status} isn't + * {@code 0}. + * + * @param {Object} msg + * Object with the properties {@code value}, {@code status}, and + * {@code sessionId}. + * @param {UUID} cmdId + * The unique identifier for the command the message is a response to. + */ +Dispatcher.prototype.send = function(msg, cmdId) { + let packet = { + from: this.actorId, + value: msg.value, + status: msg.status, + sessionId: msg.sessionId, + }; + + if (typeof packet.value == "undefined") { + packet.value = null; + } + + // the Marionette protocol sends errors using the "error" + // key instead of, as Selenium, "value" + if (!error.isSuccess(msg.status)) { + packet.error = packet.value; + delete packet.value; + } + + this.sendResponse(packet, cmdId); +}; + +// Low-level methods: + +/** + * Delegates message to client or emulator based on the provided + * {@code cmdId}. The message is sent over the debugger transport socket. + * + * The command ID is a unique identifier assigned to the client's request + * that is used to distinguish the asynchronous responses. + * + * Whilst responses to commands are synchronous and must be sent in the + * correct order, emulator callbacks are more transparent and can be sent + * at any time. These callbacks won't change the current command state. + * + * @param {Object} payload + * The payload to send. + * @param {UUID} cmdId + * The unique identifier for this payload. {@code -1} signifies + * that it's an emulator callback. + */ +Dispatcher.prototype.sendResponse = function(payload, cmdId) { + if (emulator.isCallback(cmdId)) { + this.sendToEmulator(payload); + } else { + this.sendToClient(payload, cmdId); + this.commandId = null; + } +}; + +/** + * Send message to emulator over the debugger transport socket. + * Notably this skips out-of-sync command checks. + */ +Dispatcher.prototype.sendToEmulator = function(payload) { + this.sendRaw("emulator", payload); +}; + +/** + * Send given payload as-is to the connected client over the debugger + * transport socket. + * + * If {@code cmdId} evaluates to false, the current command state isn't + * set, or the response is out-of-sync, a warning is logged and this + * routine will return (no-op). + */ +Dispatcher.prototype.sendToClient = function(payload, cmdId) { + if (!cmdId) { + logger.warn("Got response with no command ID"); + return; + } else if (this.commandId === null) { + logger.warn(`No current command, ignoring response: ${payload.toSource}`); + return; + } else if (this.isOutOfSync(cmdId)) { + logger.warn(`Ignoring out-of-sync response with command ID: ${cmdId}`); + return; + } + this.driver.responseCompleted(); + this.sendRaw("client", payload); +}; + +/** + * Sends payload as-is over debugger transport socket to client, + * and logs it. + */ +Dispatcher.prototype.sendRaw = function(dest, payload) { + logger.debug(`${this.id} ${dest} <- ${payload.toSource()}`); + this.conn.send(payload); +}; + +/** + * Begins a new command by generating a unique identifier and assigning + * it to the current command state {@code Dispatcher.prototype.commandId}. + * + * @return {UUID} + * The generated unique identifier for the current command. + */ +Dispatcher.prototype.beginNewCommand = function() { + let uuid = uuidGen.generateUUID().toString(); + this.commandId = uuid; + return uuid; +}; + +Dispatcher.prototype.isOutOfSync = function(cmdId) { + return this.commandId !== cmdId; +}; + +Dispatcher.prototype.requests = { + getMarionetteID: Dispatcher.prototype.getMarionetteID, + emulatorCmdResult: Dispatcher.prototype.emulatorCmdResult, + quitApplication: Dispatcher.prototype.quitApplication +}; diff --git a/testing/marionette/driver.js b/testing/marionette/driver.js new file mode 100644 index 0000000000..eab5ccae74 --- /dev/null +++ b/testing/marionette/driver.js @@ -0,0 +1,3305 @@ +/* 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +const loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader); + +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +this.DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js"); + +XPCOMUtils.defineLazyServiceGetter( + this, "cookieManager", "@mozilla.org/cookiemanager;1", "nsICookieManager"); + +Cu.import("chrome://marionette/content/emulator.js"); +Cu.import("chrome://marionette/content/error.js"); +Cu.import("chrome://marionette/content/marionette-elements.js"); +Cu.import("chrome://marionette/content/marionette-simpletest.js"); + +loader.loadSubScript("chrome://marionette/content/marionette-common.js"); + +// preserve this import order: +let utils = {}; +loader.loadSubScript("chrome://marionette/content/EventUtils.js", utils); +loader.loadSubScript("chrome://marionette/content/ChromeUtils.js", utils); +loader.loadSubScript("chrome://marionette/content/atoms.js", utils); +loader.loadSubScript("chrome://marionette/content/marionette-sendkeys.js", utils); +loader.loadSubScript("chrome://marionette/content/marionette-frame-manager.js"); + +this.EXPORTED_SYMBOLS = ["GeckoDriver", "Context"]; + +const FRAME_SCRIPT = "chrome://marionette/content/marionette-listener.js"; +const BROWSER_STARTUP_FINISHED = "browser-delayed-startup-finished"; +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const SECURITY_PREF = "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer"; +const CLICK_TO_START_PREF = "marionette.debugging.clicktostart"; +const CONTENT_LISTENER_PREF = "marionette.contentListener"; +const COMMON_DIALOG_LOADED = "common-dialog-loaded"; +const TABMODAL_DIALOG_LOADED = "tabmodal-dialog-loaded"; + +const logger = Log.repository.getLogger("Marionette"); +const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); +const globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"] + .getService(Ci.nsIMessageBroadcaster); +let specialpowers = {}; + +// This is used to prevent newSession from returning before the telephony +// API's are ready; see bug 792647. This assumes that marionette-server.js +// will be loaded before the 'system-message-listener-ready' message +// is fired. If this stops being true, this approach will have to change. +let systemMessageListenerReady = false; +Services.obs.addObserver(function() { + systemMessageListenerReady = true; +}, "system-message-listener-ready", false); + +// This is used on desktop to prevent newSession from returning before a page +// load initiated by the Firefox command line has completed. +let delayedBrowserStarted = false; +Services.obs.addObserver(function () { + delayedBrowserStarted = true; +}, BROWSER_STARTUP_FINISHED, false); + +this.Context = { + CHROME: "chrome", + CONTENT: "content", +}; + +this.Context.fromString = function(s) { + s = s.toUpperCase(); + if (s in this) + return this[s]; + return null; +}; + +/** + * Creates a transparent interface between the chrome- and content + * processes. + * + * Calls to this object will be proxied via the message manager to the active + * browsing context (content) and responses will be provided back as + * promises. + * + * The argument sequence is serialised and passed as an array, unless it + * consists of a single object type that isn't null, in which case it's + * passed literally. The latter specialisation is temporary to achieve + * backwards compatibility with marionette-listener.js. + * + * @param {function(): nsIMessageManager} mmFn + * Function returning the current message manager. + * @param {function(string, Object, number)} sendAsyncFn + * Callback for sending async messages to the current listener. + * @param {function(): BrowserObj} curBrowserFn + * Function that returns the current browser. + */ +let ListenerProxy = function(mmFn, sendAsyncFn, curBrowserFn) { + this.curCmdId = null; + this.sendAsync = sendAsyncFn; + this.ondialog = d => {}; + + this.mmFn_ = mmFn; + this.curBrowserFn_ = curBrowserFn; +}; + +Object.defineProperty(ListenerProxy.prototype, "mm", { + get: function() { return this.mmFn_(); } +}); + +Object.defineProperty(ListenerProxy.prototype, "curBrowser", { + get: function() { return this.curBrowserFn_(); } +}); + +ListenerProxy.prototype.__noSuchMethod__ = function*(name, args) { + const ok = "Marionette:ok"; + const val = "Marionette:done"; + const err = "Marionette:error"; + const all = [ok, val, err]; + + let proxy = new Promise((resolve, reject) => { + let listeners = []; + let obs = new Map(); + obs.add = function(modalHandler) { + if (Services.appinfo.name != "Firefox") + return; + this.set(COMMON_DIALOG_LOADED, modalHandler); + this.set(TABMODAL_DIALOG_LOADED, modalHandler); + for (let [t,o] of this) { + Services.obs.addObserver(o, t, false); + } + }; + obs.remove = function() { + for (let [t,o] of this) { + Services.obs.removeObserver(o, t); + } + }; + + let okListener = () => resolve(); + let valListener = msg => resolve(msg.json.value); + let errListener = msg => reject( + "error" in msg.objects ? msg.objects.error : msg.json); + + let handleDialogLoad = function(subject, topic) { + obs.remove(); + this.cancelRequest(); + + // we shouldn't return to the client due to the modal associated with the + // jsdebugger + let clickToStart; + try { + clickToStart = Services.prefs.getBoolPref(CLICK_TO_START_PREF); + } catch (e) {} + if (clickToStart) { + Services.prefs.setBoolPref(CLICK_TO_START_PREF, false); + return; + } + + let winr; + if (topic == COMMON_DIALOG_LOADED) + winr = Cu.getWeakReference(subject); + let d = new ModalDialog(() => this.curBrowser, winr); + this.ondialog(d); + + // shortcut to return a response immediately, + // causes next reply from listener to be out-of-sync + resolve(); + }; + + let removeListeners = (name, listenerFn) => { + let fn = msg => { + if (this.isOutOfSync(msg.json.command_id)) { + logger.warn("Skipping out-of-sync response from listener: " + + msg.name + msg.json.toSource()); + return; + } + + listeners.map(l => this.mm.removeMessageListener(l[0], l[1])); + obs.remove(); + + listenerFn(msg); + this.curCmdId = null; + }; + + listeners.push([name, fn]); + return fn; + }; + + this.mm.addMessageListener(ok, removeListeners(ok, okListener)); + this.mm.addMessageListener(val, removeListeners(val, valListener)); + this.mm.addMessageListener(err, removeListeners(err, errListener)); + + // install observers for global- and tab modal dialogues + obs.add(handleDialogLoad.bind(this)); + + // convert to array if passed arguments + let msg; + if (args.length == 1 && typeof args[0] == "object" && args[0] !== null) + msg = args[0]; + else + msg = Array.prototype.slice.call(args); + + this.sendAsync(name, msg, this.curCmdId); + }); + + return proxy; +}; + +ListenerProxy.prototype.isOutOfSync = function(id) { + return this.curCmdId !== id; +}; + +/** + * Implements (parts of) the W3C WebDriver protocol. GeckoDriver lives + * in the chrome context and mediates content calls to the listener via + * ListenerProxy. + * + * Throughout this prototype, functions with the argument {@code cmd}'s + * documentation refers to the contents of the {@code cmd.parameters} + * object. + * + * @param {string} appName + * Description of the product, for example "B2G" or "Firefox". + * @param {string} device + * Device this driver should assume. + * @param {Emulator=} emulator + * Reference to the emulator connection, if running on an emulator. + */ +this.GeckoDriver = function(appName, device, emulator) { + this.appName = appName; + this.emulator = emulator; + + this.sessionId = null; + // holds list of BrowserObjs + this.browsers = {}; + // points to current browser + this.curBrowser = null; + this.context = Context.CONTENT; + this.scriptTimeout = null; + this.searchTimeout = null; + this.pageTimeout = null; + this.timer = null; + this.inactivityTimer = null; + // called by simpletest methods + this.heartbeatCallback = function() {}; + this.marionetteLog = new MarionetteLogObj(); + // topmost chrome frame + this.mainFrame = null; + // chrome iframe that currently has focus + this.curFrame = null; + this.mainContentFrameId = null; + this.importedScripts = FileUtils.getFile("TmpD", ["marionetteChromeScripts"]); + this.importedScriptHashes = {}; + this.importedScriptHashes[Context.CONTENT] = []; + this.importedScriptHashes[Context.CHROME] = []; + this.currentFrameElement = null; + this.testName = null; + this.mozBrowserClose = null; + this.enabled_security_pref = false; + this.sandbox = null; + // frame ID of the current remote frame, used for mozbrowserclose events + this.oopFrameId = null; + this.observing = null; + this._browserIds = new WeakMap(); + this.dialog = null; + + this.sessionCapabilities = { + // Mandated capabilities + "browserName": this.appName, + "browserVersion": Services.appinfo.version, + "platformName": Services.appinfo.OS.toUpperCase(), + "platformVersion": Services.appinfo.platformVersion, + + // Supported features + "handlesAlerts": false, + "nativeEvents": false, + "raisesAccessibilityExceptions": false, + "rotatable": this.appName == "B2G", + "secureSsl": false, + "takesElementScreenshot": true, + "takesScreenshot": true, + + // Selenium 2 compat + "platform": Services.appinfo.OS.toUpperCase(), + + // Proprietary extensions + "XULappId" : Services.appinfo.ID, + "appBuildId" : Services.appinfo.appBuildID, + "device": device, + "version": Services.appinfo.version + }; + + this.mm = globalMessageManager; + this.listener = new ListenerProxy( + () => this.mm, + this.sendAsync.bind(this), + () => this.curBrowser); + this.listener.ondialog = d => this.dialog = d; +}; + +GeckoDriver.prototype.QueryInterface = XPCOMUtils.generateQI([ + Ci.nsIMessageListener, + Ci.nsIObserver, + Ci.nsISupportsWeakReference +]); + +/** + * Switches to the global ChromeMessageBroadcaster, potentially replacing + * a frame-specific ChromeMessageSender. Has no effect if the global + * ChromeMessageBroadcaster is already in use. If this replaces a + * frame-specific ChromeMessageSender, it removes the message listeners + * from that sender, and then puts the corresponding frame script "to + * sleep", which removes most of the message listeners from it as well. + */ +GeckoDriver.prototype.switchToGlobalMessageManager = function() { + if (this.curBrowser && this.curBrowser.frameManager.currentRemoteFrame !== null) { + this.curBrowser.frameManager.removeMessageManagerListeners(this.mm); + this.sendAsync("sleepSession"); + this.curBrowser.frameManager.currentRemoteFrame = null; + } + this.mm = globalMessageManager; +}; + +/** + * Helper method to send async messages to the content listener. + * Correct usage is to pass in the name of a function in marionette-listener.js, + * a message object consisting of JSON serialisable primitives, + * and the current command's ID. + * + * @param {string} name + * Suffix of the targetted message listener ({@code Marionette:}). + * @param {Object=} msg + * JSON serialisable object to send to the listener. + * @param {number=} cmdId + * Command ID to ensure synchronisity. + */ +GeckoDriver.prototype.sendAsync = function(name, msg, cmdId) { + let curRemoteFrame = this.curBrowser.frameManager.currentRemoteFrame; + name = `Marionette:${name}`; + + if (cmdId) + msg.command_id = cmdId; + + if (curRemoteFrame === null) { + this.curBrowser.executeWhenReady(() => { + this.mm.broadcastAsyncMessage(name + this.curBrowser.curFrameId, msg); + }); + } else { + let remoteFrameId = curRemoteFrame.targetFrameId; + try { + this.mm.sendAsyncMessage(name + remoteFrameId, msg); + } catch (e) { + switch(e.result) { + case Components.results.NS_ERROR_FAILURE: + throw new FrameSendFailureError(curRemoteFrame); + case Components.results.NS_ERROR_NOT_INITIALIZED: + throw new FrameSendNotInitializedError(curRemoteFrame); + default: + throw new WebDriverError(e.toString()); + } + } + } +}; + +/** + * Gets the current active window. + * + * @return {nsIDOMWindow} + */ +GeckoDriver.prototype.getCurrentWindow = function() { + let typ = null; + if (this.curFrame == null) { + if (this.curBrowser == null) { + if (this.context == Context.CONTENT) { + typ = 'navigator:browser'; + } + return Services.wm.getMostRecentWindow(typ); + } else { + return this.curBrowser.window; + } + } else { + return this.curFrame; + } +}; + +/** + * Gets the the window enumerator. + * + * @return {nsISimpleEnumerator} + */ +GeckoDriver.prototype.getWinEnumerator = function() { + let typ = null; + if (this.appName != "B2G" && this.context == Context.CONTENT) { + typ = "navigator:browser"; + } + return Services.wm.getEnumerator(typ); +}; + +GeckoDriver.prototype.addFrameCloseListener = function(action) { + let win = this.getCurrentWindow(); + this.mozBrowserClose = e => { + if (e.target.id == this.oopFrameId) { + win.removeEventListener("mozbrowserclose", this.mozBrowserClose, true); + this.switchToGlobalMessageManager(); + throw new FrameSendFailureError( + `The frame closed during the ${action}, recovering to allow further communications`); + } + }; + win.addEventListener("mozbrowserclose", this.mozBrowserClose, true); +}; + +/** + * Create a new BrowserObj for window and add to known browsers. + * + * @param {nsIDOMWindow} win + * Window for which we will create a BrowserObj. + * + * @return {string} + * Returns the unique server-assigned ID of the window. + */ +GeckoDriver.prototype.addBrowser = function(win) { + let browser = new BrowserObj(win, this); + let winId = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils).outerWindowID; + winId = winId + ((this.appName == "B2G") ? "-b2g" : ""); + this.browsers[winId] = browser; + this.curBrowser = this.browsers[winId]; + if (typeof this.curBrowser.elementManager.seenItems[winId] == "undefined") { + // add this to seenItems so we can guarantee + // the user will get winId as this window's id + this.curBrowser.elementManager.seenItems[winId] = Cu.getWeakReference(win); + } +}; + +/** + * Registers a new browser, win, with Marionette. + * + * If we have not seen the browser content window before, the listener + * frame script will be loaded into it. If isNewSession is true, we will + * switch focus to the start frame when it registers. + * + * @param {nsIDOMWindow} win + * Window whose browser we need to access. + * @param {boolean=false} isNewSession + * True if this is the first time we're talking to this browser. + */ +GeckoDriver.prototype.startBrowser = function(win, isNewSession=false) { + this.mainFrame = win; + this.curFrame = null; + this.addBrowser(win); + this.curBrowser.isNewSession = isNewSession; + this.curBrowser.startSession(isNewSession, win, this.whenBrowserStarted.bind(this)); +}; + +/** + * Callback invoked after a new session has been started in a browser. + * Loads the Marionette frame script into the browser if needed. + * + * @param {nsIDOMWindow} win + * Window whose browser we need to access. + * @param {boolean} isNewSession + * True if this is the first time we're talking to this browser. + */ +GeckoDriver.prototype.whenBrowserStarted = function(win, isNewSession) { + utils.window = win; + + try { + let mm = win.window.messageManager; + if (!isNewSession) { + // Loading the frame script corresponds to a situation we need to + // return to the server. If the messageManager is a message broadcaster + // with no children, we don't have a hope of coming back from this call, + // so send the ack here. Otherwise, make a note of how many child scripts + // will be loaded so we known when it's safe to return. + if (mm.childCount != 0) { + this.curBrowser.frameRegsPending = mm.childCount; + } + } + + if (!Services.prefs.getBoolPref("marionette.contentListener") || !isNewSession) { + mm.loadFrameScript(FRAME_SCRIPT, true, true); + Services.prefs.setBoolPref("marionette.contentListener", true); + } + } catch (e) { + // there may not always be a content process + logger.error( + `Could not load listener into content for page ${win.location.href}: ${e}`); + } +}; + +/** + * Recursively get all labeled text. + * + * @param {nsIDOMElement} el + * The parent element. + * @param {Array.} lines + * Array that holds the text lines. + */ +GeckoDriver.prototype.getVisibleText = function(el, lines) { + try { + if (utils.isElementDisplayed(el)) { + if (el.value) { + lines.push(el.value); + } + for (let child in el.childNodes) { + this.getVisibleText(el.childNodes[child], lines); + } + } + } catch (e) { + if (el.nodeName == "#text") { + lines.push(el.textContent); + } + } +}; + +/** + * Given a file name, this will delete the file from the temp directory + * if it exists. + * + * @param {string} filename + */ +GeckoDriver.prototype.deleteFile = function(filename) { + let file = FileUtils.getFile("TmpD", [filename.toString()]); + if (file.exists()) + file.remove(true); +}; + +/** + * Handles registration of new content listener browsers. Depending on + * their type they are either accepted or ignored. + */ +GeckoDriver.prototype.registerBrowser = function(id, be) { + let nullPrevious = this.curBrowser.curFrameId == null; + let listenerWindow = Services.wm.getOuterWindowWithId(id); + + // go in here if we're already in a remote frame + if (this.curBrowser.frameManager.currentRemoteFrame !== null && + (!listenerWindow || this.mm == this.curBrowser.frameManager + .currentRemoteFrame.messageManager.get())) { + // The outerWindowID from an OOP frame will not be meaningful to + // the parent process here, since each process maintains its own + // independent window list. So, it will either be null (!listenerWindow) + // if we're already in a remote frame, or it will point to some + // random window, which will hopefully cause an href mismatch. + // Currently this only happens in B2G for OOP frames registered in + // Marionette:switchToFrame, so we'll acknowledge the switchToFrame + // message here. + // + // TODO: Should have a better way of determining that this message + // is from a remote frame. + this.curBrowser.frameManager.currentRemoteFrame.targetFrameId = + this.generateFrameId(id); + } + + let reg = {}; + // this will be sent to tell the content process if it is the main content + let mainContent = this.curBrowser.mainContentId == null; + if (be.getAttribute("type") != "content") { + // curBrowser holds all the registered frames in knownFrames + let uid = this.generateFrameId(id); + reg.id = uid; + reg.remotenessChange = this.curBrowser.register(uid, be); + } + + // set to true if we updated mainContentId + mainContent = mainContent == true && + this.curBrowser.mainContentId != null; + if (mainContent) + this.mainContentFrameId = this.curBrowser.curFrameId; + + this.curBrowser.elementManager.seenItems[reg.id] = + Cu.getWeakReference(listenerWindow); + if (nullPrevious && (this.curBrowser.curFrameId != null)) { + this.sendAsync("newSession", + { + B2G: (this.appName == "B2G"), + raisesAccessibilityExceptions: + this.sessionCapabilities.raisesAccessibilityExceptions + }, + this.newSessionCommandId); + if (this.curBrowser.isNewSession) + this.newSessionCommandId = null; + } + + return [reg, mainContent]; +}; + +GeckoDriver.prototype.registerPromise = function() { + const li = "Marionette:register"; + + return new Promise((resolve) => { + this.mm.addMessageListener(li, function cb(msg) { + let wid = msg.json.value; + let be = msg.target; + let rv = this.registerBrowser(wid, be); + + if (this.curBrowser.frameRegsPending > 0) + this.curBrowser.frameRegsPending--; + + if (this.curBrowser.frameRegsPending == 0) { + this.mm.removeMessageListener(li, cb); + resolve(); + } + + // this is a sync message and listeners expect the ID back + return rv; + }.bind(this)); + }); +}; + +GeckoDriver.prototype.listeningPromise = function() { + const li = "Marionette:listenersAttached"; + return new Promise((resolve) => { + this.mm.addMessageListener(li, function() { + this.mm.removeMessageListener(li, this); + resolve(); + }.bind(this)); + }); +}; + +/** Create a new session. */ +GeckoDriver.prototype.newSession = function(cmd, resp) { + this.sessionId = cmd.parameters.sessionId || + cmd.parameters.session_id || + uuidGen.generateUUID().toString(); + + this.newSessionCommandId = cmd.id; + this.setSessionCapabilities(cmd.parameters.capabilities); + this.scriptTimeout = 10000; + + // SpecialPowers requires insecure automation-only features that we + // put behind a pref + let sec = false; + try { + sec = Services.prefs.getBoolPref(SECURITY_PREF); + } catch (e) {} + if (!sec) { + this.enabled_security_pref = true; + Services.prefs.setBoolPref(SECURITY_PREF, true); + } + + if (!specialpowers.hasOwnProperty("specialPowersObserver")) { + loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserver.js", + specialpowers); + specialpowers.specialPowersObserver = new specialpowers.SpecialPowersObserver(); + specialpowers.specialPowersObserver.init(); + specialpowers.specialPowersObserver._loadFrameScript(); + } + + let registerBrowsers = this.registerPromise(); + let browserListening = this.listeningPromise(); + + let waitForWindow = function() { + let win = this.getCurrentWindow(); + if (!win) { + // if the window isn't even created, just poll wait for it + let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + checkTimer.initWithCallback(waitForWindow.bind(this), 100, + Ci.nsITimer.TYPE_ONE_SHOT); + } else if (win.document.readyState != "complete") { + // otherwise, wait for it to be fully loaded before proceeding + let listener = ev => { + // ensure that we proceed, on the top level document load event + // (not an iframe one...) + if (ev.target != win.document) + return; + win.removeEventListener("load", listener); + waitForWindow.call(this); + }; + win.addEventListener("load", listener, true); + } else { + let clickToStart; + try { + clickToStart = Services.prefs.getBoolPref(CLICK_TO_START_PREF); + } catch (e) {} + if (clickToStart && (this.appName != "B2G")) { + let pService = Cc["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Ci.nsIPromptService); + pService.alert(win, "", "Click to start execution of marionette tests"); + } + this.startBrowser(win, true); + } + }; + + let runSessionStart = function() { + if (!Services.prefs.getBoolPref(CONTENT_LISTENER_PREF)) { + waitForWindow.call(this); + } else if (this.appName != "Firefox" && this.curBrowser === null) { + // if there is a content listener, then we just wake it up + this.addBrowser(this.getCurrentWindow()); + this.curBrowser.startSession(this.whenBrowserStarted.bind(this)); + this.mm.broadcastAsyncMessage("Marionette:restart", {}); + } else { + throw new WebDriverError("Session already running"); + } + this.switchToGlobalMessageManager(); + }; + + if (!delayedBrowserStarted && this.appName != "B2G") { + let self = this; + Services.obs.addObserver(function onStart() { + Services.obs.removeObserver(onStart, BROWSER_STARTUP_FINISHED); + runSessionStart.call(self); + }, BROWSER_STARTUP_FINISHED, false); + } else { + runSessionStart.call(this); + } + + yield registerBrowsers; + yield browserListening; + + resp.sessionId = this.sessionId; + resp.value = this.sessionCapabilities; +}; + +/** + * Send the current session's capabilities to the client. + * + * Capabilities informs the client of which WebDriver features are + * supported by Firefox and Marionette. They are immutable for the + * length of the session. + * + * The return value is an immutable map of string keys + * ("capabilities") to values, which may be of types boolean, + * numerical or string. + */ +GeckoDriver.prototype.getSessionCapabilities = function(cmd, resp) { + resp.value = this.sessionCapabilities; +}; + +/** + * Update the sessionCapabilities object with the keys that have been + * passed in when a new session is created. + * + * This part of the WebDriver spec is currently in flux, see + * http://lists.w3.org/Archives/Public/public-browser-tools-testing/2014OctDec/0000.html + * + * This is not a public API, only available when a new session is + * created. + * + * @param {Object} newCaps + * Key/value dictionary to overwrite session's current capabilities. + */ +GeckoDriver.prototype.setSessionCapabilities = function(newCaps) { + const copy = (from, to={}) => { + let errors = {}; + + for (let key in from) { + if (key === "desiredCapabilities") { + // Keeping desired capabilities separate for now so that we can keep + // backwards compatibility + to = copy(from[key], to); + } else if (key === "requiredCapabilities") { + for (let caps in from[key]) { + if (from[key][caps] !== this.sessionCapabilities[caps]) { + errors[caps] = from[key][caps] + " does not equal " + + this.sessionCapabilities[caps]; + } + } + } + to[key] = from[key]; + } + + if (Object.keys(errors).length == 0) + return to; + + throw new SessionNotCreatedError( + `Not all requiredCapabilities could be met: ${JSON.stringify(errors)}`); + }; + + // clone, overwrite, and set + let caps = copy(this.sessionCapabilities); + caps = copy(newCaps, caps); + this.sessionCapabilities = caps; +}; + +/** + * Log message. Accepts user defined log-level. + * + * @param {string} value + * Log message. + * @param {string} level + * Arbitrary log level. + */ +GeckoDriver.prototype.log = function(cmd, resp) { + this.marionetteLog.log(cmd.parameters.value, cmd.parameters.level); +}; + +/** Return all logged messages. */ +GeckoDriver.prototype.getLogs = function(cmd, resp) { + resp.value = this.marionetteLog.getLogs(); +}; + +/** + * Sets the context of the subsequent commands to be either "chrome" or + * "content". + * + * @param {string} value + * Name of the context to be switched to. Must be one of "chrome" or + * "content". + */ +GeckoDriver.prototype.setContext = function(cmd, resp) { + let val = cmd.parameters.value; + let ctx = Context.fromString(val); + if (ctx === null) + throw new WebDriverError(`Invalid context: ${val}`); + this.context = ctx; +}; + +/** Gets the context of the server, either "chrome" or "content". */ +GeckoDriver.prototype.getContext = function(cmd, resp) { + resp.value = this.context.toString(); +}; + +/** + * Returns a chrome sandbox that can be used by the execute and + * executeWithCallback functions. + * + * @param {nsIDOMWindow} win + * Window in which we will execute code. + * @param {Marionette} mn + * Marionette test instance. + * @param {Object} args + * Arguments given by client. + * @param {boolean} sp + * True to enable special powers in the sandbox, false not to. + * + * @return {nsIXPCComponents_utils_Sandbox} + * Returns the sandbox. + */ +GeckoDriver.prototype.createExecuteSandbox = function(win, mn, sp) { + let sb = new Cu.Sandbox(win, + {sandboxPrototype: win, wantXrays: false, sandboxName: ""}); + sb.global = sb; + sb.testUtils = utils; + + mn.exports.forEach(function(fn) { + try { + sb[fn] = mn[fn].bind(mn); + } catch(e) { + sb[fn] = mn[fn]; + } + }); + + sb.isSystemMessageListenerReady = () => systemMessageListenerReady; + + if (sp) { + let pow = [ + "chrome://specialpowers/content/specialpowersAPI.js", + "chrome://specialpowers/content/SpecialPowersObserverAPI.js", + "chrome://specialpowers/content/ChromePowers.js", + ]; + pow.map(s => loader.loadSubScript(s, sb)); + } + + return sb; +}; + +/** + * Apply arguments sent from the client to the current (possibly reused) + * execution sandbox. + */ +GeckoDriver.prototype.applyArgumentsToSandbox = function(win, sb, args) { + sb.__marionetteParams = this.curBrowser.elementManager.convertWrappedArguments(args, win); + sb.__namedArgs = this.curBrowser.elementManager.applyNamedArgs(args); +}; + +/** + * Executes a script in the given sandbox. + * + * @param {Response} resp + * Response object given to the command calling this routine. + * @param {nsIXPCComponents_utils_Sandbox} sandbox + * Sandbox in which the script will run. + * @param {string} script + * Script to run. + * @param {boolean} directInject + * If true, then the script will be run as is, and not as a function + * body (as you would do using the WebDriver spec). + * @param {boolean} async + * True if the script is asynchronous. + * @param {number} timeout + * When to interrupt script in milliseconds. + */ +GeckoDriver.prototype.executeScriptInSandbox = function( + resp, + sandbox, + script, + directInject, + async, + timeout) { + if (directInject && async && (timeout == null || timeout == 0)) + throw new TimeoutError("Please set a timeout"); + + if (this.importedScripts.exists()) { + let stream = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + stream.init(this.importedScripts, -1, 0, 0); + let data = NetUtil.readInputStreamToString(stream, stream.available()); + stream.close(); + script = data + script; + } + + let res = Cu.evalInSandbox(script, sandbox, "1.8", "dummy file", 0); + + if (directInject && !async && + (res == undefined || res.passed == undefined)) + throw new WebDriverError("finish() not called"); + + if (!async) { + // It's fine to pass on and modify resp here because + // executeScriptInSandbox is the last function to be called + // in execute and executeWithCallback respectively. + resp.value = this.curBrowser.elementManager.wrapValue(res); + } +}; + +/** + * Execute the given script either as a function body or directly (for + * mochitest-like JS Marionette tests). + * + * If directInject is ture, it will run directly and not as a function + * body. + */ +GeckoDriver.prototype.execute = function(cmd, resp, directInject) { + let {inactivityTimeout, + scriptTimeout, + script, + newSandbox, + args, + specialPowers, + filename, + line} = cmd.parameters; + + if (!scriptTimeout) + scriptTimeout = this.scriptTimeout; + if (typeof newSandbox == "undefined") + newSandbox = true; + + if (this.context == Context.CONTENT) { + resp.value = yield this.listener.executeScript({ + script: script, + args: args, + newSandbox: newSandbox, + timeout: scriptTimeout, + specialPowers: specialPowers, + filename: filename, + line: line + }); + return; + } + + // handle the inactivity timeout + let that = this; + if (inactivityTimeout) { + let setTimer = function() { + that.inactivityTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + if (that.inactivityTimer != null) { + that.inactivityTimer.initWithCallback(function() { + throw new ScriptTimeoutError("timed out due to inactivity"); + }, inactivityTimeout, Ci.nsITimer.TYPE_ONE_SHOT); + } + }; + setTimer(); + this.heartbeatCallback = function() { + that.inactivityTimer.cancel(); + setTimer(); + }; + } + + let win = this.getCurrentWindow(); + if (!this.sandbox || newSandbox) { + let marionette = new Marionette( + this, + win, + "chrome", + this.marionetteLog, + scriptTimeout, + this.heartbeatCallback, + this.testName); + this.sandbox = this.createExecuteSandbox( + win, + marionette, + specialPowers); + if (!this.sandbox) + return; + } + this.applyArgumentsToSandbox(win, this.sandbox, args); + + try { + this.sandbox.finish = () => { + if (this.inactivityTimer != null) + this.inactivityTimer.cancel(); + return this.sandbox.generate_results(); + }; + + if (!directInject) + script = `let func = function() { ${script} }; func.apply(null, __marionetteParams);`; + this.executeScriptInSandbox( + resp, + this.sandbox, + script, + directInject, + false /* async */, + scriptTimeout); + } catch (e) { + throw new JavaScriptError(e, "execute_script", filename, line, script); + } +}; + +/** + * Set the timeout for asynchronous script execution. + * + * @param {number} ms + * Time in milliseconds. + */ +GeckoDriver.prototype.setScriptTimeout = function(cmd, resp) { + let ms = parseInt(cmd.parameters.ms); + if (isNaN(ms)) + throw new WebDriverError("Not a Number"); + this.scriptTimeout = ms; +}; + +/** + * Execute pure JavaScript. Used to execute mochitest-like Marionette + * tests. + */ +GeckoDriver.prototype.executeJSScript = function(cmd, resp) { + // TODO(ato): cmd.newSandbox doesn't ever exist? + // All pure JS scripts will need to call + // Marionette.finish() to complete the test + if (typeof cmd.newSandbox == "undefined") { + // If client does not send a value in newSandbox, + // then they expect the same behaviour as WebDriver. + cmd.newSandbox = true; + } + + switch (this.context) { + case Context.CHROME: + if (cmd.parameters.async) + yield this.executeWithCallback(cmd, resp, cmd.parameters.async); + else + this.execute(cmd, resp, true /* async */); + break; + + case Context.CONTENT: + resp.value = yield this.listener.executeJSScript({ + script: cmd.parameters.script, + args: cmd.parameters.args, + newSandbox: cmd.parameters.newSandbox, + async: cmd.parameters.async, + timeout: cmd.parameters.scriptTimeout ? + cmd.parameters.scriptTimeout : this.scriptTimeout, + inactivityTimeout: cmd.parameters.inactivityTimeout, + specialPowers: cmd.parameters.specialPowers, + filename: cmd.parameters.filename, + line: cmd.parameters.line, + }); + break; + } +}; + +/** + * This function is used by executeAsync and executeJSScript to execute + * a script in a sandbox. + * + * For executeJSScript, it will return a message only when the finish() + * method is called. + * + * For executeAsync, it will return a response when + * {@code marionetteScriptFinished} (equivalent to + * {@code arguments[arguments.length-1]}) function is called, + * or if it times out. + * + * If directInject is true, it will be run directly and not as a + * function body. + */ +GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) { + let {script, + args, + newSandbox, + inactivityTimeout, + scriptTimeout, + specialPowers, + filename, + line} = cmd.parameters; + + if (!scriptTimeout) + scriptTimeout = this.scriptTimeout; + if (typeof newSandbox == "undefined") + newSandbox = true; + + if (this.context == Context.CONTENT) { + resp.value = yield this.listener.executeAsyncScript({ + script: script, + args: args, + id: cmd.id, + newSandbox: newSandbox, + timeout: scriptTimeout, + inactivityTimeout: inactivityTimeout, + specialPowers: specialPowers, + filename: filename, + line: line + }); + return; + } + + // handle the inactivity timeout + let that = this; + if (inactivityTimeout) { + this.inactivityTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + if (this.inactivityTimer != null) { + this.inactivityTimer.initWithCallback(function() { + chromeAsyncReturnFunc(new ScriptTimeoutError("timed out due to inactivity")); + }, inactivityTimeout, Ci.nsITimer.TYPE_ONE_SHOT); + } + this.heartbeatCallback = function resetInactivityTimer() { + that.inactivityTimer.cancel(); + that.inactivityTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + if (that.inactivityTimer != null) { + that.inactivityTimer.initWithCallback(function() { + chromeAsyncReturnFunc(new ScriptTimeoutError("timed out due to inactivity")); + }, inactivityTimeout, Ci.nsITimer.TYPE_ONE_SHOT); + } + }; + } + + let win = this.getCurrentWindow(); + let origOnError = win.onerror; + that.timeout = scriptTimeout; + + let res = yield new Promise(function(resolve, reject) { + let chromeAsyncReturnFunc = function(val) { + if (that.emulator.cbs.length > 0) { + that.emulator.cbs = []; + throw new WebDriverError("Emulator callback still pending when finish() called"); + } + + if (cmd.id == that.sandbox.command_id) { + if (that.timer != null) { + that.timer.cancel(); + that.timer = null; + } + + win.onerror = origOnError; + + if (error.isError(val)) + reject(val); + else + resolve(val); + } + + if (that.inactivityTimer != null) + that.inactivityTimer.cancel(); + }; + + let chromeAsyncFinish = function() { + let res = that.sandbox.generate_results(); + chromeAsyncReturnFunc(res); + }; + + let chromeAsyncError = function(e, func, file, line, script) { + let err = new JavaScriptError(e, func, file, line, script); + chromeAsyncReturnFunc(err); + }; + + if (!this.sandbox || newSandbox) { + let marionette = new Marionette( + this, + win, + "chrome", + this.marionetteLog, + scriptTimeout, + this.heartbeatCallback, + this.testName); + this.sandbox = this.createExecuteSandbox(win, marionette, specialPowers); + if (!this.sandbox) + return; + } + this.sandbox.command_id = cmd.id; + this.sandbox.runEmulatorCmd = (cmd, cb) => { + let ecb = new EmulatorCallback(); + ecb.onresult = cb; + ecb.onerror = chromeAsyncError; + this.emulator.pushCallback(ecb); + this.emulator.send({emulator_cmd: cmd, id: ecb.id}); + }; + this.sandbox.runEmulatorShell = (args, cb) => { + let ecb = new EmulatorCallback(); + ecb.onresult = cb; + ecb.onerror = chromeAsyncError; + this.emulator.pushCallback(ecb); + this.emulator.send({emulator_shell: args, id: ecb.id}); + }; + this.applyArgumentsToSandbox(win, this.sandbox, args); + + // NB: win.onerror is not hooked by default due to the inability to + // differentiate content exceptions from chrome exceptions. See bug + // 1128760 for more details. A debug_script flag can be set to + // reenable onerror hooking to help debug test scripts. + if (cmd.parameters.debug_script) { + win.onerror = function(msg, url, line) { + let err = new JavaScriptError(`${msg} at: ${url} line: ${line}`); + chromeAsyncReturnFunc(err); + return true; + }; + } + + try { + this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + if (this.timer != null) { + this.timer.initWithCallback(function() { + chromeAsyncReturnFunc(new ScriptTimeoutError("timed out")); + }, that.timeout, Ci.nsITimer.TYPE_ONE_SHOT); + } + + this.sandbox.returnFunc = chromeAsyncReturnFunc; + this.sandbox.finish = chromeAsyncFinish; + + if (!directInject) { + script = "__marionetteParams.push(returnFunc);" + + "let marionetteScriptFinished = returnFunc;" + + "let __marionetteFunc = function() {" + script + "};" + + "__marionetteFunc.apply(null, __marionetteParams);"; + } + + this.executeScriptInSandbox( + resp, + this.sandbox, + script, + directInject, + true /* async */, + scriptTimeout); + } catch (e) { + chromeAsyncError(e, "execute_async_script", filename, line, script); + } + }.bind(this)); + + resp.value = that.curBrowser.elementManager.wrapValue(res); +}; + +/** + * Navigate to to given URL. + * + * This will follow redirects issued by the server. When the method + * returns is based on the page load strategy that the user has + * selected. + * + * Documents that contain a META tag with the "http-equiv" attribute + * set to "refresh" will return if the timeout is greater than 1 + * second and the other criteria for determining whether a page is + * loaded are met. When the refresh period is 1 second or less and + * the page load strategy is "normal" or "conservative", it will + * wait for the page to complete loading before returning. + * + * If any modal dialog box, such as those opened on + * window.onbeforeunload or window.alert, is opened at any point in + * the page load, it will return immediately. + * + * If a 401 response is seen by the browser, it will return + * immediately. That is, if BASIC, DIGEST, NTLM or similar + * authentication is required, the page load is assumed to be + * complete. This does not include FORM-based authentication. + * + * @param {string} url + * URL to navigate to. + */ +GeckoDriver.prototype.get = function(cmd, resp) { + let url = cmd.parameters.url; + + switch (this.context) { + case Context.CONTENT: + // If a remoteness update interrupts our page load, this will never return + // We need to re-issue this request to correctly poll for readyState and + // send errors. + this.curBrowser.pendingCommands.push(() => { + cmd.parameters.command_id = this.listener.curCmdId; + this.mm.broadcastAsyncMessage( + "Marionette:pollForReadyState" + this.curBrowser.curFrameId, + cmd.parameters); + }); + yield this.listener.get({url: url, pageTimeout: this.pageTimeout}); + break; + + case Context.CHROME: + // At least on desktop, navigating in chrome scope does not + // correspond to something a user can do, and leaves marionette + // and the browser in an unusable state. Return a generic error insted. + // TODO: Error codes need to be refined as a part of bug 1100545 and + // bug 945729. + if (this.appName == "Firefox") + throw new UnknownError("Cannot navigate in chrome context"); + + this.getCurrentWindow().location.href = url; + yield this.pageLoadPromise(); + break; + } +}; + +GeckoDriver.prototype.pageLoadPromise = function() { + let win = this.getCurrentWindow(); + let timeout = this.pageTimeout; + let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + let start = new Date().getTime(); + let end = null; + + return new Promise((resolve) => { + let checkLoad = function() { + end = new Date().getTime(); + let elapse = end - start; + if (timeout == null || elapse <= timeout) { + if (win.document.readyState == "complete") + resolve(); + else + checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT); + } else { + throw new UnknownError("Error loading page"); + } + }; + checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT); + }); +}; + +/** + * Get a string representing the current URL. + * + * On Desktop this returns a string representation of the URL of the + * current top level browsing context. This is equivalent to + * document.location.href. + * + * When in the context of the chrome, this returns the canonical URL + * of the current resource. + */ +GeckoDriver.prototype.getCurrentUrl = function(cmd, resp) { + switch (this.context) { + case Context.CHROME: + resp.value = this.getCurrentWindow().location.href; + break; + + case Context.CONTENT: + let isB2G = this.appName == "B2G"; + resp.value = yield this.listener.getCurrentUrl({isB2G: isB2G}); + break; + } +}; + +/** Gets the current title of the window. */ +GeckoDriver.prototype.getTitle = function(cmd, resp) { + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + resp.value = win.document.documentElement.getAttribute("title"); + break; + + case Context.CONTENT: + resp.value = yield this.listener.getTitle(); + break; + } +}; + +/** Gets the current type of the window. */ +GeckoDriver.prototype.getWindowType = function(cmd, resp) { + let win = this.getCurrentWindow(); + resp.value = win.document.documentElement.getAttribute("windowtype"); +}; + +/** Gets the page source of the content document. */ +GeckoDriver.prototype.getPageSource = function(cmd, resp) { + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let s = new win.XMLSerializer(); + resp.value = s.serializeToString(win.document); + break; + + case Context.CONTENT: + resp.value = yield this.listener.getPageSource(); + break; + } +}; + +/** Go back in history. */ +GeckoDriver.prototype.goBack = function(cmd, resp) { + yield this.listener.goBack(); +}; + +/** Go forward in history. */ +GeckoDriver.prototype.goForward = function(cmd, resp) { + yield this.listener.goForward(); +}; + +/** Refresh the page. */ +GeckoDriver.prototype.refresh = function(cmd, resp) { + yield this.listener.refresh(); +}; + +/** + * Get the current window's handle. On desktop this typically corresponds + * to the currently selected tab. + * + * Return an opaque server-assigned identifier to this window that + * uniquely identifies it within this Marionette instance. This can + * be used to switch to this window at a later point. + * + * @return {string} + * Unique window handle. + */ +GeckoDriver.prototype.getWindowHandle = function(cmd, resp) { + // curFrameId always holds the current tab. + if (this.curBrowser.curFrameId && this.appName != "B2G") { + resp.value = this.curBrowser.curFrameId; + return; + } + + for (let i in this.browsers) { + if (this.curBrowser == this.browsers[i]) { + resp.value = i; + return; + } + } +}; + +/** + * Forces an update for the given browser's id. + */ +GeckoDriver.prototype.updateIdForBrowser = function (browser, newId) { + this._browserIds.set(browser.permanentKey, newId); +}; + +/** + * Retrieves a listener id for the given xul browser element. In case + * the browser is not known, an attempt is made to retrieve the id from + * a CPOW, and null is returned if this fails. + */ +GeckoDriver.prototype.getIdForBrowser = function getIdForBrowser(browser) { + if (browser === null) { + return null; + } + let permKey = browser.permanentKey; + if (this._browserIds.has(permKey)) { + return this._browserIds.get(permKey); + } + + let winId = browser.outerWindowID; + if (winId) { + winId += ""; + this._browserIds.set(permKey, winId); + return winId; + } + return null; +}, + +/** + * Get a list of top-level browsing contexts. On desktop this typically + * corresponds to the set of open tabs. + * + * Each window handle is assigned by the server and is guaranteed unique, + * however the return array does not have a specified ordering. + * + * @return {Array.} + * Unique window handles. + */ +GeckoDriver.prototype.getWindowHandles = function(cmd, resp) { + let rv = []; + let winEn = this.getWinEnumerator(); + while (winEn.hasMoreElements()) { + let win = winEn.getNext(); + if (win.gBrowser && this.appName != "B2G") { + let tabbrowser = win.gBrowser; + for (let i = 0; i < tabbrowser.browsers.length; ++i) { + let winId = this.getIdForBrowser(tabbrowser.getBrowserAtIndex(i)); + if (winId !== null) { + rv.push(winId); + } + } + } else { + // XUL Windows, at least, do not have gBrowser + let winId = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; + winId += (this.appName == "B2G") ? "-b2g" : ""; + rv.push(winId); + } + } + resp.value = rv; +}; + +/** + * Get the current window's handle. This corresponds to a window that + * may itself contain tabs. + * + * Return an opaque server-assigned identifier to this window that + * uniquely identifies it within this Marionette instance. This can + * be used to switch to this window at a later point. + * + * @return {string} + * Unique window handle. + */ +GeckoDriver.prototype.getChromeWindowHandle = function(cmd, resp) { + for (let i in this.browsers) { + if (this.curBrowser == this.browsers[i]) { + resp.value = i; + return; + } + } +}; + +/** + * Returns identifiers for each open chrome window for tests interested in + * managing a set of chrome windows and tabs separately. + * + * @return {Array.} + * Unique window handles. + */ +GeckoDriver.prototype.getChromeWindowHandles = function(cmd, resp) { + let rv = []; + let winEn = this.getWinEnumerator(); + while (winEn.hasMoreElements()) { + let foundWin = winEn.getNext(); + let winId = foundWin.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; + winId = winId + ((this.appName == "B2G") ? "-b2g" : ""); + rv.push(winId); + } + resp.value = rv; +}; + +/** + * Get the current window position. + * + * @return {Object.} + * Object with x and y coordinates. + */ +GeckoDriver.prototype.getWindowPosition = function(cmd, resp) { + let win = this.getCurrentWindow(); + resp.value = {x: win.screenX, y: win.screenY}; +}; + +/** + * Set the window position of the browser on the OS Window Manager + * + * @param {number} x + * X coordinate of the top/left of the window that it will be + * moved to. + * @param {number} y + * Y coordinate of the top/left of the window that it will be + * moved to. + */ +GeckoDriver.prototype.setWindowPosition = function(cmd, resp) { + if (this.appName != "Firefox") + throw new WebDriverError("Unable to set the window position on mobile"); + + let x = parseInt(cmd.parameters.x); + let y = parseInt(cmd.parameters.y); + if (isNaN(x) || isNaN(y)) + throw new UnknownError("x and y arguments should be integers"); + + let win = this.getCurrentWindow(); + win.moveTo(x, y); +}; + +/** + * Switch current top-level browsing context by name or server-assigned ID. + * Searches for windows by name, then ID. Content windows take precedence. + * + * @param {string} name + * Target name or ID of the window to switch to. + */ +GeckoDriver.prototype.switchToWindow = function(cmd, resp) { + let switchTo = cmd.parameters.name; + let isB2G = this.appName == "B2G"; + let found; + + let getOuterWindowId = function(win) { + let rv = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; + rv += isB2G ? "-b2g" : ""; + return rv; + }; + + let byNameOrId = function(name, outerId, contentWindowId) { + return switchTo == name || + switchTo == contentWindowId || + switchTo == outerId; + }; + + let winEn = this.getWinEnumerator(); + while (winEn.hasMoreElements()) { + let win = winEn.getNext(); + let outerId = getOuterWindowId(win); + + if (win.gBrowser && !isB2G) { + let tabbrowser = win.gBrowser; + for (let i = 0; i < tabbrowser.browsers.length; ++i) { + let browser = tabbrowser.getBrowserAtIndex(i); + let contentWindowId = this.getIdForBrowser(browser); + if (byNameOrId(win.name, contentWindowId, outerId)) { + found = { + win: win, + outerId: outerId, + tabIndex: i, + contentId: contentWindowId + }; + break; + } + } + } else { + if (byNameOrId(win.name, outerId)) { + found = {win: win, outerId: outerId}; + break; + } + } + } + + if (found) { + // As in content, switching to a new window invalidates a sandbox + // for reuse. + this.sandbox = null; + + // Initialise Marionette if browser has not been seen before, + // otherwise switch to known browser and activate the tab if it's a + // content browser. + if (!(found.outerId in this.browsers)) { + let registerBrowsers, browserListening; + if (found.contentId) { + registerBrowsers = this.registerPromise(); + browserListening = this.listeningPromise(); + } + + this.startBrowser(found.win, false /* isNewSession */); + + if (registerBrowsers && browserListening) { + yield registerBrowsers; + yield browserListening; + } + } else { + utils.window = found.win; + this.curBrowser = this.browsers[found.outerId]; + + if (found.contentId) { + this.curBrowser.switchToTab(found.tabIndex); + } + } + } else { + throw new NoSuchWindowError(`Unable to locate window: ${switchTo}`); + } +}; + +GeckoDriver.prototype.getActiveFrame = function(cmd, resp) { + switch (this.context) { + case Context.CHROME: + // no frame means top-level + resp.value = null; + if (this.curFrame) + resp.value = this.curBrowser.elementManager + .addToKnownElements(this.curFrame.frameElement); + break; + + case Context.CONTENT: + resp.value = this.currentFrameElement; + break; + } +}; + +/** + * Switch to a given frame within the current window. + * + * @param {Object} element + * A web element reference to the element to switch to. + * @param {(string|number)} id + * If element is not defined, then this holds either the id, name, + * or index of the frame to switch to. + */ +GeckoDriver.prototype.switchToFrame = function(cmd, resp) { + let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + let curWindow = this.getCurrentWindow(); + + let checkLoad = function() { + let errorRegex = /about:.+(error)|(blocked)\?/; + let curWindow = this.getCurrentWindow(); + if (curWindow.document.readyState == "complete") { + return; + } else if (curWindow.document.readyState == "interactive" && + errorRegex.exec(curWindow.document.baseURI)) { + throw new UnknownError("Error loading page"); + } + + checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT); + }; + + if (this.context == Context.CHROME) { + let foundFrame = null; + if ((cmd.parameters.id == null) && (cmd.parameters.element == null)) { + this.curFrame = null; + if (cmd.parameters.focus) { + this.mainFrame.focus(); + } + checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT); + return; + } + if (cmd.parameters.element != undefined) { + if (this.curBrowser.elementManager.seenItems[cmd.parameters.element]) { + // HTMLIFrameElement + let wantedFrame = this.curBrowser.elementManager + .getKnownElement(cmd.parameters.element, curWindow); + // Deal with an embedded xul:browser case + if (wantedFrame.tagName == "xul:browser" || wantedFrame.tagName == "browser") { + curWindow = wantedFrame.contentWindow; + this.curFrame = curWindow; + if (cmd.parameters.focus) { + this.curFrame.focus(); + } + checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT); + return; + } + // else, assume iframe + let frames = curWindow.document.getElementsByTagName("iframe"); + let numFrames = frames.length; + for (let i = 0; i < numFrames; i++) { + if (XPCNativeWrapper(frames[i]) == XPCNativeWrapper(wantedFrame)) { + curWindow = frames[i].contentWindow; + this.curFrame = curWindow; + if (cmd.parameters.focus) { + this.curFrame.focus(); + } + checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT); + return; + } + } + } + } + switch(typeof(cmd.parameters.id)) { + case "string" : + let foundById = null; + let frames = curWindow.document.getElementsByTagName("iframe"); + let numFrames = frames.length; + for (let i = 0; i < numFrames; i++) { + //give precedence to name + let frame = frames[i]; + if (frame.getAttribute("name") == cmd.parameters.id) { + foundFrame = i; + curWindow = frame.contentWindow; + break; + } else if ((foundById == null) && (frame.id == cmd.parameters.id)) { + foundById = i; + } + } + if ((foundFrame == null) && (foundById != null)) { + foundFrame = foundById; + curWindow = frames[foundById].contentWindow; + } + break; + case "number": + if (curWindow.frames[cmd.parameters.id] != undefined) { + foundFrame = cmd.parameters.id; + curWindow = curWindow.frames[foundFrame].frameElement.contentWindow; + } + break; + } + if (foundFrame != null) { + this.curFrame = curWindow; + if (cmd.parameters.focus) { + this.curFrame.focus(); + } + checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT); + } else { + throw new NoSuchFrameError( + `Unable to locate frame: ${cmd.parameters.id}`); + } + } + else { + if ((!cmd.parameters.id) && (!cmd.parameters.element) && + (this.curBrowser.frameManager.currentRemoteFrame !== null)) { + // We're currently using a ChromeMessageSender for a remote frame, so this + // request indicates we need to switch back to the top-level (parent) frame. + // We'll first switch to the parent's (global) ChromeMessageBroadcaster, so + // we send the message to the right listener. + this.switchToGlobalMessageManager(); + } + cmd.command_id = cmd.id; + + let res = yield this.listener.switchToFrame(cmd.parameters); + if (res) { + let {win: winId, frame: frameId} = res; + this.mm = this.curBrowser.frameManager.getFrameMM(winId, frameId); + + let registerBrowsers = this.registerPromise(); + let browserListening = this.listeningPromise(); + + this.oopFrameId = + this.curBrowser.frameManager.switchToFrame(winId, frameId); + + yield registerBrowsers; + yield browserListening; + } + } +}; + +/** + * Set timeout for searching for elements. + * + * @param {number} ms + * Search timeout in milliseconds. + */ +GeckoDriver.prototype.setSearchTimeout = function(cmd, resp) { + let ms = parseInt(cmd.parameters.ms); + if (isNaN(ms)) + throw new WebDriverError("Not a Number"); + this.searchTimeout = ms; +}; + +/** + * Set timeout for page loading, searching, and scripts. + * + * @param {string} type + * Type of timeout. + * @param {number} ms + * Timeout in milliseconds. + */ +GeckoDriver.prototype.timeouts = function(cmd, resp) { + let typ = cmd.parameters.type; + let ms = parseInt(cmd.parameters.ms); + if (isNaN(ms)) + throw new WebDriverError("Not a Number"); + + switch (typ) { + case "implicit": + this.setSearchTimeout(cmd, resp); + break; + + case "script": + this.setScriptTimeout(cmd, resp); + break; + + default: + this.pageTimeout = ms; + break; + } +}; + +/** Single tap. */ +GeckoDriver.prototype.singleTap = function(cmd, resp) { + let {id, x, y} = cmd.parameters; + + switch (this.context) { + case Context.CHROME: + throw new WebDriverError("Command 'singleTap' is not available in chrome context"); + + case Context.CONTENT: + this.addFrameCloseListener("tap"); + yield this.listener.singleTap({id: id, corx: x, cory: y}); + break; + } +}; + +/** + * An action chain. + * + * @param {Object} value + * A nested array where the inner array represents each event, + * and the outer array represents a collection of events. + * + * @return {number} + * Last touch ID. + */ +GeckoDriver.prototype.actionChain = function(cmd, resp) { + switch (this.context) { + case Context.CHROME: + throw new WebDriverError("Command 'actionChain' is not available in chrome context"); + + case Context.CONTENT: + this.addFrameCloseListener("action chain"); + resp.value = yield this.listener.actionChain( + {chain: cmd.parameters.chain, nextId: cmd.parameters.nextId}); + break; + } +}; + +/** + * A multi-action chain. + * + * @param {Object} value + * A nested array where the inner array represents eache vent, + * the middle array represents a collection of events for each + * finger, and the outer array represents all fingers. + */ +GeckoDriver.prototype.multiAction = function(cmd, resp) { + switch (this.context) { + case Context.CHROME: + throw new WebDriverError("Command 'multiAction' is not available in chrome context"); + + case Context.CONTENT: + this.addFrameCloseListener("multi action chain"); + yield this.listener.multiAction( + {value: value, maxlen: max_len} = cmd.parameters); + break; + } +}; + +/** + * Find an element using the indicated search strategy. + * + * @param {string} using + * Indicates which search method to use. + * @param {string} value + * Value the client is looking for. + */ +GeckoDriver.prototype.findElement = function(cmd, resp) { + switch (this.context) { + case Context.CHROME: + resp.value = yield new Promise((resolve, reject) => { + let win = this.getCurrentWindow(); + this.curBrowser.elementManager.find( + win, + cmd.parameters, + this.searchTimeout, + false /* all */, + resolve, + reject); + }).then(null, e => { throw e; }); + break; + + case Context.CONTENT: + resp.value = yield this.listener.findElementContent({ + value: cmd.parameters.value, + using: cmd.parameters.using, + element: cmd.parameters.element, + searchTimeout: this.searchTimeout}); + break; + } +}; + +/** + * Find element using the indicated search strategy starting from a + * known element. Used for WebDriver Compatibility only. + * + * @param {string} using + * Indicates which search method to use. + * @param {string} value + * Value the client is looking for. + * @param {string} id + * Value of the element to start from. + */ +GeckoDriver.prototype.findChildElement = function(cmd, resp) { + resp.value = yield this.listener.findElementContent({ + value: cmd.parameters.value, + using: cmd.parameters.using, + element: cmd.parameters.id, + searchTimeout: this.searchTimeout}); +}; + +/** + * Find elements using the indicated search strategy. + * + * @param {string} using + * Indicates which search method to use. + * @param {string} value + * Value the client is looking for. + */ +GeckoDriver.prototype.findElements = function(cmd, resp) { + switch (this.context) { + case Context.CHROME: + resp.value = yield new Promise((resolve, reject) => { + let win = this.getCurrentWindow(); + this.curBrowser.elementManager.find( + win, + cmd.parameters, + this.searchTimeout, + true /* all */, + resolve, + reject); + }).then(null, e => { throw new NoSuchElementError(e.message); }); + break; + + case Context.CONTENT: + resp.value = yield this.listener.findElementsContent({ + value: cmd.parameters.value, + using: cmd.parameters.using, + element: cmd.parameters.element, + searchTimeout: this.searchTimeout}); + break; + } +}; + +/** + * Find elements using the indicated search strategy starting from a + * known element. Used for WebDriver Compatibility only. + * + * @param {string} using + * Indicates which search method to use. + * @param {string} value + * Value the client is looking for. + * @param {string} id + * Value of the element to start from. + */ +GeckoDriver.prototype.findChildElements = function(cmd, resp) { + resp.value = yield this.listener.findElementsContent({ + value: cmd.parameters.value, + using: cmd.parameters.using, + element: cmd.parameters.id, + searchTimeout: this.searchTimeout}); +}; + +/** Return the active element on the page. */ +GeckoDriver.prototype.getActiveElement = function(cmd, resp) { + resp.value = yield this.listener.getActiveElement(); +}; + +/** + * Send click event to element. + * + * @param {string} id + * Reference ID to the element that will be clicked. + */ +GeckoDriver.prototype.clickElement = function(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + // click atom fails, fall back to click() action + let win = this.getCurrentWindow(); + let el = this.curBrowser.elementManager.getKnownElement(id, win); + el.click(); + break; + + case Context.CONTENT: + // We need to protect against the click causing an OOP frame to close. + // This fires the mozbrowserclose event when it closes so we need to + // listen for it and then just send an error back. The person making the + // call should be aware something isnt right and handle accordingly + this.addFrameCloseListener("click"); + yield this.listener.clickElement({id: id}); + break; + } +}; + +/** + * Get a given attribute of an element. + * + * @param {string} id + * Reference ID to the element that will be inspected. + * @param {string} name + * Name of the attribute to retrieve. + */ +GeckoDriver.prototype.getElementAttribute = function(cmd, resp) { + let {id, name} = cmd.parameters; + + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let el = this.curBrowser.elementManager.getKnownElement(id, win); + resp.value = utils.getElementAttribute(el, name); + break; + + case Context.CONTENT: + resp.value = yield this.listener.getElementAttribute({id: id, name: name}); + break; + } +}; + +/** + * Get the text of an element, if any. Includes the text of all child + * elements. + * + * @param {string} id + * Reference ID to the element that will be inspected. + */ +GeckoDriver.prototype.getElementText = function(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + // for chrome, we look at text nodes, and any node with a "label" field + let win = this.getCurrentWindow(); + let el = this.curBrowser.elementManager.getKnownElement(id, win); + let lines = []; + this.getVisibleText(el, lines); + resp.value = lines.join("\n"); + break; + + case Context.CONTENT: + resp.value = yield this.listener.getElementText({id: id}); + break; + } +}; + +/** + * Get the tag name of the element. + * + * @param {string} id + * Reference ID to the element that will be inspected. + */ +GeckoDriver.prototype.getElementTagName = function(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let el = this.curBrowser.elementManager.getKnownElement(id, win); + resp.value = el.tagName.toLowerCase(); + break; + + case Context.CONTENT: + resp.value = yield this.listener.getElementTagName({id: id}); + break; + } +}; + +/** + * Check if element is displayed. + * + * @param {string} id + * Reference ID to the element that will be inspected. + */ +GeckoDriver.prototype.isElementDisplayed = function(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let el = this.curBrowser.elementManager.getKnownElement(id, win); + resp.value = utils.isElementDisplayed(el); + break; + + case Context.CONTENT: + resp.value = yield this.listener.isElementDisplayed({id: id}); + break; + } +}; + +/** + * Return the property of the computed style of an element. + * + * @param {string} id + * Reference ID to the element that will be checked. + * @param {string} propertyName + * CSS rule that is being requested. + */ +GeckoDriver.prototype.getElementValueOfCssProperty = function(cmd, resp) { + let {id, propertyName: prop} = cmd.parameters; + + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let el = this.curBrowser.elementManager.getKnownElement(id, win); + let sty = win.document.defaultView.getComputedStyle(el, null); + resp.value = sty.getPropertyValue(prop); + break; + + case Context.CONTENT: + resp.value = yield this.listener.getElementValueOfCssProperty( + {id: id, propertyName: prop}); + break; + } +}; + +/** + * Submit a form on a content page by either using form or element in + * a form. + * + * @param {string} id + * Reference to the elemen that will be checked. + */ +GeckoDriver.prototype.submitElement = function(cmd, resp) { + switch (this.context) { + case Context.CHROME: + throw new WebDriverError( + "Command 'submitElement' is not available in chrome context"); + + case Context.CONTENT: + yield this.listener.submitElement({id: cmd.parameters.id}); + break; + } +}; + +/** + * Check if element is enabled. + * + * @param {string} id + * Reference ID to the element that will be checked. + */ +GeckoDriver.prototype.isElementEnabled = function(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + // Selenium atom doesn't quite work here + let win = this.getCurrentWindow(); + let el = this.curBrowser.elementManager.getKnownElement(id, win); + resp.value = !(!!el.disabled); + break; + + case Context.CONTENT: + resp.value = yield this.listener.isElementEnabled({id: id}); + break; + } +}, + +/** + * Check if element is selected. + * + * @param {string} id + * Reference ID to the element that will be checked. + */ +GeckoDriver.prototype.isElementSelected = function(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + // Selenium atom doesn't quite work here + let win = this.getCurrentWindow(); + let el = this.curBrowser.elementManager.getKnownElement(id, win); + if (typeof el.checked != "undefined") { + resp.value = !!el.checked; + } else if (typeof el.selected != "undefined") { + resp.value = !!el.selected; + } else { + resp.value = true; + } + break; + + case Context.CONTENT: + resp.value = yield this.listener.isElementSelected({id: id}); + break; + } +}; + +GeckoDriver.prototype.getElementSize = function(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let el = this.curBrowser.elementManager.getKnownElement(id, win); + let rect = el.getBoundingClientRect(); + resp.value = {width: rect.width, height: rect.height}; + break; + + case Context.CONTENT: + resp.value = yield this.listener.getElementSize({id: id}); + break; + } +}; + +GeckoDriver.prototype.getElementRect = function(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let el = this.curBrowser.elementManager.getKnownElement(id, win); + let rect = el.getBoundingClientRect(); + resp.value = { + x: rect.x + win.pageXOffset, + y: rect.y + win.pageYOffset, + width: rect.width, + height: rect.height + }; + break; + + case Context.CONTENT: + resp.value = yield this.listener.getElementRect({id: id}); + break; + } +}; + +/** + * Send key presses to element after focusing on it. + * + * @param {string} id + * Reference ID to the element that will be checked. + * @param {string} value + * Value to send to the element. + */ +GeckoDriver.prototype.sendKeysToElement = function(cmd, resp) { + let {id, value} = cmd.parameters; + + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let el = this.curBrowser.elementManager.getKnownElement(id, win); + utils.sendKeysToElement( + win, + el, + value, + () => {}, + e => { throw e; }, + cmd.id, + true /* ignore visibility check */); + break; + + case Context.CONTENT: + yield this.listener.sendKeysToElement({id: id, value: value}); + break; + } +}; + +/** Sets the test name. The test name is used for logging purposes. */ +GeckoDriver.prototype.setTestName = function(cmd, resp) { + let val = cmd.parameters.value; + this.testName = val; + yield this.listener.setTestName({value: val}); +}; + +/** + * Clear the text of an element. + * + * @param {string} id + * Reference ID to the element that will be cleared. + */ +GeckoDriver.prototype.clearElement = function(cmd, resp) { + let id = cmd.parameters.id; + + switch (this.context) { + case Context.CHROME: + // the selenium atom doesn't work here + let win = this.getCurrentWindow(); + let el = this.curBrowser.elementManager.getKnownElement(id, win); + if (el.nodeName == "textbox") { + el.value = ""; + } else if (el.nodeName == "checkbox") { + el.checked = false; + } + break; + + case Context.CONTENT: + yield this.listener.clearElement({id: id}); + break; + } +}; + +/** + * Get an element's location on the page. + * + * The returned point will contain the x and y coordinates of the + * top left-hand corner of the given element. The point (0,0) + * refers to the upper-left corner of the document. + * + * @return {Object.} + * A point containing X and Y coordinates as properties. + */ +GeckoDriver.prototype.getElementLocation = function(cmd, resp) { + resp.value = yield this.listener.getElementLocation( + {id: cmd.parameters.id}); +}; + +/** Add a cookie to the document. */ +GeckoDriver.prototype.addCookie = function(cmd, resp) { + yield this.listener.addCookie({cookie: cmd.parameters.cookie}); +}; + +/** + * Get all the cookies for the current domain. + * + * This is the equivalent of calling {@code document.cookie} and parsing + * the result. + */ +GeckoDriver.prototype.getCookies = function(cmd, resp) { + resp.value = yield this.listener.getCookies(); +}; + +/** Delete all cookies that are visible to a document. */ +GeckoDriver.prototype.deleteAllCookies = function(cmd, resp) { + yield this.listener.deleteAllCookies(); +}; + +/** Delete a cookie by name. */ +GeckoDriver.prototype.deleteCookie = function(cmd, resp) { + yield this.listener.deleteCookie({name: cmd.parameters.name}); +}; + +/** + * Close the current window, ending the session if it's the last + * window currently open. + * + * On B2G this method is a noop and will return immediately. + */ +GeckoDriver.prototype.close = function(cmd, resp) { + // can't close windows on B2G + if (this.appName == "B2G") + return; + + let nwins = 0; + let winEn = this.getWinEnumerator(); + while (winEn.hasMoreElements()) { + let win = winEn.getNext(); + + // count both windows and tabs + if (win.gBrowser) + nwins += win.gBrowser.browsers.length; + else + nwins++; + } + + // if there is only 1 window left, delete the session + if (nwins == 1) { + this.sessionTearDown(); + return; + } + + try { + if (this.mm != globalMessageManager) + this.mm.removeDelayedFrameScript(FRAME_SCRIPT); + + if (this.curBrowser.tab) + this.curBrowser.closeTab(); + else + this.getCurrentWindow().close(); + } catch (e) { + throw new UnknownError(`Could not close window: ${e.message}`); + } +}; + +/** + * Close the currently selected chrome window, ending the session if it's the last + * window currently open. + * + * On B2G this method is a noop and will return immediately. + */ +GeckoDriver.prototype.closeChromeWindow = function(cmd, resp) { + // can't close windows on B2G + if (this.appName == "B2G") + return; + + // Get the total number of windows + let nwins = 0; + let winEn = this.getWinEnumerator(); + while (winEn.hasMoreElements()) { + nwins++; + winEn.getNext(); + } + + // if there is only 1 window left, delete the session + if (nwins == 1) { + this.sessionTearDown(); + return; + } + + try { + this.mm.removeDelayedFrameScript(FRAME_SCRIPT); + this.getCurrentWindow().close(); + } catch (e) { + throw new UnknownError(`Could not close window: ${e.message}`); + } +}; + +/** + * Deletes the session. + * + * If it is a desktop environment, it will close all listeners. + * + * If it is a B2G environment, it will make the main content listener + * sleep, and close all other listeners. The main content listener + * persists after disconnect (it's the homescreen), and can safely + * be reused. + */ +GeckoDriver.prototype.sessionTearDown = function(cmd, resp) { + if (this.curBrowser != null) { + if (this.appName == "B2G") { + globalMessageManager.broadcastAsyncMessage( + "Marionette:sleepSession" + this.curBrowser.mainContentId, {}); + this.curBrowser.knownFrames.splice( + this.curBrowser.knownFrames.indexOf(this.curBrowser.mainContentId), 1); + } else { + // don't set this pref for B2G since the framescript can be safely reused + Services.prefs.setBoolPref("marionette.contentListener", false); + } + + // delete session in each frame in each browser + for (let win in this.browsers) { + let browser = this.browsers[win]; + for (let i in browser.knownFrames) { + globalMessageManager.broadcastAsyncMessage( + "Marionette:deleteSession" + browser.knownFrames[i], {}); + } + } + + let winEn = this.getWinEnumerator(); + while (winEn.hasMoreElements()) { + winEn.getNext().messageManager.removeDelayedFrameScript(FRAME_SCRIPT); + } + + this.curBrowser.frameManager.removeSpecialPowers(); + this.curBrowser.frameManager.removeMessageManagerListeners( + globalMessageManager); + } + + this.switchToGlobalMessageManager(); + + // reset frame to the top-most frame + this.curFrame = null; + if (this.mainFrame) + this.mainFrame.focus(); + + this.sessionId = null; + this.deleteFile("marionetteChromeScripts"); + this.deleteFile("marionetteContentScripts"); + + if (this.observing !== null) { + for (let topic in this.observing) { + Services.obs.removeObserver(this.observing[topic], topic); + } + this.observing = null; + } +}; + +/** + * Processes the "deleteSession" request from the client by tearing down + * the session and responding "ok". + */ +GeckoDriver.prototype.deleteSession = function(cmd, resp) { + this.sessionTearDown(); +}; + +/** Returns the current status of the Application Cache. */ +GeckoDriver.prototype.getAppCacheStatus = function(cmd, resp) { + resp.value = yield this.listener.getAppCacheStatus(); +}; + +GeckoDriver.prototype.importScript = function(cmd, resp) { + let script = cmd.parameters.script; + + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + converter.charset = "UTF-8"; + let result = {}; + let data = converter.convertToByteArray(cmd.parameters.script, result); + let ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash); + ch.init(ch.MD5); + ch.update(data, data.length); + let hash = ch.finish(true); + // return if we've already imported this script + if (this.importedScriptHashes[this.context].indexOf(hash) > -1) + return; + this.importedScriptHashes[this.context].push(hash); + + switch (this.context) { + case Context.CHROME: + let file; + if (this.importedScripts.exists()) { + file = FileUtils.openFileOutputStream(this.importedScripts, + FileUtils.MODE_APPEND | FileUtils.MODE_WRONLY); + } else { + // the permission bits here don't actually get set (bug 804563) + this.importedScripts.createUnique( + Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8)); + file = FileUtils.openFileOutputStream(this.importedScripts, + FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE); + this.importedScripts.permissions = parseInt("0666", 8); + } + file.write(script, script.length); + file.close(); + break; + + case Context.CONTENT: + yield this.listener.importScript({script: script}); + break; + } +}; + +GeckoDriver.prototype.clearImportedScripts = function(cmd, resp) { + switch (this.context) { + case Context.CHROME: + this.deleteFile("marionetteChromeScripts"); + break; + + case Context.CONTENT: + this.deleteFile("marionetteContentScripts"); + break; + } +}; + +/** + * Takes a screenshot of a web element, current frame, or viewport. + * + * The screen capture is returned as a lossless PNG image encoded as + * a base 64 string. + * + * If called in the content context, the id argument is not null + * and refers to a present and visible web element's ID, the capture area + * will be limited to the bounding box of that element. Otherwise, the + * capture area will be the bounding box of the current frame. + * + * If called in the chrome context, the screenshot will always represent the + * entire viewport. + * + * @param {string} id + * Reference to a web element. + * @param {string} highlights + * List of web elements to highlight. + * + * @return {string} + * PNG image encoded as base64 encoded string. + */ +GeckoDriver.prototype.takeScreenshot = function(cmd, resp) { + switch (this.context) { + case Context.CHROME: + let win = this.getCurrentWindow(); + let canvas = win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + let doc; + if (this.appName == "B2G") + doc = win.document.body; + else + doc = win.document.getElementsByTagName("window")[0]; + let docRect = doc.getBoundingClientRect(); + let width = docRect.width; + let height = docRect.height; + + // Convert width and height from CSS pixels (potentially fractional) + // to device pixels (integer). + let scale = win.devicePixelRatio; + canvas.setAttribute("width", Math.round(width * scale)); + canvas.setAttribute("height", Math.round(height * scale)); + + let context = canvas.getContext("2d"); + let flags; + if (this.appName == "B2G") { + flags = + context.DRAWWINDOW_DRAW_CARET | + context.DRAWWINDOW_DRAW_VIEW | + context.DRAWWINDOW_USE_WIDGET_LAYERS; + } else { + // Bug 1075168: CanvasRenderingContext2D image is distorted + // when using certain flags in chrome context. + flags = + context.DRAWWINDOW_DRAW_VIEW | + context.DRAWWINDOW_USE_WIDGET_LAYERS; + } + context.scale(scale, scale); + context.drawWindow(win, 0, 0, width, height, "rgb(255,255,255)", flags); + let dataUrl = canvas.toDataURL("image/png", ""); + let data = dataUrl.substring(dataUrl.indexOf(",") + 1); + resp.value = data; + break; + + case Context.CONTENT: + resp.value = yield this.listener.takeScreenshot({ + id: cmd.parameters.id, + highlights: cmd.parameters.highlights, + full: cmd.parameters.full}); + break; + } +}; + +/** + * Get the current browser orientation. + * + * Will return one of the valid primary orientation values + * portrait-primary, landscape-primary, portrait-secondary, or + * landscape-secondary. + */ +GeckoDriver.prototype.getScreenOrientation = function(cmd, resp) { + resp.value = this.getCurrentWindow().screen.mozOrientation; +}; + +/** + * Set the current browser orientation. + * + * The supplied orientation should be given as one of the valid + * orientation values. If the orientation is unknown, an error will + * be raised. + * + * Valid orientations are "portrait" and "landscape", which fall + * back to "portrait-primary" and "landscape-primary" respectively, + * and "portrait-secondary" as well as "landscape-secondary". + */ +GeckoDriver.prototype.setScreenOrientation = function(cmd, resp) { + const ors = [ + "portrait", "landscape", + "portrait-primary", "landscape-primary", + "portrait-secondary", "landscape-secondary" + ]; + + let or = String(cmd.parameters.orientation); + let mozOr = or.toLowerCase(); + if (ors.indexOf(mozOr) < 0) + throw new WebDriverError(`Unknown screen orientation: ${or}`); + + let win = this.getCurrentWindow(); + if (!win.screen.mozLockOrientation(mozOr)) + throw new WebDriverError(`Unable to set screen orientation: ${or}`); +}; + +/** + * Get the size of the browser window currently in focus. + * + * Will return the current browser window size in pixels. Refers to + * window outerWidth and outerHeight values, which include scroll bars, + * title bars, etc. + */ +GeckoDriver.prototype.getWindowSize = function(cmd, resp) { + let win = this.getCurrentWindow(); + resp.value = {width: win.outerWidth, height: win.outerHeight}; +}; + +/** + * Set the size of the browser window currently in focus. + * + * Not supported on B2G. The supplied width and height values refer to + * the window outerWidth and outerHeight values, which include scroll + * bars, title bars, etc. + * + * An error will be returned if the requested window size would result + * in the window being in the maximized state. + */ +GeckoDriver.prototype.setWindowSize = function(cmd, resp) { + if (this.appName !== "Firefox") + throw new UnsupportedOperationError("Not supported on mobile"); + + let width = parseInt(cmd.parameters.width); + let height = parseInt(cmd.parameters.height); + + let win = this.getCurrentWindow(); + if (width >= win.screen.availWidth && height >= win.screen.availHeight) + throw new UnsupportedOperationError("Invalid requested size, cannot maximize"); + + win.resizeTo(width, height); +}; + +/** + * Maximizes the user agent window as if the user pressed the maximise + * button. + * + * Not Supported on B2G or Fennec. + */ +GeckoDriver.prototype.maximizeWindow = function(cmd, resp) { + if (this.appName != "Firefox") + throw new UnsupportedOperationError("Not supported for mobile"); + + let win = this.getCurrentWindow(); + win.moveTo(0,0); + win.resizeTo(win.screen.availWidth, win.screen.availHeight); +}; + +/** + * Dismisses a currently displayed tab modal, or returns no such alert if + * no modal is displayed. + */ +GeckoDriver.prototype.dismissDialog = function(cmd, resp) { + if (!this.dialog) + throw new NoAlertOpenError( + "No tab modal was open when attempting to dismiss the dialog"); + + let {button0, button1} = this.dialog.ui; + (button1 ? button1 : button0).click(); + this.dialog = null; +}; + +/** + * Accepts a currently displayed tab modal, or returns no such alert if + * no modal is displayed. + */ +GeckoDriver.prototype.acceptDialog = function(cmd, resp) { + if (!this.dialog) + throw new NoAlertOpenError( + "No tab modal was open when attempting to accept the dialog"); + + let {button0} = this.dialog.ui; + button0.click(); + this.dialog = null; +}; + +/** + * Returns the message shown in a currently displayed modal, or returns a no such + * alert error if no modal is currently displayed. + */ +GeckoDriver.prototype.getTextFromDialog = function(cmd, resp) { + if (!this.dialog) + throw new NoAlertOpenError( + "No tab modal was open when attempting to get the dialog text"); + + let {infoBody} = this.dialog.ui; + resp.value = infoBody.textContent; +}; + +/** + * Sends keys to the input field of a currently displayed modal, or + * returns a no such alert error if no modal is currently displayed. If + * a tab modal is currently displayed but has no means for text input, + * an element not visible error is returned. + */ +GeckoDriver.prototype.sendKeysToDialog = function(cmd, resp) { + if (!this.dialog) + throw new NoAlertOpenError( + "No tab modal was open when attempting to send keys to a dialog"); + + // see toolkit/components/prompts/content/commonDialog.js + let {loginContainer, loginTextbox} = this.dialog.ui; + if (loginContainer.hidden) + throw new ElementNotVisibleError("This prompt does not accept text input"); + + let win = this.dialog.window ? this.dialog.window : this.getCurrentWindow(); + utils.sendKeysToElement( + win, + loginTextbox, + cmd.parameters.value, + () => {}, + e => { throw e; }, + this.command_id, + true /* ignore visibility check */); +}; + +/** + * Helper function to convert an outerWindowID into a UID that Marionette + * tracks. + */ +GeckoDriver.prototype.generateFrameId = function(id) { + let uid = id + (this.appName == "B2G" ? "-b2g" : ""); + return uid; +}; + +/** Receives all messages from content messageManager. */ +GeckoDriver.prototype.receiveMessage = function(message) { + // we need to just check if we need to remove the mozbrowserclose listener + if (this.mozBrowserClose !== null) { + let win = this.getCurrentWindow(); + win.removeEventListener("mozbrowserclose", this.mozBrowserClose, true); + this.mozBrowserClose = null; + } + + switch (message.name) { + case "Marionette:log": + // log server-side messages + logger.info(message.json.message); + break; + + case "Marionette:shareData": + // log messages from tests + if (message.json.log) + this.marionetteLog.addLogs(message.json.log); + break; + + case "Marionette:runEmulatorCmd": + case "Marionette:runEmulatorShell": + this.emulator.send(message.json); + break; + + case "Marionette:switchToModalOrigin": + this.curBrowser.frameManager.switchToModalOrigin(message); + this.mm = this.curBrowser.frameManager + .currentRemoteFrame.messageManager.get(); + break; + + case "Marionette:switchedToFrame": + if (message.json.restorePrevious) { + this.currentFrameElement = this.previousFrameElement; + } else { + // we don't arbitrarily save previousFrameElement, since + // we allow frame switching after modals appear, which would + // override this value and we'd lose our reference + if (message.json.storePrevious) + this.previousFrameElement = this.currentFrameElement; + this.currentFrameElement = message.json.frameValue; + } + break; + + case "Marionette:getVisibleCookies": + let [currentPath, host] = message.json.value; + let isForCurrentPath = path => currentPath.indexOf(path) != -1; + let results = []; + + let en = cookieManager.enumerator; + while (en.hasMoreElements()) { + let cookie = en.getNext().QueryInterface(Ci.nsICookie); + // take the hostname and progressively shorten + let hostname = host; + do { + if ((cookie.host == "." + hostname || cookie.host == hostname) && + isForCurrentPath(cookie.path)) { + results.push({ + "name": cookie.name, + "value": cookie.value, + "path": cookie.path, + "host": cookie.host, + "secure": cookie.isSecure, + "expiry": cookie.expires + }); + break; + } + hostname = hostname.replace(/^.*?\./, ""); + } while (hostname.indexOf(".") != -1); + } + return results; + + case "Marionette:addCookie": + let cookieToAdd = message.json.value; + Services.cookies.add( + cookieToAdd.domain, + cookieToAdd.path, + cookieToAdd.name, + cookieToAdd.value, + cookieToAdd.secure, + false, + false, + cookieToAdd.expiry); + return true; + + case "Marionette:deleteCookie": + let cookieToDelete = message.json.value; + cookieManager.remove( + cookieToDelete.host, + cookieToDelete.name, + cookieToDelete.path, + false); + return true; + + case "Marionette:emitTouchEvent": + globalMessageManager.broadcastAsyncMessage( + "MarionetteMainListener:emitTouchEvent", message.json); + break; + + case "Marionette:register": + let wid = message.json.value; + let be = message.target; + let rv = this.registerBrowser(wid, be); + return rv; + + case "Marionette:listenersAttached": + if (message.json.listenerId === this.curBrowser.curFrameId) { + // If remoteness gets updated we need to call newSession. In the case + // of desktop this just sets up a small amount of state that doesn't + // change over the course of a session. + let newSessionValues = { + B2G: (this.appName == "B2G"), + raisesAccessibilityExceptions: + this.sessionCapabilities.raisesAccessibilityExceptions + }; + this.sendAsync("newSession", newSessionValues); + this.curBrowser.flushPendingCommands(); + } + break; + } +}; + +GeckoDriver.prototype.responseCompleted = function () { + if (this.curBrowser !== null) { + this.curBrowser.pendingCommands = []; + } +}; + +GeckoDriver.prototype.commands = { + "getMarionetteID": GeckoDriver.prototype.getMarionetteID, + "sayHello": GeckoDriver.prototype.sayHello, + "newSession": GeckoDriver.prototype.newSession, + "getSessionCapabilities": GeckoDriver.prototype.getSessionCapabilities, + "log": GeckoDriver.prototype.log, + "getLogs": GeckoDriver.prototype.getLogs, + "setContext": GeckoDriver.prototype.setContext, + "getContext": GeckoDriver.prototype.getContext, + "executeScript": GeckoDriver.prototype.execute, + "setScriptTimeout": GeckoDriver.prototype.setScriptTimeout, + "timeouts": GeckoDriver.prototype.timeouts, + "singleTap": GeckoDriver.prototype.singleTap, + "actionChain": GeckoDriver.prototype.actionChain, + "multiAction": GeckoDriver.prototype.multiAction, + "executeAsyncScript": GeckoDriver.prototype.executeWithCallback, + "executeJSScript": GeckoDriver.prototype.executeJSScript, + "setSearchTimeout": GeckoDriver.prototype.setSearchTimeout, + "findElement": GeckoDriver.prototype.findElement, + "findChildElement": GeckoDriver.prototype.findChildElements, // Needed for WebDriver compat + "findElements": GeckoDriver.prototype.findElements, + "findChildElements":GeckoDriver.prototype.findChildElements, // Needed for WebDriver compat + "clickElement": GeckoDriver.prototype.clickElement, + "getElementAttribute": GeckoDriver.prototype.getElementAttribute, + "getElementText": GeckoDriver.prototype.getElementText, + "getElementTagName": GeckoDriver.prototype.getElementTagName, + "isElementDisplayed": GeckoDriver.prototype.isElementDisplayed, + "getElementValueOfCssProperty": GeckoDriver.prototype.getElementValueOfCssProperty, + "submitElement": GeckoDriver.prototype.submitElement, + "getElementSize": GeckoDriver.prototype.getElementSize, //deprecated + "getElementRect": GeckoDriver.prototype.getElementRect, + "isElementEnabled": GeckoDriver.prototype.isElementEnabled, + "isElementSelected": GeckoDriver.prototype.isElementSelected, + "sendKeysToElement": GeckoDriver.prototype.sendKeysToElement, + "getElementLocation": GeckoDriver.prototype.getElementLocation, // deprecated + "getElementPosition": GeckoDriver.prototype.getElementLocation, // deprecated + "clearElement": GeckoDriver.prototype.clearElement, + "getTitle": GeckoDriver.prototype.getTitle, + "getWindowType": GeckoDriver.prototype.getWindowType, + "getPageSource": GeckoDriver.prototype.getPageSource, + "get": GeckoDriver.prototype.get, + "goUrl": GeckoDriver.prototype.get, // deprecated + "getCurrentUrl": GeckoDriver.prototype.getCurrentUrl, + "getUrl": GeckoDriver.prototype.getCurrentUrl, // deprecated + "goBack": GeckoDriver.prototype.goBack, + "goForward": GeckoDriver.prototype.goForward, + "refresh": GeckoDriver.prototype.refresh, + "getWindowHandle": GeckoDriver.prototype.getWindowHandle, + "getCurrentWindowHandle": GeckoDriver.prototype.getWindowHandle, // Selenium 2 compat + "getChromeWindowHandle": GeckoDriver.prototype.getChromeWindowHandle, + "getCurrentChromeWindowHandle": GeckoDriver.prototype.getChromeWindowHandle, + "getWindow": GeckoDriver.prototype.getWindowHandle, // deprecated + "getWindowHandles": GeckoDriver.prototype.getWindowHandles, + "getChromeWindowHandles": GeckoDriver.prototype.getChromeWindowHandles, + "getCurrentWindowHandles": GeckoDriver.prototype.getWindowHandles, // Selenium 2 compat + "getWindows": GeckoDriver.prototype.getWindowHandles, // deprecated + "getWindowPosition": GeckoDriver.prototype.getWindowPosition, + "setWindowPosition": GeckoDriver.prototype.setWindowPosition, + "getActiveFrame": GeckoDriver.prototype.getActiveFrame, + "switchToFrame": GeckoDriver.prototype.switchToFrame, + "switchToWindow": GeckoDriver.prototype.switchToWindow, + "deleteSession": GeckoDriver.prototype.deleteSession, + "importScript": GeckoDriver.prototype.importScript, + "clearImportedScripts": GeckoDriver.prototype.clearImportedScripts, + "getAppCacheStatus": GeckoDriver.prototype.getAppCacheStatus, + "close": GeckoDriver.prototype.close, + "closeWindow": GeckoDriver.prototype.close, // deprecated + "closeChromeWindow": GeckoDriver.prototype.closeChromeWindow, + "setTestName": GeckoDriver.prototype.setTestName, + "takeScreenshot": GeckoDriver.prototype.takeScreenshot, + "screenShot": GeckoDriver.prototype.takeScreenshot, // deprecated + "screenshot": GeckoDriver.prototype.takeScreenshot, // Selenium 2 compat + "addCookie": GeckoDriver.prototype.addCookie, + "getCookies": GeckoDriver.prototype.getCookies, + "getAllCookies": GeckoDriver.prototype.getCookies, // deprecated + "deleteAllCookies": GeckoDriver.prototype.deleteAllCookies, + "deleteCookie": GeckoDriver.prototype.deleteCookie, + "getActiveElement": GeckoDriver.prototype.getActiveElement, + "getScreenOrientation": GeckoDriver.prototype.getScreenOrientation, + "setScreenOrientation": GeckoDriver.prototype.setScreenOrientation, + "getWindowSize": GeckoDriver.prototype.getWindowSize, + "setWindowSize": GeckoDriver.prototype.setWindowSize, + "maximizeWindow": GeckoDriver.prototype.maximizeWindow, + "dismissDialog": GeckoDriver.prototype.dismissDialog, + "acceptDialog": GeckoDriver.prototype.acceptDialog, + "getTextFromDialog": GeckoDriver.prototype.getTextFromDialog, + "sendKeysToDialog": GeckoDriver.prototype.sendKeysToDialog +}; + +/** + * Represents the current modal dialogue. + * + * @param {function(): BrowserObj} curBrowserFn + * Function that returns the current BrowserObj. + * @param {?nsIWeakReference} winRef + * A weak reference to the current ChromeWindow. + */ +this.ModalDialog = function(curBrowserFn, winRef=null) { + Object.defineProperty(this, "curBrowser", { + get() { return curBrowserFn(); } + }); + this.win_ = winRef; +}; + +/** + * Returns the ChromeWindow associated with an open dialog window if it is + * currently attached to the dom. + * + */ +Object.defineProperty(ModalDialog.prototype, "window", { + get() { + if (this.win_ !== null) { + let win = this.win_.get(); + if (win && win.parent) + return win; + } + return null; + } +}); + +Object.defineProperty(ModalDialog.prototype, "ui", { + get() { + let win = this.window; + if (win) + return win.Dialog.ui; + return this.curBrowser.getTabModalUI(); + } +}); + +/** + * Creates a BrowserObj. BrowserObjs handle interactions with the + * browser, according to the current environment (desktop, b2g, etc.). + * + * @param {nsIDOMWindow} win + * The window whose browser needs to be accessed. + * @param {GeckoDriver} driver + * Reference to the driver the browser is attached to. + */ +let BrowserObj = function(win, driver) { + this.browser = undefined; + this.window = win; + this.driver = driver; + this.knownFrames = []; + this.startPage = "about:blank"; + // used in B2G to identify the homescreen content page + this.mainContentId = null; + // used to set curFrameId upon new session + this.newSession = true; + this.elementManager = new ElementManager([NAME, LINK_TEXT, PARTIAL_LINK_TEXT]); + this.setBrowser(win); + + // A reference to the tab corresponding to the current window handle, if any. + this.tab = null; + this.pendingCommands = []; + + // we should have one FM per BO so that we can handle modals in each Browser + this.frameManager = new FrameManager(driver); + this.frameRegsPending = 0; + + // register all message listeners + this.frameManager.addMessageManagerListeners(driver.mm); + this.getIdForBrowser = driver.getIdForBrowser.bind(driver); + this.updateIdForBrowser = driver.updateIdForBrowser.bind(driver); + this._curFrameId = null; + this._browserWasRemote = null; + this._hasRemotenessChange = false; +}; + +Object.defineProperty(BrowserObj.prototype, "browserForTab", { + get() { + return this.browser.getBrowserForTab(this.tab); + } +}); + +/** + * The current frame ID is managed per browser element on desktop in + * case the ID needs to be refreshed. The currently selected window is + * identified within BrowserObject by a tab. + */ +Object.defineProperty(BrowserObj.prototype, "curFrameId", { + get() { + let rv = null; + if (this.driver.appName != "Firefox") { + rv = this._curFrameId; + } else if (this.tab) { + rv = this.getIdForBrowser(this.browserForTab); + } + return rv; + }, + + set(id) { + if (this.driver.appName != "Firefox") { + this._curFrameId = id; + } + } +}); + +/** + * Retrieves the current tabmodal UI object. According to the browser + * associated with the currently selected tab. + */ +BrowserObj.prototype.getTabModalUI = function() { + let br = this.browserForTab; + if (!br.hasAttribute("tabmodalPromptShowing")) + return null; + + // The modal is a direct sibling of the browser element. + // See tabbrowser.xml's getTabModalPromptBox. + let modals = br.parentNode.getElementsByTagNameNS( + XUL_NS, "tabmodalprompt"); + return modals[0].ui; +}; + +/** + * Set the browser if the application is not B2G. + * + * @param {nsIDOMWindow} win + * Current window reference. + */ +BrowserObj.prototype.setBrowser = function(win) { + switch (this.driver.appName) { + case "Firefox": + this.browser = win.gBrowser; + break; + + case "Fennec": + this.browser = win.BrowserApp; + break; + + case "B2G": + // eideticker (bug 965297) and mochitest (bug 965304) + // compatibility. They only check for the presence of this + // property and should not be in caps if not on a B2G device. + this.driver.sessionCapabilities.b2g = true; + break; + } +}; + +/** Called when we start a session with this browser. */ +BrowserObj.prototype.startSession = function(newSession, win, callback) { + callback(win, newSession); +}; + +/** Closes current tab. */ +BrowserObj.prototype.closeTab = function() { + if (this.browser && + this.browser.removeTab && + this.tab != null && (this.driver.appName != "B2G")) { + this.browser.removeTab(this.tab); + } +}; + +/** + * Opens a tab with given URI. + * + * @param {string} uri + * URI to open. + */ +BrowserObj.prototype.addTab = function(uri) { + return this.browser.addTab(uri, true); +}; + +/** + * Re-sets this BrowserObject's current tab and updates remoteness tracking. + */ +BrowserObj.prototype.switchToTab = function(ind) { + if (this.browser) { + this.browser.selectTabAtIndex(ind); + this.tab = this.browser.selectedTab; + } + this._browserWasRemote = this.browserForTab.isRemoteBrowser; + this._hasRemotenessChange = false; +}; + +/** + * Registers a new frame, and sets its current frame id to this frame + * if it is not already assigned, and if a) we already have a session + * or b) we're starting a new session and it is the right start frame. + * + * @param {string} uid + * Frame uid for use by Marionette. + * @param the XUL that was the target of the originating message. + */ +BrowserObj.prototype.register = function(uid, target) { + let remotenessChange = this.hasRemotenessChange(); + if (this.curFrameId === null || remotenessChange) { + if (this.browser) { + // If we're setting up a new session on Firefox, we only process the + // registration for this frame if it belongs to the current tab. + if (!this.tab) + this.switchToTab(this.browser.selectedIndex); + + if (target == this.browserForTab) { + this.updateIdForBrowser(this.browserForTab, uid); + this.mainContentId = uid; + } + } else { + this._curFrameId = uid; + this.mainContentId = uid; + } + } + + // used to delete sessions + this.knownFrames.push(uid); + return remotenessChange; +}; + +/** + * When navigating between pages results in changing a browser's + * process, we need to take measures not to lose contact with a listener + * script. This function does the necessary bookkeeping. + */ +BrowserObj.prototype.hasRemotenessChange = function() { + // None of these checks are relevant on b2g or if we don't have a tab yet, + // and may not apply on Fennec. + if (this.driver.appName != "Firefox" || this.tab === null) + return false; + + if (this._hasRemotenessChange) + return true; + + let currentIsRemote = this.browserForTab.isRemoteBrowser; + this._hasRemotenessChange = this._browserWasRemote !== currentIsRemote; + this._browserWasRemote = currentIsRemote; + return this._hasRemotenessChange; +}; + +/** + * Flushes any pending commands queued when a remoteness change is being + * processed and mark this remotenessUpdate as complete. + */ +BrowserObj.prototype.flushPendingCommands = function() { + if (!this._hasRemotenessChange) + return; + + this._hasRemotenessChange = false; + this.pendingCommands.forEach(cb => cb()); + this.pendingCommands = []; +}; + +/** + * This function intercepts commands interacting with content and queues + * or executes them as needed. + * + * No commands interacting with content are safe to process until + * the new listener script is loaded and registers itself. + * This occurs when a command whose effect is asynchronous (such + * as goBack) results in a remoteness change and new commands + * are subsequently posted to the server. + */ +BrowserObj.prototype.executeWhenReady = function(cb) { + if (this.hasRemotenessChange()) + this.pendingCommands.push(cb); + else + cb(); +}; diff --git a/testing/marionette/emulator.js b/testing/marionette/emulator.js new file mode 100644 index 0000000000..fde65fc38e --- /dev/null +++ b/testing/marionette/emulator.js @@ -0,0 +1,118 @@ +/* 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 {classes: Cc, interfaces: Ci} = Components; +const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); +this.EXPORTED_SYMBOLS = ["emulator", "Emulator", "EmulatorCallback"]; + +this.emulator = {}; + +/** + * Determines if command ID is an emulator callback. + */ +this.emulator.isCallback = function(cmdId) { + return cmdId < 0; +}; + +/** + * Represents the connection between Marionette and the emulator it's + * running on. + * + * When injected scripts call the JS routines {@code runEmulatorCmd} or + * {@code runEmulatorShell}, the second argument to those is a callback + * which is stored in cbs. They are later retreived by their unique ID + * using popCallback. + * + * @param {function(Object)} sendFn + * Callback function that sends a message to the emulator. + */ +this.Emulator = function(sendFn) { + this.send = sendFn; + this.cbs = []; +}; + +/** + * Pops a callback off the stack if found. Otherwise this is a no-op. + * + * @param {number} id + * Unique ID associated with the callback. + * + * @return {?function(Object)} + * Callback function that takes an emulator response message as + * an argument. + */ +Emulator.prototype.popCallback = function(id) { + let f, fi; + for (let i = 0; i < this.cbs.length; ++i) { + if (this.cbs[i].id == id) { + f = this.cbs[i]; + fi = i; + } + } + + if (!f) + return null; + + this.cbs.splice(fi, 1); + return f; +}; + +/** + * Pushes callback on to the stack. + * + * @param {function(Object)} cb + * Callback function that takes an emulator response message as + * an argument. + */ +Emulator.prototype.pushCallback = function(cb) { + cb.send_ = this.sendFn; + this.cbs.push(cb); +}; + +/** + * Encapsulates a callback to the emulator and provides an execution + * environment for them. + * + * Each callback is assigned a unique identifier, id, that can be used + * to retrieve them from Emulator's stack using popCallback. + * + * The onresult event listener is triggered when a result arrives on + * the callback. + * + * The onerror event listener is triggered when an error occurs during + * the execution of that callback. + */ +this.EmulatorCallback = function() { + this.id = uuidGen.generateUUID().toString(); + this.onresult = null; + this.onerror = null; + this.send_ = null; +}; + +EmulatorCallback.prototype.command = function(cmd, cb) { + this.onresult = cb; + this.send_({emulator_cmd: cmd, id: this.id}); +}; + +EmulatorCallback.prototype.shell = function(args, cb) { + this.onresult = cb; + this.send_({emulator_shell: args, id: this.id}); +}; + +EmulatorCallback.prototype.result = function(msg) { + if (this.send_ === null) + throw new TypeError( + "EmulatorCallback must be registered with Emulator to fire"); + + try { + if (!this.onresult) + return; + this.onresult(msg.result); + } catch (e) { + if (this.onerror) + this.onerror(e); + } +}; diff --git a/testing/marionette/error.js b/testing/marionette/error.js new file mode 100644 index 0000000000..17e2c5a78e --- /dev/null +++ b/testing/marionette/error.js @@ -0,0 +1,314 @@ +/* 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 {utils: Cu} = Components; + +const errors = [ + "ElementNotVisibleError", + "FrameSendFailureError", + "FrameSendNotInitializedError", + "JavaScriptError", + "NoAlertOpenError", + "NoSuchElementError", + "NoSuchFrameError", + "NoSuchWindowError", + "ScriptTimeoutError", + "SessionNotCreatedError", + "TimeoutError", + "UnknownCommandError", + "UnknownError", + "UnsupportedOperationError", + "WebDriverError", +]; + +this.EXPORTED_SYMBOLS = ["error"].concat(errors); + +this.error = {}; + +error.toJSON = function(err) { + return { + message: err.message, + stacktrace: err.stack || null, + status: err.code + }; +}; + +/** + * Gets WebDriver error by its Selenium status code number. + */ +error.byCode = n => lookup.get(n); + +/** + * Determines if the given status code is successful. + */ +error.isSuccess = code => code === 0; + +/** + * Old-style errors are objects that has all of the properties + * "message", "code", and "stack". + * + * When listener.js starts forwarding real errors by CPOW + * we can remove this. + */ +let isOldStyleError = function(obj) { + return typeof obj == "object" && + ["message", "code", "stack"].every(c => obj.hasOwnProperty(c)); +} + +/** + * Checks if obj is an instance of the Error prototype in a safe manner. + * Prefer using this over using instanceof since the Error prototype + * isn't unique across browsers, and XPCOM exceptions are special + * snowflakes. + */ +error.isError = function(obj) { + if (obj === null || typeof obj != "object") { + return false; + // XPCOM exception. + // Object.getPrototypeOf(obj).result throws error, + // consequently we must do the check for properties in its + // prototypal chain (using in, instead of obj.hasOwnProperty) here. + } else if ("result" in obj) { + return true; + } else { + return Object.getPrototypeOf(obj) == "Error" || isOldStyleError(obj); + } +}; + +/** + * Checks if obj is an object in the WebDriverError prototypal chain. + */ +error.isWebDriverError = function(obj) { + return error.isError(obj) && + (("name" in obj && errors.indexOf(obj.name) > 0) || + isOldStyleError(obj)); +}; + +/** + * Unhandled error reporter. Dumps the error and its stacktrace to console, + * and reports error to the Browser Console. + */ +error.report = function(err) { + let msg = `Marionette threw an error: ${error.stringify(err)}`; + dump(msg + "\n"); + if (Cu.reportError) { + Cu.reportError(msg); + } +}; + +/** + * Prettifies an instance of Error and its stacktrace to a string. + */ +error.stringify = function(err) { + try { + let s = err.toString(); + if ("stack" in err) { + s += "\n" + err.stack; + } + return s; + } catch (e) { + return ""; + } +}; + +/** + * WebDriverError is the prototypal parent of all WebDriver errors. + * It should not be used directly, as it does not correspond to a real + * error in the specification. + */ +this.WebDriverError = function(msg) { + Error.call(this, msg); + this.name = "WebDriverError"; + this.message = msg; + this.code = 500; // overridden +}; +WebDriverError.prototype = Object.create(Error.prototype); + +this.NoSuchElementError = function(msg) { + WebDriverError.call(this, msg); + this.name = "NoSuchElementError"; + this.status = "no such element"; + this.code = 7; +}; +NoSuchElementError.prototype = Object.create(WebDriverError.prototype); + +this.NoSuchFrameError = function(msg) { + WebDriverError.call(this, msg); + this.name = "NoSuchFrameError"; + this.status = "no such frame"; + this.code = 8; +}; +NoSuchFrameError.prototype = Object.create(WebDriverError.prototype); + +this.UnknownCommandError = function(msg) { + WebDriverError.call(this, msg); + this.name = "UnknownCommandError"; + this.status = "unknown command"; + this.code = 9; +}; +UnknownCommandError.prototype = Object.create(WebDriverError.prototype); + +this.ElementNotVisibleError = function(msg) { + WebDriverError.call(this, msg); + this.name = "ElementNotVisibleError"; + this.status = "element not visible"; + this.code = 11; +}; +ElementNotVisibleError.prototype = Object.create(WebDriverError.prototype); + +this.InvalidElementState = function(msg) { + WebDriverError.call(this, msg); + this.name = "InvalidElementState"; + this.status = "invalid element state"; + this.code = 12; +}; +InvalidElementState.prototype = Object.create(WebDriverError.prototype); + +this.UnknownError = function(msg) { + WebDriverError.call(this, msg); + this.name = "UnknownError"; + this.status = "unknown error"; + this.code = 13; +}; +UnknownError.prototype = Object.create(WebDriverError.prototype); + +/** + * Creates an error message for a JavaScript error thrown during + * executeScript or executeAsyncScript. + * + * @param {Error} err + * An Error object passed to a catch block or a message. + * @param {string} fnName + * The name of the function to use in the stack trace message + * (e.g. execute_script). + * @param {string} file + * The filename of the test file containing the Marionette + * command that caused this error to occur. + * @param {number} line + * The line number of the above test file. + * @param {string=} script + * The JS script being executed in text form. + */ +this.JavaScriptError = function(err, fnName, file, line, script) { + let msg = String(err); + let trace = ""; + + if (fnName && line) { + trace += `${fnName} @${file}`; + if (line) { + trace += `, line ${line}`; + } + } + + if (typeof err == "object" && "name" in err && "stack" in err) { + let jsStack = err.stack.split("\n"); + let match = jsStack[0].match(/:(\d+):\d+$/); + let jsLine = match ? parseInt(match[1]) : 0; + if (script) { + let src = script.split("\n")[jsLine]; + trace += "\n" + + "inline javascript, line " + jsLine + "\n" + + "src: \"" + src + "\""; + } + } + + WebDriverError.call(this, msg); + this.name = "JavaScriptError"; + this.status = "javascript error"; + this.code = 17; + this.stack = trace; +}; +JavaScriptError.prototype = Object.create(WebDriverError.prototype); + +this.TimeoutError = function(msg) { + WebDriverError.call(this, msg); + this.name = "TimeoutError"; + this.status = "timeout"; + this.code = 21; +}; +TimeoutError.prototype = Object.create(WebDriverError.prototype); + +this.NoSuchWindowError = function(msg) { + WebDriverError.call(this, msg); + this.name = "NoSuchWindowError"; + this.status = "no such window"; + this.code = 23; +}; +NoSuchWindowError.prototype = Object.create(WebDriverError.prototype); + +this.NoAlertOpenError = function(msg) { + WebDriverError.call(this, msg); + this.name = "NoAlertOpenError"; + this.status = "no such alert"; + this.code = 27; +} +NoAlertOpenError.prototype = Object.create(WebDriverError.prototype); + +this.ScriptTimeoutError = function(msg) { + WebDriverError.call(this, msg); + this.name = "ScriptTimeoutError"; + this.status = "script timeout"; + this.code = 28; +}; +ScriptTimeoutError.prototype = Object.create(WebDriverError.prototype); + +this.SessionNotCreatedError = function(msg) { + WebDriverError.call(this, msg); + this.name = "SessionNotCreatedError"; + this.status = "session not created"; + // should be 33 to match Selenium + this.code = 71; +} +SessionNotCreatedError.prototype = Object.create(WebDriverError.prototype); + +this.FrameSendNotInitializedError = function(frame) { + this.message = "Error sending message to frame (NS_ERROR_NOT_INITIALIZED)"; + WebDriverError.call(this, this.message); + this.name = "FrameSendNotInitializedError"; + this.status = "frame send not initialized error"; + this.code = 54; + this.frame = frame; + this.errMsg = `${this.message} ${this.frame}; frame has closed.`; +}; +FrameSendNotInitializedError.prototype = Object.create(WebDriverError.prototype); + +this.FrameSendFailureError = function(frame) { + this.message = "Error sending message to frame (NS_ERROR_FAILURE)"; + WebDriverError.call(this, this.message); + this.name = "FrameSendFailureError"; + this.status = "frame send failure error"; + this.code = 55; + this.frame = frame; + this.errMsg = `${this.message} ${this.frame}; frame not responding.`; +}; +FrameSendFailureError.prototype = Object.create(WebDriverError.prototype); + +this.UnsupportedOperationError = function(msg) { + WebDriverError.call(this, msg); + this.name = "UnsupportedOperationError"; + this.status = "unsupported operation"; + this.code = 405; +}; +UnsupportedOperationError.prototype = Object.create(WebDriverError.prototype); + +const errorObjs = [ + this.ElementNotVisibleError, + this.FrameSendFailureError, + this.FrameSendNotInitializedError, + this.JavaScriptError, + this.NoAlertOpenError, + this.NoSuchElementError, + this.NoSuchFrameError, + this.NoSuchWindowError, + this.ScriptTimeoutError, + this.SessionNotCreatedError, + this.TimeoutError, + this.UnknownCommandError, + this.UnknownError, + this.UnsupportedOperationError, + this.WebDriverError, +]; +const lookup = new Map(errorObjs.map(err => [new err().code, err])); diff --git a/testing/marionette/jar.mn b/testing/marionette/jar.mn index 0b4446a864..1e799d4de1 100644 --- a/testing/marionette/jar.mn +++ b/testing/marionette/jar.mn @@ -4,15 +4,21 @@ marionette.jar: % content marionette %content/ - content/marionette-server.js (marionette-server.js) + content/server.js (server.js) + content/driver.js (driver.js) content/marionette-listener.js (marionette-listener.js) content/marionette-elements.js (marionette-elements.js) content/marionette-sendkeys.js (marionette-sendkeys.js) content/marionette-common.js (marionette-common.js) + content/marionette-actions.js (marionette-actions.js) content/marionette-simpletest.js (marionette-simpletest.js) content/marionette-frame-manager.js (marionette-frame-manager.js) content/EventUtils.js (EventUtils.js) content/ChromeUtils.js (ChromeUtils.js) + content/error.js (error.js) + content/command.js (command.js) + content/dispatcher.js (dispatcher.js) + content/emulator.js (emulator.js) #ifdef ENABLE_TESTS content/test.xul (client/marionette/chrome/test.xul) content/test2.xul (client/marionette/chrome/test2.xul) diff --git a/testing/marionette/marionette-actions.js b/testing/marionette/marionette-actions.js new file mode 100644 index 0000000000..dde4b8c609 --- /dev/null +++ b/testing/marionette/marionette-actions.js @@ -0,0 +1,382 @@ +/* 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/. */ + +/** + * Functionality for (single finger) action chains. + */ +this.ActionChain = function (utils, checkForInterrupted) { + // For assigning unique ids to all touches + this.nextTouchId = 1000; + // Keep track of active Touches + this.touchIds = {}; + // last touch for each fingerId + this.lastCoordinates = null; + this.isTap = false; + this.scrolling = false; + // whether to send mouse event + this.mouseEventsOnly = false; + this.checkTimer = Components.classes["@mozilla.org/timer;1"] + .createInstance(Components.interfaces.nsITimer); + + // Callbacks for command completion. + this.onSuccess = null; + this.onError = null; + if (typeof checkForInterrupted == "function") { + this.checkForInterrupted = checkForInterrupted; + } else { + this.checkForInterrupted = () => {}; + } + + // Determines if we create touch events. + this.inputSource = null; + + // Test utilities providing some event synthesis code. + this.utils = utils; +} + +ActionChain.prototype = { + + dispatchActions: function (args, touchId, frame, elementManager, callbacks, + touchProvider) { + // Some touch events code in the listener needs to do ipc, so we can't + // share this code across chrome/content. + if (touchProvider) { + this.touchProvider = touchProvider; + } + + this.elementManager = elementManager; + let commandArray = elementManager.convertWrappedArguments(args, frame); + let {onSuccess, onError} = callbacks; + this.onSuccess = onSuccess; + this.onError = onError; + this.frame = frame; + + if (touchId == null) { + touchId = this.nextTouchId++; + } + + if (!frame.document.createTouch) { + this.mouseEventsOnly = true; + } + + let keyModifiers = { + shiftKey: false, + ctrlKey: false, + altKey: false, + metaKey: false + }; + + try { + this.actions(commandArray, touchId, 0, keyModifiers); + } catch (e) { + this.onError(e.message, e.code, e.stack); + this.resetValues(); + } + }, + + /** + * This function emit mouse event + * @param: doc is the current document + * type is the type of event to dispatch + * clickCount is the number of clicks, button notes the mouse button + * elClientX and elClientY are the coordinates of the mouse relative to the viewport + * modifiers is an object of modifier keys present + */ + emitMouseEvent: function (doc, type, elClientX, elClientY, button, clickCount, modifiers) { + if (!this.checkForInterrupted()) { + let loggingInfo = "emitting Mouse event of type " + type + + " at coordinates (" + elClientX + ", " + elClientY + + ") relative to the viewport\n" + + " button: " + button + "\n" + + " clickCount: " + clickCount + "\n"; + dump(Date.now() + " Marionette: " + loggingInfo); + let win = doc.defaultView; + let domUtils = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils); + let mods; + if (typeof modifiers != "undefined") { + mods = this.utils._parseModifiers(modifiers); + } else { + mods = 0; + } + domUtils.sendMouseEvent(type, elClientX, elClientY, button || 0, clickCount || 1, + mods, false, 0, this.inputSource); + } + }, + + /** + * Reset any persisted values after a command completes. + */ + resetValues: function () { + this.onSuccess = null; + this.onError = null; + this.frame = null; + this.elementManager = null; + this.touchProvider = null; + this.mouseEventsOnly = false; + }, + + /** + * Function to emit touch events for each finger. e.g. finger=[['press', id], ['wait', 5], ['release']] + * touchId represents the finger id, i keeps track of the current action of the chain + * keyModifiers is an object keeping track keyDown/keyUp pairs through an action chain. + */ + actions: function (chain, touchId, i, keyModifiers) { + + if (i == chain.length) { + this.onSuccess({value: touchId}); + this.resetValues(); + return; + } + + let pack = chain[i]; + let command = pack[0]; + let el; + let c; + i++; + + if (['press', 'wait', 'keyDown', 'keyUp', 'click'].indexOf(command) == -1) { + // if mouseEventsOnly, then touchIds isn't used + if (!(touchId in this.touchIds) && !this.mouseEventsOnly) { + this.onError("Element has not been pressed", 500, null); + this.resetValues(); + return; + } + } + + switch(command) { + case 'keyDown': + this.utils.sendKeyDown(pack[1], keyModifiers, this.frame); + this.actions(chain, touchId, i, keyModifiers); + break; + case 'keyUp': + this.utils.sendKeyUp(pack[1], keyModifiers, this.frame); + this.actions(chain, touchId, i, keyModifiers); + break; + case 'click': + el = this.elementManager.getKnownElement(pack[1], this.frame); + let button = pack[2]; + let clickCount = pack[3]; + c = this.coordinates(el, null, null); + this.mouseTap(el.ownerDocument, c.x, c.y, button, clickCount, + keyModifiers); + if (button == 2) { + this.emitMouseEvent(el.ownerDocument, 'contextmenu', c.x, c.y, + button, clickCount, keyModifiers); + } + this.actions(chain, touchId, i, keyModifiers); + break; + case 'press': + if (this.lastCoordinates) { + this.generateEvents('cancel', this.lastCoordinates[0], this.lastCoordinates[1], + touchId, null, keyModifiers); + this.onError("Invalid Command: press cannot follow an active touch event", 500, null); + this.resetValues(); + return; + } + // look ahead to check if we're scrolling. Needed for APZ touch dispatching. + if ((i != chain.length) && (chain[i][0].indexOf('move') !== -1)) { + this.scrolling = true; + } + el = this.elementManager.getKnownElement(pack[1], this.frame); + c = this.coordinates(el, pack[2], pack[3]); + touchId = this.generateEvents('press', c.x, c.y, null, el, keyModifiers); + this.actions(chain, touchId, i, keyModifiers); + break; + case 'release': + this.generateEvents('release', this.lastCoordinates[0], this.lastCoordinates[1], + touchId, null, keyModifiers); + this.actions(chain, null, i, keyModifiers); + this.scrolling = false; + break; + case 'move': + el = this.elementManager.getKnownElement(pack[1], this.frame); + c = this.coordinates(el); + this.generateEvents('move', c.x, c.y, touchId, null, keyModifiers); + this.actions(chain, touchId, i, keyModifiers); + break; + case 'moveByOffset': + this.generateEvents('move', this.lastCoordinates[0] + pack[1], + this.lastCoordinates[1] + pack[2], + touchId, null, keyModifiers); + this.actions(chain, touchId, i, keyModifiers); + break; + case 'wait': + if (pack[1] != null ) { + let time = pack[1]*1000; + // standard waiting time to fire contextmenu + let standard = 750; + try { + standard = Services.prefs.getIntPref("ui.click_hold_context_menus.delay"); + } + catch (e){} + if (time >= standard && this.isTap) { + chain.splice(i, 0, ['longPress'], ['wait', (time-standard)/1000]); + time = standard; + } + this.checkTimer.initWithCallback(() => { + this.actions(chain, touchId, i, keyModifiers); + }, time, Components.interfaces.nsITimer.TYPE_ONE_SHOT); + } + else { + this.actions(chain, touchId, i, keyModifiers); + } + break; + case 'cancel': + this.generateEvents('cancel', this.lastCoordinates[0], this.lastCoordinates[1], + touchId, null, keyModifiers); + this.actions(chain, touchId, i, keyModifiers); + this.scrolling = false; + break; + case 'longPress': + this.generateEvents('contextmenu', this.lastCoordinates[0], this.lastCoordinates[1], + touchId, null, keyModifiers); + this.actions(chain, touchId, i, keyModifiers); + break; + } + }, + + /** + * This function generates a pair of coordinates relative to the viewport given a + * target element and coordinates relative to that element's top-left corner. + * @param 'x', and 'y' are the relative to the target. + * If they are not specified, then the center of the target is used. + */ + coordinates: function (target, x, y) { + let box = target.getBoundingClientRect(); + if (x == null) { + x = box.width / 2; + } + if (y == null) { + y = box.height / 2; + } + let coords = {}; + coords.x = box.left + x; + coords.y = box.top + y; + return coords; + }, + + /** + * Given an element and a pair of coordinates, returns an array of the form + * [ clientX, clientY, pageX, pageY, screenX, screenY ] + */ + getCoordinateInfo: function (el, corx, cory) { + let win = el.ownerDocument.defaultView; + return [ corx, // clientX + cory, // clientY + corx + win.pageXOffset, // pageX + cory + win.pageYOffset, // pageY + corx + win.mozInnerScreenX, // screenX + cory + win.mozInnerScreenY // screenY + ]; + }, + + //x and y are coordinates relative to the viewport + generateEvents: function (type, x, y, touchId, target, keyModifiers) { + this.lastCoordinates = [x, y]; + let doc = this.frame.document; + switch (type) { + case 'tap': + if (this.mouseEventsOnly) { + this.mouseTap(touch.target.ownerDocument, touch.clientX, touch.clientY, + null, null, keyModifiers); + } else { + touchId = this.nextTouchId++; + let touch = this.touchProvider.createATouch(target, x, y, touchId); + this.touchProvider.emitTouchEvent('touchstart', touch); + this.touchProvider.emitTouchEvent('touchend', touch); + this.mouseTap(touch.target.ownerDocument, touch.clientX, touch.clientY, + null, null, keyModifiers); + } + this.lastCoordinates = null; + break; + case 'press': + this.isTap = true; + if (this.mouseEventsOnly) { + this.emitMouseEvent(doc, 'mousemove', x, y, null, null, keyModifiers); + this.emitMouseEvent(doc, 'mousedown', x, y, null, null, keyModifiers); + } + else { + touchId = this.nextTouchId++; + let touch = this.touchProvider.createATouch(target, x, y, touchId); + this.touchProvider.emitTouchEvent('touchstart', touch); + this.touchIds[touchId] = touch; + return touchId; + } + break; + case 'release': + if (this.mouseEventsOnly) { + let [x, y] = this.lastCoordinates; + this.emitMouseEvent(doc, 'mouseup', x, y, + null, null, keyModifiers); + } + else { + let touch = this.touchIds[touchId]; + let [x, y] = this.lastCoordinates; + touch = this.touchProvider.createATouch(touch.target, x, y, touchId); + this.touchProvider.emitTouchEvent('touchend', touch); + if (this.isTap) { + this.mouseTap(touch.target.ownerDocument, touch.clientX, touch.clientY, + null, null, keyModifiers); + } + delete this.touchIds[touchId]; + } + this.isTap = false; + this.lastCoordinates = null; + break; + case 'cancel': + this.isTap = false; + if (this.mouseEventsOnly) { + let [x, y] = this.lastCoordinates; + this.emitMouseEvent(doc, 'mouseup', x, y, + null, null, keyModifiers); + } + else { + this.touchProvider.emitTouchEvent('touchcancel', this.touchIds[touchId]); + delete this.touchIds[touchId]; + } + this.lastCoordinates = null; + break; + case 'move': + this.isTap = false; + if (this.mouseEventsOnly) { + this.emitMouseEvent(doc, 'mousemove', x, y, null, null, keyModifiers); + } + else { + let touch = this.touchProvider.createATouch(this.touchIds[touchId].target, + x, y, touchId); + this.touchIds[touchId] = touch; + this.touchProvider.emitTouchEvent('touchmove', touch); + } + break; + case 'contextmenu': + this.isTap = false; + let event = this.frame.document.createEvent('MouseEvents'); + if (this.mouseEventsOnly) { + target = doc.elementFromPoint(this.lastCoordinates[0], this.lastCoordinates[1]); + } + else { + target = this.touchIds[touchId].target; + } + let [ clientX, clientY, + pageX, pageY, + screenX, screenY ] = this.getCoordinateInfo(target, x, y); + event.initMouseEvent('contextmenu', true, true, + target.ownerDocument.defaultView, 1, + screenX, screenY, clientX, clientY, + false, false, false, false, 0, null); + target.dispatchEvent(event); + break; + default: + throw {message:"Unknown event type: " + type, code: 500, stack:null}; + } + this.checkForInterrupted(); + }, + + mouseTap: function (doc, x, y, button, clickCount, keyModifiers) { + this.emitMouseEvent(doc, 'mousemove', x, y, button, clickCount, keyModifiers); + this.emitMouseEvent(doc, 'mousedown', x, y, button, clickCount, keyModifiers); + this.emitMouseEvent(doc, 'mouseup', x, y, button, clickCount, keyModifiers); + }, +} diff --git a/testing/marionette/marionette-elements.js b/testing/marionette/marionette-elements.js index 97815c9090..f764d4abf0 100644 --- a/testing/marionette/marionette-elements.js +++ b/testing/marionette/marionette-elements.js @@ -422,18 +422,18 @@ ElementManager.prototype = { * as the start node instead of the document root * If this object has a 'time' member, this number will be * used to see if we have hit the search timelimit. - * @param function on_success - * The notification callback used when we are returning successfully. - * @param function on_error - The callback to invoke when an error occurs. * @param boolean all * If true, all found elements will be returned. * If false, only the first element will be returned. + * @param function on_success + * Callback used when operating is successful. + * @param function on_error + * Callback to invoke when an error occurs. * * @return nsIDOMElement or list of nsIDOMElements * Returns the element(s) by calling the on_success function. */ - find: function EM_find(win, values, searchTimeout, on_success, on_error, all, command_id) { + find: function EM_find(win, values, searchTimeout, all, on_success, on_error, command_id) { let startTime = values.time ? values.time : new Date().getTime(); let startNode = (values.element != undefined) ? this.getKnownElement(values.element, win) : win.document; @@ -456,13 +456,13 @@ ElementManager.prototype = { } else if (values.using == ANON_ATTRIBUTE) { message = "Unable to locate anonymous element: " + JSON.stringify(values.value); } - on_error(message, 7, null, command_id); + on_error({message: message, code: 7}, command_id); } } else { values.time = startTime; this.timer.initWithCallback(this.find.bind(this, win, values, - searchTimeout, - on_success, on_error, all, + searchTimeout, all, + on_success, on_error, command_id), 100, Components.interfaces.nsITimer.TYPE_ONE_SHOT); @@ -471,14 +471,13 @@ ElementManager.prototype = { if (isArrayLike) { let ids = [] for (let i = 0 ; i < found.length ; i++) { - ids.push({'ELEMENT': this.addToKnownElements(found[i])}); + ids.push({"ELEMENT": this.addToKnownElements(found[i])}); } on_success(ids, command_id); } else { let id = this.addToKnownElements(found); - on_success({'ELEMENT':id}, command_id); + on_success({"ELEMENT": id}, command_id); } - return; } }, diff --git a/testing/marionette/marionette-frame-manager.js b/testing/marionette/marionette-frame-manager.js index b86fd4cb1c..a6eb4e9e59 100644 --- a/testing/marionette/marionette-frame-manager.js +++ b/testing/marionette/marionette-frame-manager.js @@ -2,17 +2,15 @@ * 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.EXPORTED_SYMBOLS = [ - "FrameManager" -]; +let {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +this.EXPORTED_SYMBOLS = ["FrameManager"]; let FRAME_SCRIPT = "chrome://marionette/content/marionette-listener.js"; + Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -let logger = Log.repository.getLogger("Marionette"); - let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] .getService(Ci.mozIJSSubScriptLoader); let specialpowers = {}; @@ -99,30 +97,43 @@ FrameManager.prototype = { } }, - //This is just 'switch to OOP frame'. We're handling this here so we can maintain a list of remoteFrames. - switchToFrame: function FM_switchToFrame(message) { - // Switch to a remote frame. - let frameWindow = Services.wm.getOuterWindowWithId(message.json.win); //get the original frame window - let oopFrame = frameWindow.document.getElementsByTagName("iframe")[message.json.frame]; //find the OOP frame - let mm = oopFrame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager; //get the OOP frame's mm + getOopFrame: function FM_getOopFrame(winId, frameId) { + // get original frame window + let outerWin = Services.wm.getOuterWindowWithId(winId); + // find the OOP frame + let f = outerWin.document.getElementsByTagName("iframe")[frameId]; + return f; + }, + + getFrameMM: function FM_getFrameMM(winId, frameId) { + let oopFrame = this.getOopFrame(winId, frameId); + let mm = oopFrame.QueryInterface(Ci.nsIFrameLoaderOwner) + .frameLoader.messageManager; + return mm; + }, + + /** + * Switch to OOP frame. We're handling this here + * so we can maintain a list of remote frames. + */ + switchToFrame: function FM_switchToFrame(winId, frameId) { + let oopFrame = this.getOopFrame(winId, frameId); + let mm = this.getFrameMM(winId, frameId); if (!specialpowers.hasOwnProperty("specialPowersObserver")) { loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserver.js", - specialpowers); + specialpowers); } - // See if this frame already has our frame script loaded in it; if so, - // just wake it up. + // See if this frame already has our frame script loaded in it; + // if so, just wake it up. for (let i = 0; i < remoteFrames.length; i++) { let frame = remoteFrames[i]; let frameMessageManager = frame.messageManager.get(); - logger.info("trying remote frame " + i); try { frameMessageManager.sendAsyncMessage("aliveCheck", {}); - } - catch(e) { + } catch (e) { if (e.result == Components.results.NS_ERROR_NOT_INITIALIZED) { - logger.info("deleting frame"); remoteFrames.splice(i, 1); continue; } @@ -130,29 +141,31 @@ FrameManager.prototype = { if (frameMessageManager == mm) { this.currentRemoteFrame = frame; this.addMessageManagerListeners(mm); + if (!frame.specialPowersObserver) { frame.specialPowersObserver = new specialpowers.SpecialPowersObserver(); frame.specialPowersObserver.init(mm); } - mm.sendAsyncMessage("Marionette:restart", {}); + mm.sendAsyncMessage("Marionette:restart"); return oopFrame.id; } } // If we get here, then we need to load the frame script in this frame, - // and set the frame's ChromeMessageSender as the active message manager the server will listen to + // and set the frame's ChromeMessageSender as the active message manager + // the server will listen to. this.addMessageManagerListeners(mm); - let aFrame = new MarionetteRemoteFrame(message.json.win, message.json.frame); + let aFrame = new MarionetteRemoteFrame(winId, frameId); aFrame.messageManager = Cu.getWeakReference(mm); remoteFrames.push(aFrame); this.currentRemoteFrame = aFrame; - logger.info("frame-manager load script: " + mm.toString()); mm.loadFrameScript(FRAME_SCRIPT, true, true); aFrame.specialPowersObserver = new specialpowers.SpecialPowersObserver(); aFrame.specialPowersObserver.init(mm); + return oopFrame.id; }, @@ -184,64 +197,58 @@ FrameManager.prototype = { }, /** - * Adds message listeners to the server, listening for messages from content frame scripts. - * It also adds a "MarionetteFrame:getInterruptedState" message listener to the FrameManager, - * so the frame manager's state can be checked by the frame + * Adds message listeners to the server, + * listening for messages from content frame scripts. + * It also adds a MarionetteFrame:getInterruptedState + * message listener to the FrameManager, + * so the frame manager's state can be checked by the frame. * - * @param object messageManager - * The messageManager object (ChromeMessageBroadcaster or ChromeMessageSender) - * to which the listeners should be added. + * @param {nsIMessageListenerManager} mm + * The message manager object, typically + * ChromeMessageBroadcaster or ChromeMessageSender. */ - addMessageManagerListeners: function MDA_addMessageManagerListeners(messageManager) { - messageManager.addWeakMessageListener("Marionette:ok", this.server); - messageManager.addWeakMessageListener("Marionette:done", this.server); - messageManager.addWeakMessageListener("Marionette:error", this.server); - messageManager.addWeakMessageListener("Marionette:emitTouchEvent", this.server); - messageManager.addWeakMessageListener("Marionette:log", this.server); - messageManager.addWeakMessageListener("Marionette:register", this.server); - messageManager.addWeakMessageListener("Marionette:runEmulatorCmd", this.server); - messageManager.addWeakMessageListener("Marionette:runEmulatorShell", this.server); - messageManager.addWeakMessageListener("Marionette:shareData", this.server); - messageManager.addWeakMessageListener("Marionette:switchToModalOrigin", this.server); - messageManager.addWeakMessageListener("Marionette:switchToFrame", this.server); - messageManager.addWeakMessageListener("Marionette:switchedToFrame", this.server); - messageManager.addWeakMessageListener("Marionette:addCookie", this.server); - messageManager.addWeakMessageListener("Marionette:getVisibleCookies", this.server); - messageManager.addWeakMessageListener("Marionette:deleteCookie", this.server); - messageManager.addWeakMessageListener("Marionette:listenersAttached", this.server); - messageManager.addWeakMessageListener("MarionetteFrame:handleModal", this); - messageManager.addWeakMessageListener("MarionetteFrame:getCurrentFrameId", this); - messageManager.addWeakMessageListener("MarionetteFrame:getInterruptedState", this); + addMessageManagerListeners: function FM_addMessageManagerListeners(mm) { + mm.addWeakMessageListener("Marionette:emitTouchEvent", this.server); + mm.addWeakMessageListener("Marionette:log", this.server); + mm.addWeakMessageListener("Marionette:runEmulatorCmd", this.server); + mm.addWeakMessageListener("Marionette:runEmulatorShell", this.server); + mm.addWeakMessageListener("Marionette:shareData", this.server); + mm.addWeakMessageListener("Marionette:switchToModalOrigin", this.server); + mm.addWeakMessageListener("Marionette:switchedToFrame", this.server); + mm.addWeakMessageListener("Marionette:addCookie", this.server); + mm.addWeakMessageListener("Marionette:getVisibleCookies", this.server); + mm.addWeakMessageListener("Marionette:deleteCookie", this.server); + mm.addWeakMessageListener("Marionette:register", this.server); + mm.addWeakMessageListener("Marionette:listenersAttached", this.server); + mm.addWeakMessageListener("MarionetteFrame:handleModal", this); + mm.addWeakMessageListener("MarionetteFrame:getCurrentFrameId", this); + mm.addWeakMessageListener("MarionetteFrame:getInterruptedState", this); }, /** * Removes listeners for messages from content frame scripts. - * We do not remove the "MarionetteFrame:getInterruptedState" or the - * "Marioentte:switchToModalOrigin" message listener, - * because we want to allow all known frames to contact the frame manager so that - * it can check if it was interrupted, and if so, it will call switchToModalOrigin - * when its process gets resumed. + * We do not remove the MarionetteFrame:getInterruptedState + * or the Marionette:switchToModalOrigin message listener, + * because we want to allow all known frames to contact the frame manager + * so that it can check if it was interrupted, and if so, + * it will call switchToModalOrigin when its process gets resumed. * - * @param object messageManager - * The messageManager object (ChromeMessageBroadcaster or ChromeMessageSender) - * from which the listeners should be removed. + * @param {nsIMessageListenerManager} mm + * The message manager object, typically + * ChromeMessageBroadcaster or ChromeMessageSender. */ - removeMessageManagerListeners: function MDA_removeMessageManagerListeners(messageManager) { - messageManager.removeWeakMessageListener("Marionette:ok", this.server); - messageManager.removeWeakMessageListener("Marionette:done", this.server); - messageManager.removeWeakMessageListener("Marionette:error", this.server); - messageManager.removeWeakMessageListener("Marionette:log", this.server); - messageManager.removeWeakMessageListener("Marionette:shareData", this.server); - messageManager.removeWeakMessageListener("Marionette:register", this.server); - messageManager.removeWeakMessageListener("Marionette:runEmulatorCmd", this.server); - messageManager.removeWeakMessageListener("Marionette:runEmulatorShell", this.server); - messageManager.removeWeakMessageListener("Marionette:switchToFrame", this.server); - messageManager.removeWeakMessageListener("Marionette:switchedToFrame", this.server); - messageManager.removeWeakMessageListener("Marionette:addCookie", this.server); - messageManager.removeWeakMessageListener("Marionette:getVisibleCookies", this.server); - messageManager.removeWeakMessageListener("Marionette:deleteCookie", this.server); - messageManager.removeWeakMessageListener("Marionette:listenersAttached", this.server); - messageManager.removeWeakMessageListener("MarionetteFrame:handleModal", this); - messageManager.removeWeakMessageListener("MarionetteFrame:getCurrentFrameId", this); - }, + removeMessageManagerListeners: function FM_removeMessageManagerListeners(mm) { + mm.removeWeakMessageListener("Marionette:log", this.server); + mm.removeWeakMessageListener("Marionette:shareData", this.server); + mm.removeWeakMessageListener("Marionette:runEmulatorCmd", this.server); + mm.removeWeakMessageListener("Marionette:runEmulatorShell", this.server); + mm.removeWeakMessageListener("Marionette:switchedToFrame", this.server); + mm.removeWeakMessageListener("Marionette:addCookie", this.server); + mm.removeWeakMessageListener("Marionette:getVisibleCookies", this.server); + mm.removeWeakMessageListener("Marionette:deleteCookie", this.server); + mm.removeWeakMessageListener("Marionette:listenersAttached", this.server); + mm.removeWeakMessageListener("Marionette:register", this.server); + mm.removeWeakMessageListener("MarionetteFrame:handleModal", this); + mm.removeWeakMessageListener("MarionetteFrame:getCurrentFrameId", this); + } }; diff --git a/testing/marionette/marionette-listener.js b/testing/marionette/marionette-listener.js index 9a3ef1cfec..a0ec9701fd 100644 --- a/testing/marionette/marionette-listener.js +++ b/testing/marionette/marionette-listener.js @@ -13,6 +13,7 @@ let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] loader.loadSubScript("chrome://marionette/content/marionette-simpletest.js"); loader.loadSubScript("chrome://marionette/content/marionette-common.js"); +loader.loadSubScript("chrome://marionette/content/marionette-actions.js"); Cu.import("chrome://marionette/content/marionette-elements.js"); Cu.import("resource://gre/modules/FileUtils.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); @@ -40,8 +41,8 @@ let curFrame = content; let previousFrame = null; let elementManager = new ElementManager([]); let accessibility = new Accessibility(); +let actions = new ActionChain(utils, checkForInterrupted); let importedScripts = null; -let inputSource = null; // The sandbox we execute test scripts in. Gets lazily created in // createExecuteContentSandbox(). @@ -68,17 +69,8 @@ let navTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); let onDOMContentLoaded; // Send move events about this often let EVENT_INTERVAL = 30; // milliseconds -// For assigning unique ids to all touches -let nextTouchId = 1000; -//Keep track of active Touches -let touchIds = {}; // last touch for each fingerId let multiLast = {}; -let lastCoordinates = null; -let isTap = false; -let scrolling = false; -// whether to send mouse event -let mouseEventsOnly = false; Cu.import("resource://gre/modules/Log.jsm"); let logger = Log.repository.getLogger("Marionette"); @@ -115,16 +107,18 @@ function registerSelf() { importedScripts = FileUtils.getDir('TmpD', [], false); importedScripts.append('marionetteContentScripts'); startListeners(); + let rv = {}; if (remotenessChange) { - sendAsyncMessage("Marionette:listenersAttached", {listenerId: id}); + rv.listenerId = id; } + sendAsyncMessage("Marionette:listenersAttached", rv); } } } function emitTouchEventForIFrame(message) { message = message.json; - let identifier = nextTouchId; + let identifier = actions.nextTouchId; let domWindowUtils = curFrame. QueryInterface(Components.interfaces.nsIInterfaceRequestor). @@ -245,7 +239,7 @@ function newSession(msg) { // events being the result of a physical mouse action. // This is especially important for the touch event shim, // in order to prevent creating touch event for these fake mouse events. - inputSource = Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH; + actions.inputSource = Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH; } } @@ -326,7 +320,7 @@ function deleteSession(msg) { // reset frame to the top-most frame curFrame = content; curFrame.focus(); - touchIds = {}; + actions.touchIds = {}; } /* @@ -367,9 +361,9 @@ function sendLog(msg) { /** * Send error message to server */ -function sendError(message, status, trace, command_id) { - let error_msg = { message: message, status: status, stacktrace: trace }; - sendToServer("Marionette:error", error_msg, command_id); +function sendError(msg, code, stack, cmdId) { + let payload = {message: msg, code: code, stack: stack}; + sendToServer("Marionette:error", payload, cmdId); } /** @@ -378,7 +372,7 @@ function sendError(message, status, trace, command_id) { function resetValues() { sandbox = null; curFrame = content; - mouseEventsOnly = false; + actions.mouseEventsOnly = false; } /** @@ -404,6 +398,22 @@ function wasInterrupted() { return sendSyncMessage("MarionetteFrame:getInterruptedState", {})[0].value; } +function checkForInterrupted() { + if (wasInterrupted()) { + if (previousFrame) { + //if previousFrame is set, then we're in a single process environment + cuFrame = actions.frame = previousFrame; + previousFrame = null; + sandbox = null; + } + else { + //else we're in OOP environment, so we'll switch to the original OOP frame + sendSyncMessage("Marionette:switchToModalOrigin"); + } + sendSyncMessage("Marionette:switchedToFrame", { restorePrevious: true }); + } +} + /* * Marionette Methods */ @@ -424,6 +434,8 @@ function createExecuteContentSandbox(aWindow, timeout) { marionetteLogObj, timeout, heartbeatCallback, marionetteTestName); + marionette.runEmulatorCmd = (cmd, cb) => this.runEmulatorCmd(cmd, cb); + marionette.runEmulatorShell = (args, cb) => this.runEmulatorShell(args, cb); sandbox.marionette = marionette; marionette.exports.forEach(function(fn) { try { @@ -731,7 +743,7 @@ function emitTouchEvent(type, touch) { QueryInterface(Components.interfaces.nsIInterfaceRequestor). getInterface(Components.interfaces.nsIWebNavigation). QueryInterface(Components.interfaces.nsIDocShell); - if (docShell.asyncPanZoomEnabled && scrolling) { + if (docShell.asyncPanZoomEnabled && actions.scrolling) { // if we're in APZ and we're scrolling, we must use injectTouchEvent to dispatch our touchmove events let index = sendSyncMessage("MarionetteFrame:getCurrentFrameId"); // only call emitTouchEventForIFrame if we're inside an iframe. @@ -758,53 +770,6 @@ function emitTouchEvent(type, touch) { } } -/** - * This function emit mouse event - * @param: doc is the current document - * type is the type of event to dispatch - * clickCount is the number of clicks, button notes the mouse button - * elClientX and elClientY are the coordinates of the mouse relative to the viewport - * modifiers is an object of modifier keys present - */ -function emitMouseEvent(doc, type, elClientX, elClientY, button, clickCount, modifiers) { - if (!wasInterrupted()) { - let loggingInfo = "emitting Mouse event of type " + type + - " at coordinates (" + elClientX + ", " + elClientY + - ") relative to the viewport\n" + - " button: " + button + "\n" + - " clickCount: " + clickCount + "\n"; - dumpLog(loggingInfo); - /* - Disabled per bug 888303 - marionetteLogObj.log(loggingInfo, "TRACE"); - sendSyncMessage("Marionette:shareData", - {log: elementManager.wrapValue(marionetteLogObj.getLogs())}); - marionetteLogObj.clearLogs(); - */ - let win = doc.defaultView; - let domUtils = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor) - .getInterface(Components.interfaces.nsIDOMWindowUtils); - let mods; - if (typeof modifiers != "undefined") { - mods = utils._parseModifiers(modifiers); - } else { - mods = 0; - } - domUtils.sendMouseEvent(type, elClientX, elClientY, button || 0, clickCount || 1, - mods, false, 0, inputSource); - } -} - -/** - * Helper function that perform a mouse tap - */ -function mousetap(doc, x, y, keyModifiers) { - emitMouseEvent(doc, 'mousemove', x, y, null, null, keyModifiers); - emitMouseEvent(doc, 'mousedown', x, y, null, null, keyModifiers); - emitMouseEvent(doc, 'mouseup', x, y, null, null, keyModifiers); -} - - /** * This function generates a pair of coordinates relative to the viewport given a * target element and coordinates relative to that element's top-left corner. @@ -825,6 +790,7 @@ function coordinates(target, x, y) { return coords; } + /** * This function returns true if the given coordinates are in the viewport. * @param 'x', and 'y' are the coordinates relative to the target. @@ -876,113 +842,6 @@ function checkVisible(el, x, y) { return true; } -//x and y are coordinates relative to the viewport -function generateEvents(type, x, y, touchId, target, keyModifiers) { - lastCoordinates = [x, y]; - let doc = curFrame.document; - switch (type) { - case 'tap': - if (mouseEventsOnly) { - mousetap(target.ownerDocument, x, y); - } - else { - let touchId = nextTouchId++; - let touch = createATouch(target, x, y, touchId); - emitTouchEvent('touchstart', touch); - emitTouchEvent('touchend', touch); - mousetap(target.ownerDocument, x, y); - } - lastCoordinates = null; - break; - case 'press': - isTap = true; - if (mouseEventsOnly) { - emitMouseEvent(doc, 'mousemove', x, y, null, null, keyModifiers); - emitMouseEvent(doc, 'mousedown', x, y, null, null, keyModifiers); - } - else { - let touchId = nextTouchId++; - let touch = createATouch(target, x, y, touchId); - emitTouchEvent('touchstart', touch); - touchIds[touchId] = touch; - return touchId; - } - break; - case 'release': - if (mouseEventsOnly) { - emitMouseEvent(doc, 'mouseup', lastCoordinates[0], lastCoordinates[1], - null, null, keyModifiers); - } - else { - let touch = touchIds[touchId]; - touch = createATouch(touch.target, lastCoordinates[0], lastCoordinates[1], touchId); - emitTouchEvent('touchend', touch); - if (isTap) { - mousetap(touch.target.ownerDocument, touch.clientX, touch.clientY, keyModifiers); - } - delete touchIds[touchId]; - } - isTap = false; - lastCoordinates = null; - break; - case 'cancel': - isTap = false; - if (mouseEventsOnly) { - emitMouseEvent(doc, 'mouseup', lastCoordinates[0], lastCoordinates[1], - null, null, keyModifiers); - } - else { - emitTouchEvent('touchcancel', touchIds[touchId]); - delete touchIds[touchId]; - } - lastCoordinates = null; - break; - case 'move': - isTap = false; - if (mouseEventsOnly) { - emitMouseEvent(doc, 'mousemove', x, y, null, null, keyModifiers); - } - else { - touch = createATouch(touchIds[touchId].target, x, y, touchId); - touchIds[touchId] = touch; - emitTouchEvent('touchmove', touch); - } - break; - case 'contextmenu': - isTap = false; - let event = curFrame.document.createEvent('MouseEvents'); - if (mouseEventsOnly) { - target = doc.elementFromPoint(lastCoordinates[0], lastCoordinates[1]); - } - else { - target = touchIds[touchId].target; - } - let [ clientX, clientY, - pageX, pageY, - screenX, screenY ] = getCoordinateInfo(target, x, y); - event.initMouseEvent('contextmenu', true, true, - target.ownerDocument.defaultView, 1, - screenX, screenY, clientX, clientY, - false, false, false, false, 0, null); - target.dispatchEvent(event); - break; - default: - throw {message:"Unknown event type: " + type, code: 500, stack:null}; - } - if (wasInterrupted()) { - if (previousFrame) { - //if previousFrame is set, then we're in a single process environment - curFrame = previousFrame; - previousFrame = null; - sandbox = null; - } - else { - //else we're in OOP environment, so we'll switch to the original OOP frame - sendSyncMessage("Marionette:switchToModalOrigin"); - } - sendSyncMessage("Marionette:switchedToFrame", { restorePrevious: true }); - } -} /** * Function that perform a single tap @@ -1001,10 +860,16 @@ function singleTap(msg) { } checkActionableAccessibility(acc); if (!curFrame.document.createTouch) { - mouseEventsOnly = true; + actions.mouseEventsOnly = true; } - c = coordinates(el, msg.json.corx, msg.json.cory); - generateEvents('tap', c.x, c.y, null, el); + let c = coordinates(el, msg.json.corx, msg.json.cory); + if (!actions.mouseEventsOnly) { + let touchId = actions.nextTouchId++; + let touch = createATouch(el, c.x, c.y, touchId); + emitTouchEvent('touchstart', touch); + emitTouchEvent('touchend', touch); + } + actions.mouseTap(el.ownerDocument, c.x, c.y); sendOk(msg.json.command_id); } catch (e) { @@ -1070,20 +935,6 @@ function checkActionableAccessibility(accesible) { accessibility.handleErrorMessage(message); } -/** - * Given an element and a pair of coordinates, returns an array of the form - * [ clientX, clientY, pageX, pageY, screenX, screenY ] - */ -function getCoordinateInfo(el, corx, cory) { - let win = el.ownerDocument.defaultView; - return [ corx, // clientX - cory, // clientY - corx + win.pageXOffset, // pageX - cory + win.pageYOffset, // pageY - corx + win.mozInnerScreenX, // screenX - cory + win.mozInnerScreenY // screenY - ]; -} /** * Function to create a touch based on the element @@ -1093,138 +944,11 @@ function createATouch(el, corx, cory, touchId) { let doc = el.ownerDocument; let win = doc.defaultView; let [clientX, clientY, pageX, pageY, screenX, screenY] = - getCoordinateInfo(el, corx, cory); + actions.getCoordinateInfo(el, corx, cory); let atouch = doc.createTouch(win, el, touchId, pageX, pageY, screenX, screenY, clientX, clientY); return atouch; } -/** - * Function to emit touch events for each finger. e.g. finger=[['press', id], ['wait', 5], ['release']] - * touchId represents the finger id, i keeps track of the current action of the chain - * keyModifiers is an object keeping track keyDown/keyUp pairs through an action chain. - */ -function actions(chain, touchId, command_id, i, keyModifiers) { - if (typeof i === "undefined") { - i = 0; - } - if (typeof keyModifiers === "undefined") { - keyModifiers = { - shiftKey: false, - ctrlKey: false, - altKey: false, - metaKey: false - }; - } - if (i == chain.length) { - sendResponse({value: touchId}, command_id); - return; - } - let pack = chain[i]; - let command = pack[0]; - let el; - let c; - i++; - if (['press', 'wait', 'keyDown', 'keyUp'].indexOf(command) == -1) { - //if mouseEventsOnly, then touchIds isn't used - if (!(touchId in touchIds) && !mouseEventsOnly) { - sendError("Element has not been pressed", 500, null, command_id); - return; - } - } - switch(command) { - case 'keyDown': - utils.sendKeyDown(pack[1], keyModifiers, curFrame); - actions(chain, touchId, command_id, i, keyModifiers); - break; - case 'keyUp': - utils.sendKeyUp(pack[1], keyModifiers, curFrame); - actions(chain, touchId, command_id, i, keyModifiers); - break; - case 'click': - el = elementManager.getKnownElement(pack[1], curFrame); - let button = pack[2]; - let clickCount = pack[3]; - c = coordinates(el, null, null); - emitMouseEvent(el.ownerDocument, 'mousemove', c.x, c.y, button, clickCount, - keyModifiers); - emitMouseEvent(el.ownerDocument, 'mousedown', c.x, c.y, button, clickCount, - keyModifiers); - emitMouseEvent(el.ownerDocument, 'mouseup', c.x, c.y, button, clickCount, - keyModifiers); - if (button == 2) { - emitMouseEvent(el.ownerDocument, 'contextmenu', c.x, c.y, button, clickCount, - keyModifiers); - } - actions(chain, touchId, command_id, i, keyModifiers); - break; - case 'press': - if (lastCoordinates) { - generateEvents('cancel', lastCoordinates[0], lastCoordinates[1], - touchId, null, keyModifiers); - sendError("Invalid Command: press cannot follow an active touch event", 500, null, command_id); - return; - } - // look ahead to check if we're scrolling. Needed for APZ touch dispatching. - if ((i != chain.length) && (chain[i][0].indexOf('move') !== -1)) { - scrolling = true; - } - el = elementManager.getKnownElement(pack[1], curFrame); - c = coordinates(el, pack[2], pack[3]); - touchId = generateEvents('press', c.x, c.y, null, el, keyModifiers); - actions(chain, touchId, command_id, i, keyModifiers); - break; - case 'release': - generateEvents('release', lastCoordinates[0], lastCoordinates[1], - touchId, null, keyModifiers); - actions(chain, null, command_id, i, keyModifiers); - scrolling = false; - break; - case 'move': - el = elementManager.getKnownElement(pack[1], curFrame); - c = coordinates(el); - generateEvents('move', c.x, c.y, touchId, null, keyModifiers); - actions(chain, touchId, command_id, i, keyModifiers); - break; - case 'moveByOffset': - generateEvents('move', lastCoordinates[0] + pack[1], lastCoordinates[1] + pack[2], - touchId, null, keyModifiers); - actions(chain, touchId, command_id, i, keyModifiers); - break; - case 'wait': - if (pack[1] != null ) { - let time = pack[1]*1000; - // standard waiting time to fire contextmenu - let standard = 750; - try { - standard = Services.prefs.getIntPref("ui.click_hold_context_menus.delay"); - } - catch (e){} - if (time >= standard && isTap) { - chain.splice(i, 0, ['longPress'], ['wait', (time-standard)/1000]); - time = standard; - } - checkTimer.initWithCallback(function() { - actions(chain, touchId, command_id, i, keyModifiers); - }, time, Ci.nsITimer.TYPE_ONE_SHOT); - } - else { - actions(chain, touchId, command_id, i, keyModifiers); - } - break; - case 'cancel': - generateEvents('cancel', lastCoordinates[0], lastCoordinates[1], - touchId, null, keyModifiers); - actions(chain, touchId, command_id, i, keyModifiers); - scrolling = false; - break; - case 'longPress': - generateEvents('contextmenu', lastCoordinates[0], lastCoordinates[1], - touchId, null, keyModifiers); - actions(chain, touchId, command_id, i, keyModifiers); - break; - } -} - /** * Function to start action chain on one finger */ @@ -1232,20 +956,21 @@ function actionChain(msg) { let command_id = msg.json.command_id; let args = msg.json.chain; let touchId = msg.json.nextId; - try { - let commandArray = elementManager.convertWrappedArguments(args, curFrame); - // loop the action array [ ['press', id], ['move', id], ['release', id] ] - if (touchId == null) { - touchId = nextTouchId++; - } - if (!curFrame.document.createTouch) { - mouseEventsOnly = true; - } - actions(commandArray, touchId, command_id); - } - catch (e) { - sendError(e.message, e.code, e.stack, msg.json.command_id); - } + + let callbacks = {}; + callbacks.onSuccess = (value) => { + sendResponse(value, command_id); + }; + callbacks.onError = (message, code, trace) => { + sendError(message, code, trace, msg.json.command_id); + }; + + let touchProvider = {}; + touchProvider.createATouch = createATouch; + touchProvider.emitTouchEvent = emitTouchEvent; + + actions.dispatchActions(args, touchId, curFrame, elementManager, callbacks, + touchProvider); } /** @@ -1571,10 +1296,10 @@ function refresh(msg) { function findElementContent(msg) { let command_id = msg.json.command_id; try { - let on_success = function(id, cmd_id) { sendResponse({value:id}, cmd_id); }; - let on_error = sendError; + let on_success = function(el, cmd_id) { sendResponse({value: el}, cmd_id) }; + let on_error = function(e, cmd_id) { sendError(e.message, e.code, null, cmd_id); }; elementManager.find(curFrame, msg.json, msg.json.searchTimeout, - on_success, on_error, false, command_id); + false /* all */, on_success, on_error, command_id); } catch (e) { sendError(e.message, e.code, e.stack, command_id); @@ -1587,10 +1312,10 @@ function findElementContent(msg) { function findElementsContent(msg) { let command_id = msg.json.command_id; try { - let on_success = function(id, cmd_id) { sendResponse({value:id}, cmd_id); }; - let on_error = sendError; + let on_success = function(els, cmd_id) { sendResponse({value: els}, cmd_id); }; + let on_error = function(e, cmd_id) { sendError(e.message, e.code, null, cmd_id); }; elementManager.find(curFrame, msg.json, msg.json.searchTimeout, - on_success, on_error, true, command_id); + true /* all */, on_success, on_error, command_id); } catch (e) { sendError(e.message, e.code, e.stack, command_id); @@ -1981,21 +1706,20 @@ function switchToFrame(msg) { let frameValue = elementManager.wrapValue(curFrame.wrappedJSObject)['ELEMENT']; sendSyncMessage("Marionette:switchedToFrame", { frameValue: frameValue }); - if (curFrame.contentWindow == null) { - // The frame we want to switch to is a remote (out-of-process) frame; - // notify our parent to handle the switch. + let rv = null; + if (curFrame.contentWindow === null) { + // The frame we want to switch to is a remote/OOP frame; + // notify our parent to handle the switch curFrame = content; - sendToServer('Marionette:switchToFrame', {win: parWindow, - frame: foundFrame, - command_id: command_id}); - } - else { + rv = {win: parWindow, frame: foundFrame}; + } else { curFrame = curFrame.contentWindow; - if(msg.json.focus == true) { + if (msg.json.focus) curFrame.focus(); - } checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT); } + + sendResponse({value: rv}, command_id); } /** * Add a cookie to the document diff --git a/testing/marionette/marionette-sendkeys.js b/testing/marionette/marionette-sendkeys.js index 5b2a16b94c..096330afc0 100644 --- a/testing/marionette/marionette-sendkeys.js +++ b/testing/marionette/marionette-sendkeys.js @@ -123,8 +123,8 @@ function sendSingleKey (keyToSend, modifiers, document) { utils.synthesizeKey(keyCode, modifiers, document); } -function sendKeysToElement (document, element, keysToSend, successCallback, errorCallback, command_id, context) { - if (context == "chrome" || checkVisible(element)) { +function sendKeysToElement (document, element, keysToSend, successCallback, errorCallback, command_id, ignoreVisibility) { + if (ignoreVisibility || checkVisible(element)) { element.focus(); let modifiers = { shiftKey: false, diff --git a/testing/marionette/marionette-server.js b/testing/marionette/marionette-server.js deleted file mode 100644 index 837752e678..0000000000 --- a/testing/marionette/marionette-server.js +++ /dev/null @@ -1,3654 +0,0 @@ -/* 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 FRAME_SCRIPT = "chrome://marionette/content/marionette-listener.js"; -const BROWSER_STARTUP_FINISHED = "browser-delayed-startup-finished"; -const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; - -// import logger -Cu.import("resource://gre/modules/Log.jsm"); -let logger = Log.repository.getLogger("Marionette"); -logger.info('marionette-server.js loaded'); - -let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader); -loader.loadSubScript("chrome://marionette/content/marionette-simpletest.js"); -loader.loadSubScript("chrome://marionette/content/marionette-common.js"); -Cu.import("resource://gre/modules/Services.jsm"); -loader.loadSubScript("chrome://marionette/content/marionette-frame-manager.js"); -Cu.import("chrome://marionette/content/marionette-elements.js"); -let utils = {}; -loader.loadSubScript("chrome://marionette/content/EventUtils.js", utils); -loader.loadSubScript("chrome://marionette/content/ChromeUtils.js", utils); -loader.loadSubScript("chrome://marionette/content/atoms.js", utils); -loader.loadSubScript("chrome://marionette/content/marionette-sendkeys.js", utils); - -let specialpowers = {}; - -Cu.import("resource://gre/modules/FileUtils.jsm"); -Cu.import("resource://gre/modules/NetUtil.jsm"); - -function isMulet() { - let isMulet = false; - try { - isMulet = Services.prefs.getBoolPref("b2g.is_mulet"); - } catch (ex) { } - return isMulet; -} - -Services.prefs.setBoolPref("marionette.contentListener", false); -let appName = isMulet() ? "B2G" : Services.appinfo.name; - -let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); -let DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js"); -this.DevToolsUtils = DevToolsUtils; -loader.loadSubScript("resource://gre/modules/devtools/transport/transport.js"); - -let bypassOffline = false; -let qemu = "0"; -let device = null; -const SECURITY_PREF = 'security.turn_off_all_security_so_that_viruses_can_take_over_this_computer'; - -XPCOMUtils.defineLazyServiceGetter(this, "cookieManager", - "@mozilla.org/cookiemanager;1", - "nsICookieManager"); - -try { - XPCOMUtils.defineLazyGetter(this, "libcutils", function () { - Cu.import("resource://gre/modules/systemlibs.js"); - return libcutils; - }); - if (libcutils) { - qemu = libcutils.property_get("ro.kernel.qemu"); - logger.info("B2G emulator: " + (qemu == "1" ? "yes" : "no")); - device = libcutils.property_get("ro.product.device"); - logger.info("Device detected is " + device); - bypassOffline = (qemu == "1" || device == "panda"); - } -} -catch(e) {} - -if (bypassOffline) { - logger.info("Bypassing offline status."); - Services.prefs.setBoolPref("network.gonk.manage-offline-status", false); - Services.io.manageOfflineStatus = false; - Services.io.offline = false; -} - -// This is used to prevent newSession from returning before the telephony -// API's are ready; see bug 792647. This assumes that marionette-server.js -// will be loaded before the 'system-message-listener-ready' message -// is fired. If this stops being true, this approach will have to change. -let systemMessageListenerReady = false; -Services.obs.addObserver(function() { - systemMessageListenerReady = true; -}, "system-message-listener-ready", false); - -// This is used on desktop to prevent newSession from returning before a page -// load initiated by the Firefox command line has completed. -let delayedBrowserStarted = false; -Services.obs.addObserver(function () { - delayedBrowserStarted = true; -}, BROWSER_STARTUP_FINISHED, false); - -/* - * Custom exceptions - */ -function FrameSendNotInitializedError(frame) { - this.code = 54; - this.frame = frame; - this.message = "Error sending message to frame (NS_ERROR_NOT_INITIALIZED)"; - this.toString = function() { - return this.message + " " + this.frame + "; frame has closed."; - } -} - -function FrameSendFailureError(frame) { - this.code = 55; - this.frame = frame; - this.message = "Error sending message to frame (NS_ERROR_FAILURE)"; - this.toString = function() { - return this.message + " " + this.frame + "; frame not responding."; - } -} - -/** - * The server connection is responsible for all marionette API calls. It gets created - * for each connection and manages all chrome and browser based calls. It - * mediates content calls by issuing appropriate messages to the content process. - */ -function MarionetteServerConnection(aPrefix, aTransport, aServer) -{ - this.uuidGen = Cc["@mozilla.org/uuid-generator;1"] - .getService(Ci.nsIUUIDGenerator); - - this.prefix = aPrefix; - this.server = aServer; - this.conn = aTransport; - this.conn.hooks = this; - - // marionette uses a protocol based on the debugger server, which requires - // passing back "actor ids" with responses. unlike the debugger server, - // we don't have multiple actors, so just use a dummy value of "0" here - this.actorID = "0"; - this.sessionId = null; - - this.globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"] - .getService(Ci.nsIMessageBroadcaster); - this.messageManager = this.globalMessageManager; - this.browsers = {}; //holds list of BrowserObjs - this.curBrowser = null; // points to current browser - this.context = "content"; - this.scriptTimeout = null; - this.searchTimeout = null; - this.pageTimeout = null; - this.timer = null; - this.inactivityTimer = null; - this.heartbeatCallback = function () {}; // called by simpletest methods - this.marionetteLog = new MarionetteLogObj(); - this.command_id = null; - this.mainFrame = null; //topmost chrome frame - this.curFrame = null; // chrome iframe that currently has focus - this.mainContentFrameId = null; - this.importedScripts = FileUtils.getFile('TmpD', ['marionetteChromeScripts']); - this.importedScriptHashes = {"chrome" : [], "content": []}; - this.currentFrameElement = null; - this.testName = null; - this.mozBrowserClose = null; - this.enabled_security_pref = false; - this.sandbox = null; - this.oopFrameId = null; // frame ID of current remote frame, used for mozbrowserclose events - this.sessionCapabilities = { - // Mandated capabilities - "browserName": appName, - "browserVersion": Services.appinfo.version, - "platformName": Services.appinfo.OS.toUpperCase(), - "platformVersion": Services.appinfo.platformVersion, - - // Supported features - "handlesAlerts": false, - "nativeEvents": false, - "raisesAccessibilityExceptions": false, - "rotatable": appName == "B2G", - "secureSsl": false, - "takesElementScreenshot": true, - "takesScreenshot": true, - - // Selenium 2 compat - "platform": Services.appinfo.OS.toUpperCase(), - - // Proprietary extensions - "XULappId" : Services.appinfo.ID, - "appBuildId" : Services.appinfo.appBuildID, - "device": qemu == "1" ? "qemu" : (!device ? "desktop" : device), - "version": Services.appinfo.version - }; - - this.observing = null; - this._browserIds = new WeakMap(); - this.quitFlags = null; -} - -MarionetteServerConnection.prototype = { - - QueryInterface: XPCOMUtils.generateQI([Ci.nsIMessageListener, - Ci.nsIObserver, - Ci.nsISupportsWeakReference]), - - /** - * Debugger transport callbacks: - */ - onPacket: function MSC_onPacket(aPacket) { - // Dispatch the request - if (this.requestTypes && this.requestTypes[aPacket.name]) { - try { - this.logRequest(aPacket.name, aPacket); - this.requestTypes[aPacket.name].bind(this)(aPacket); - } catch(e) { - this.conn.send({ error: ("error occurred while processing '" + - aPacket.name), - message: e.message }); - } - } else { - this.conn.send({ error: "unrecognizedPacketType", - message: ('Marionette does not ' + - 'recognize the packet type "' + - aPacket.name + '"') }); - } - }, - - onClosed: function MSC_onClosed(aStatus) { - this.server._connectionClosed(this); - this.sessionTearDown(); - - if (this.quitFlags !== null) { - let flags = this.quitFlags; - this.quitFlags = null; - Services.startup.quit(flags); - } - }, - - /** - * Helper methods: - */ - - /** - * Switches to the global ChromeMessageBroadcaster, potentially replacing a frame-specific - * ChromeMessageSender. Has no effect if the global ChromeMessageBroadcaster is already - * in use. If this replaces a frame-specific ChromeMessageSender, it removes the message - * listeners from that sender, and then puts the corresponding frame script "to sleep", - * which removes most of the message listeners from it as well. - */ - switchToGlobalMessageManager: function MDA_switchToGlobalMM() { - if (this.curBrowser && this.curBrowser.frameManager.currentRemoteFrame !== null) { - this.curBrowser.frameManager.removeMessageManagerListeners(this.messageManager); - this.sendAsync("sleepSession", null, null, true); - this.curBrowser.frameManager.currentRemoteFrame = null; - } - this.messageManager = this.globalMessageManager; - }, - - /** - * Helper method to send async messages to the content listener - * - * @param string name - * Suffix of the targetted message listener (Marionette:) - * @param object values - * Object to send to the listener - */ - sendAsync: function MDA_sendAsync(name, values, commandId, ignoreFailure) { - let success = true; - if (commandId) { - values.command_id = commandId; - } - if (this.curBrowser.frameManager.currentRemoteFrame !== null) { - try { - this.messageManager.sendAsyncMessage( - "Marionette:" + name + this.curBrowser.frameManager.currentRemoteFrame.targetFrameId, values); - } - catch(e) { - if (!ignoreFailure) { - success = false; - let error = e; - switch(e.result) { - case Components.results.NS_ERROR_FAILURE: - error = new FrameSendFailureError(this.curBrowser.frameManager.currentRemoteFrame); - break; - case Components.results.NS_ERROR_NOT_INITIALIZED: - error = new FrameSendNotInitializedError(this.curBrowser.frameManager.currentRemoteFrame); - break; - default: - break; - } - let code = error.hasOwnProperty('code') ? e.code : 500; - this.sendError(error.toString(), code, error.stack, commandId); - } - } - } - else { - this.curBrowser.executeWhenReady(() => { - this.messageManager.broadcastAsyncMessage( - "Marionette:" + name + this.curBrowser.curFrameId, values); - }); - } - return success; - }, - - logRequest: function MDA_logRequest(type, data) { - logger.debug("Got request: " + type + ", data: " + JSON.stringify(data) + ", id: " + this.command_id); - }, - - /** - * Generic method to pass a response to the client - * - * @param object msg - * Response to send back to client - * @param string command_id - * Unique identifier assigned to the client's request. - * Used to distinguish the asynchronous responses. - */ - sendToClient: function MDA_sendToClient(msg, command_id) { - logger.info("sendToClient: " + JSON.stringify(msg) + ", " + command_id + - ", " + this.command_id); - if (!command_id) { - logger.warn("got a response with no command_id"); - return; - } - else if (command_id != -1) { - // A command_id of -1 is used for emulator callbacks, and those - // don't use this.command_id. - if (!this.command_id) { - // A null value for this.command_id means we've already processed - // a message for the previous value, and so the current message is a - // duplicate. - logger.warn("ignoring duplicate response for command_id " + command_id); - return; - } - else if (this.command_id != command_id) { - logger.warn("ignoring out-of-sync response"); - return; - } - } - - if (this.curBrowser !== null) { - this.curBrowser.pendingCommands = []; - } - - this.conn.send(msg); - if (command_id != -1) { - // Don't unset this.command_id if this message is to process an - // emulator callback, since another response for this command_id is - // expected, after the containing call to execute_async_script finishes. - this.command_id = null; - } - }, - - /** - * Send a value to client - * - * @param object value - * Value to send back to client - * @param string command_id - * Unique identifier assigned to the client's request. - * Used to distinguish the asynchronous responses. - */ - sendResponse: function MDA_sendResponse(value, command_id) { - if (typeof(value) == 'undefined') - value = null; - this.sendToClient({from:this.actorID, - sessionId: this.sessionId, - value: value}, command_id); - }, - - sayHello: function MDA_sayHello() { - this.conn.send({ from: "root", - applicationType: "goanna", - traits: [] }); - }, - - getMarionetteID: function MDA_getMarionette() { - this.conn.send({ "from": "root", "id": this.actorID }); - }, - - /** - * Send ack to client - * - * @param string command_id - * Unique identifier assigned to the client's request. - * Used to distinguish the asynchronous responses. - */ - sendOk: function MDA_sendOk(command_id) { - this.sendToClient({from:this.actorID, ok: true}, command_id); - }, - - /** - * Send error message to client - * - * @param string message - * Error message - * @param number status - * Status number - * @param string trace - * Stack trace - * @param string command_id - * Unique identifier assigned to the client's request. - * Used to distinguish the asynchronous responses. - */ - sendError: function MDA_sendError(message, status, trace, command_id) { - let error_msg = {message: message, status: status, stacktrace: trace}; - this.sendToClient({from:this.actorID, error: error_msg}, command_id); - }, - - /** - * Gets the current active window - * - * @return nsIDOMWindow - */ - getCurrentWindow: function MDA_getCurrentWindow() { - let type = null; - if (this.curFrame == null) { - if (this.curBrowser == null) { - if (this.context == "content") { - type = 'navigator:browser'; - } - return Services.wm.getMostRecentWindow(type); - } - else { - return this.curBrowser.window; - } - } - else { - return this.curFrame; - } - }, - - /** - * Gets the the window enumerator - * - * @return nsISimpleEnumerator - */ - getWinEnumerator: function MDA_getWinEnumerator() { - let type = null; - if (appName != "B2G" && this.context == "content") { - type = 'navigator:browser'; - } - return Services.wm.getEnumerator(type); - }, - - /** - */ - addFrameCloseListener: function MDA_addFrameCloseListener(action) { - let curWindow = this.getCurrentWindow(); - let self = this; - this.mozBrowserClose = function(e) { - if (e.target.id == self.oopFrameId) { - curWindow.removeEventListener('mozbrowserclose', self.mozBrowserClose, true); - self.switchToGlobalMessageManager(); - self.sendError("The frame closed during the " + action + ", recovering to allow further communications", 55, null, self.command_id); - } - }; - curWindow.addEventListener('mozbrowserclose', this.mozBrowserClose, true); - }, - - /** - * Create a new BrowserObj for window and add to known browsers - * - * @param nsIDOMWindow win - * Window for which we will create a BrowserObj - * - * @return string - * Returns the unique server-assigned ID of the window - */ - addBrowser: function MDA_addBrowser(win) { - let browser = new BrowserObj(win, this); - let winId = win.QueryInterface(Ci.nsIInterfaceRequestor). - getInterface(Ci.nsIDOMWindowUtils).outerWindowID; - winId = winId + ((appName == "B2G") ? '-b2g' : ''); - this.browsers[winId] = browser; - this.curBrowser = this.browsers[winId]; - if (this.curBrowser.elementManager.seenItems[winId] == undefined) { - //add this to seenItems so we can guarantee the user will get winId as this window's id - this.curBrowser.elementManager.seenItems[winId] = Cu.getWeakReference(win); - } - }, - - /** - * Start a new session in a new browser. - * - * If newSession is true, we will switch focus to the start frame - * when it registers. - * - * @param nsIDOMWindow win - * Window whose browser we need to access - * @param boolean newSession - * True if this is the first time we're talking to this browser - */ - startBrowser: function MDA_startBrowser(win, newSession) { - this.mainFrame = win; - this.curFrame = null; - this.addBrowser(win); - this.curBrowser.newSession = newSession; - this.curBrowser.startSession(newSession, win, this.whenBrowserStarted.bind(this)); - }, - - /** - * Callback invoked after a new session has been started in a browser. - * Loads the Marionette frame script into the browser if needed. - * - * @param nsIDOMWindow win - * Window whose browser we need to access - * @param boolean newSession - * True if this is the first time we're talking to this browser - */ - whenBrowserStarted: function MDA_whenBrowserStarted(win, newSession) { - utils.window = win; - - try { - let mm = win.window.messageManager; - if (!newSession) { - // Loading the frame script corresponds to a situation we need to - // return to the server. If the messageManager is a message broadcaster - // with no children, we don't have a hope of coming back from this call, - // so send the ack here. Otherwise, make a note of how many child scripts - // will be loaded so we known when it's safe to return. - if (mm.childCount === 0) { - this.sendOk(this.command_id); - } else { - this.curBrowser.frameRegsPending = mm.childCount; - } - } - - if (!Services.prefs.getBoolPref("marionette.contentListener") || !newSession) { - mm.loadFrameScript(FRAME_SCRIPT, true, true); - Services.prefs.setBoolPref("marionette.contentListener", true); - } - } - catch (e) { - //there may not always be a content process - logger.info("could not load listener into content for page: " + win.location.href); - } - }, - - /** - * Recursively get all labeled text - * - * @param nsIDOMElement el - * The parent element - * @param array lines - * Array that holds the text lines - */ - getVisibleText: function MDA_getVisibleText(el, lines) { - let nodeName = el.nodeName; - try { - if (utils.isElementDisplayed(el)) { - if (el.value) { - lines.push(el.value); - } - for (var child in el.childNodes) { - this.getVisibleText(el.childNodes[child], lines); - }; - } - } - catch (e) { - if (nodeName == "#text") { - lines.push(el.textContent); - } - } - }, - - getCommandId: function MDA_getCommandId() { - return this.uuidGen.generateUUID().toString(); - }, - - /** - * Given a file name, this will delete the file from the temp directory if it exists - */ - deleteFile: function(filename) { - let file = FileUtils.getFile('TmpD', [filename.toString()]); - if (file.exists()) { - file.remove(true); - } - }, - - /** - * Marionette API: - * - * All methods implementing a command from the client should create a - * command_id, and then use this command_id in all messages exchanged with - * the frame scripts and with responses sent to the client. This prevents - * commands and responses from getting out-of-sync, which can happen in - * the case of execute_async calls that timeout and then later send a - * response, and other situations. See bug 779011. See setScriptTimeout() - * for a basic example. - */ - - /** - * Create a new session. This creates a new BrowserObj. - * - * This will send a hash map of supported capabilities to the client - * as part of the Marionette:register IPC command in the - * receiveMessage callback when a new browser is created. - */ - newSession: function MDA_newSession(aRequest) { - logger.info("The newSession request is " + JSON.stringify(aRequest)) - this.command_id = this.getCommandId(); - this.newSessionCommandId = this.command_id; - - // SpecialPowers requires insecure automation-only features that we put behind a pref - let security_pref_value = false; - try { - security_pref_value = Services.prefs.getBoolPref(SECURITY_PREF); - } catch(e) {} - if (!security_pref_value) { - this.enabled_security_pref = true; - Services.prefs.setBoolPref(SECURITY_PREF, true); - } - - if (!specialpowers.hasOwnProperty('specialPowersObserver')) { - loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserver.js", - specialpowers); - specialpowers.specialPowersObserver = new specialpowers.SpecialPowersObserver(); - specialpowers.specialPowersObserver.init(); - specialpowers.specialPowersObserver._loadFrameScript(); - } - - this.scriptTimeout = 10000; - if (aRequest && aRequest.parameters) { - this.sessionId = aRequest.parameters.sessionId || aRequest.parameters.session_id || null; - logger.info("Session Id is set to: " + this.sessionId); - try { - this.setSessionCapabilities(aRequest.parameters.capabilities); - } catch (e) { - // 71 error is "session not created" - this.sendError(e.message + " " + JSON.stringify(e.errors), 71, null, - this.command_id); - return; - } - } - - if (appName == "Firefox") { - this._dialogWindowRef = null; - let modalHandler = this.handleDialogLoad.bind(this); - this.observing = { - "tabmodal-dialog-loaded": modalHandler, - "common-dialog-loaded": modalHandler - } - for (let topic in this.observing) { - Services.obs.addObserver(this.observing[topic], topic, false); - } - } - - function waitForWindow() { - let win = this.getCurrentWindow(); - if (!win) { - // If the window isn't even created, just poll wait for it - let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - checkTimer.initWithCallback(waitForWindow.bind(this), 100, - Ci.nsITimer.TYPE_ONE_SHOT); - } - else if (win.document.readyState != "complete") { - // Otherwise, wait for it to be fully loaded before proceeding - let listener = (evt) => { - // ensure that we proceed, on the top level document load event - // (not an iframe one...) - if (evt.target != win.document) { - return; - } - win.removeEventListener("load", listener); - waitForWindow.call(this); - }; - win.addEventListener("load", listener, true); - } - else { - let clickToStart; - try { - clickToStart = Services.prefs.getBoolPref('marionette.debugging.clicktostart'); - } catch (e) { } - if (clickToStart && (appName != "B2G")) { - let pService = Cc["@mozilla.org/embedcomp/prompt-service;1"] - .getService(Ci.nsIPromptService); - pService.alert(win, "", "Click to start execution of marionette tests"); - } - this.startBrowser(win, true); - } - } - - function runSessionStart() { - if (!Services.prefs.getBoolPref("marionette.contentListener")) { - waitForWindow.call(this); - } - else if ((appName != "Firefox") && (this.curBrowser === null)) { - // If there is a content listener, then we just wake it up - this.addBrowser(this.getCurrentWindow()); - this.curBrowser.startSession(false, this.getCurrentWindow(), - this.whenBrowserStarted); - this.messageManager.broadcastAsyncMessage("Marionette:restart", {}); - } - else { - this.sendError("Session already running", 500, null, - this.command_id); - } - this.switchToGlobalMessageManager(); - } - - if (!delayedBrowserStarted && (appName != "B2G")) { - let self = this; - Services.obs.addObserver(function onStart () { - Services.obs.removeObserver(onStart, BROWSER_STARTUP_FINISHED); - runSessionStart.call(self); - }, BROWSER_STARTUP_FINISHED, false); - } else { - runSessionStart.call(this); - } - }, - - /** - * Send the current session's capabilities to the client. - * - * Capabilities informs the client of which WebDriver features are - * supported by Firefox and Marionette. They are immutable for the - * length of the session. - * - * The return value is an immutable map of string keys - * ("capabilities") to values, which may be of types boolean, - * numerical or string. - */ - getSessionCapabilities: function MDA_getSessionCapabilities() { - this.command_id = this.getCommandId(); - - if (!this.sessionId) { - this.sessionId = this.uuidGen.generateUUID().toString(); - } - - // eideticker (bug 965297) and mochitest (bug 965304) - // compatibility. They only check for the presence of this - // property and should so not be in caps if not on a B2G device. - if (appName == "B2G") - this.sessionCapabilities.b2g = true; - - this.sendResponse(this.sessionCapabilities, this.command_id); - }, - - /** - * Update the sessionCapabilities object with the keys that have been - * passed in when a new session is created. - * - * This part of the WebDriver spec is currently in flux, see - * http://lists.w3.org/Archives/Public/public-browser-tools-testing/2014OctDec/0000.html - * - * This is not a public API, only available when a new session is - * created. - * - * @param Object newCaps key/value dictionary to overwrite - * session's current capabilities - */ - setSessionCapabilities: function(newCaps) { - const copy = (from, to={}) => { - let errors = {}; - for (let key in from) { - if (key === "desiredCapabilities"){ - // Keeping desired capabilities separate for now so that we can keep - // backwards compatibility - to = copy(from[key], to); - } else if (key === "requiredCapabilities") { - for (let caps in from[key]) { - if (from[key][caps] !== this.sessionCapabilities[caps]) { - errors[caps] = from[key][caps] + " does not equal " + this.sessionCapabilities[caps] - } - } - } - to[key] = from[key]; - } - if (Object.keys(errors).length === 0){ - return to; - } - else { - throw { "message": "Not all requiredCapabilities could be met", - "errors": errors} - } - }; - - // Clone, overwrite, and set. - let caps = copy(this.sessionCapabilities); - caps = copy(newCaps, caps); - this.sessionCapabilities = caps; - }, - - /** - * Log message. Accepts user defined log-level. - * - * @param object aRequest - * 'value' member holds log message - * 'level' member hold log level - */ - log: function MDA_log(aRequest) { - this.command_id = this.getCommandId(); - this.marionetteLog.log(aRequest.parameters.value, aRequest.parameters.level); - this.sendOk(this.command_id); - }, - - /** - * Return all logged messages. - */ - getLogs: function MDA_getLogs() { - this.command_id = this.getCommandId(); - this.sendResponse(this.marionetteLog.getLogs(), this.command_id); - }, - - /** - * Sets the context of the subsequent commands to be either 'chrome' or 'content' - * - * @param object aRequest - * 'value' member holds the name of the context to be switched to - */ - setContext: function MDA_setContext(aRequest) { - this.command_id = this.getCommandId(); - let context = aRequest.parameters.value; - if (context != "content" && context != "chrome") { - this.sendError("invalid context", 500, null, this.command_id); - } - else { - this.context = context; - this.sendOk(this.command_id); - } - }, - - /** - * Gets the context of the server, either 'chrome' or 'content'. - */ - getContext: function MDA_getContext() { - this.command_id = this.getCommandId(); - this.sendResponse(this.context, this.command_id); - }, - - /** - * Returns a chrome sandbox that can be used by the execute_foo functions. - * - * @param nsIDOMWindow aWindow - * Window in which we will execute code - * @param Marionette marionette - * Marionette test instance - * @param object args - * Client given args - * @return Sandbox - * Returns the sandbox - */ - createExecuteSandbox: function MDA_createExecuteSandbox(aWindow, marionette, specialPowers, command_id) { - let _chromeSandbox = new Cu.Sandbox(aWindow, - { sandboxPrototype: aWindow, wantXrays: false, sandboxName: ''}); - _chromeSandbox.global = _chromeSandbox; - _chromeSandbox.testUtils = utils; - - marionette.exports.forEach(function(fn) { - try { - _chromeSandbox[fn] = marionette[fn].bind(marionette); - } - catch(e) { - _chromeSandbox[fn] = marionette[fn]; - } - }); - - _chromeSandbox.isSystemMessageListenerReady = - function() { return systemMessageListenerReady; } - - if (specialPowers == true) { - loader.loadSubScript("chrome://specialpowers/content/specialpowersAPI.js", - _chromeSandbox); - loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserverAPI.js", - _chromeSandbox); - loader.loadSubScript("chrome://specialpowers/content/ChromePowers.js", - _chromeSandbox); - } - - return _chromeSandbox; - }, - - /** - * Apply arguments sent from the client to the current (possibly reused) execution - * sandbox. - */ - applyArgumentsToSandbox: function MDA_applyArgumentsToSandbox(win, sandbox, args, command_id) { - try { - sandbox.__marionetteParams = this.curBrowser.elementManager.convertWrappedArguments(args, win); - } - catch(e) { - this.sendError(e.message, e.code, e.stack, command_id); - } - sandbox.__namedArgs = this.curBrowser.elementManager.applyNamedArgs(args); - }, - - /** - * Executes a script in the given sandbox. - * - * @param Sandbox sandbox - * Sandbox in which the script will run - * @param string script - * The script to run - * @param boolean directInject - * If true, then the script will be run as is, - * and not as a function body (as you would - * do using the WebDriver spec) - * @param boolean async - * True if the script is asynchronous - */ - executeScriptInSandbox: function MDA_executeScriptInSandbox(sandbox, script, - directInject, async, command_id, timeout) { - - if (directInject && async && - (timeout == null || timeout == 0)) { - this.sendError("Please set a timeout", 21, null, command_id); - return; - } - - if (this.importedScripts.exists()) { - let stream = Cc["@mozilla.org/network/file-input-stream;1"]. - createInstance(Ci.nsIFileInputStream); - stream.init(this.importedScripts, -1, 0, 0); - let data = NetUtil.readInputStreamToString(stream, stream.available()); - stream.close(); - script = data + script; - } - - let res = Cu.evalInSandbox(script, sandbox, "1.8", "dummy file", 0); - - if (directInject && !async && - (res == undefined || res.passed == undefined)) { - this.sendError("finish() not called", 500, null, command_id); - return; - } - - if (!async) { - this.sendResponse(this.curBrowser.elementManager.wrapValue(res), - command_id); - } - }, - - /** - * Execute the given script either as a function body (executeScript) - * or directly (for 'mochitest' like JS Marionette tests) - * - * @param object aRequest - * 'script' member is the script to run - * 'args' member holds the arguments to the script - * @param boolean directInject - * if true, it will be run directly and not as a - * function body - */ - execute: function MDA_execute(aRequest, directInject) { - let inactivityTimeout = aRequest.parameters.inactivityTimeout; - let timeout = aRequest.parameters.scriptTimeout ? aRequest.parameters.scriptTimeout : this.scriptTimeout; - let command_id = this.command_id = this.getCommandId(); - let script; - let newSandbox = aRequest.parameters.newSandbox; - if (newSandbox == undefined) { - //if client does not send a value in newSandbox, - //then they expect the same behaviour as webdriver - newSandbox = true; - } - if (this.context == "content") { - this.sendAsync("executeScript", - { - script: aRequest.parameters.script, - args: aRequest.parameters.args, - newSandbox: newSandbox, - timeout: timeout, - specialPowers: aRequest.parameters.specialPowers, - filename: aRequest.parameters.filename, - line: aRequest.parameters.line - }, - command_id); - return; - } - - // handle the inactivity timeout - let that = this; - if (inactivityTimeout) { - let inactivityTimeoutHandler = function(message, status) { - let error_msg = {message: value, status: status}; - that.sendToClient({from: that.actorID, error: error_msg}, - marionette.command_id); - }; - let setTimer = function() { - that.inactivityTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - if (that.inactivityTimer != null) { - that.inactivityTimer.initWithCallback(function() { - inactivityTimeoutHandler("timed out due to inactivity", 28); - }, inactivityTimeout, Ci.nsITimer.TYPE_ONE_SHOT); - } - } - setTimer(); - this.heartbeatCallback = function resetInactivityTimer() { - that.inactivityTimer.cancel(); - setTimer(); - } - } - - - let curWindow = this.getCurrentWindow(); - if (!this.sandbox || newSandbox) { - let marionette = new Marionette(this, curWindow, "chrome", - this.marionetteLog, - timeout, this.heartbeatCallback, this.testName); - this.sandbox = this.createExecuteSandbox(curWindow, - marionette, - aRequest.parameters.specialPowers, - command_id); - if (!this.sandbox) - return; - } - this.applyArgumentsToSandbox(curWindow, this.sandbox, aRequest.parameters.args, - command_id) - - try { - this.sandbox.finish = function chromeSandbox_finish() { - if (that.inactivityTimer != null) { - that.inactivityTimer.cancel(); - } - return that.sandbox.generate_results(); - }; - - if (directInject) { - script = aRequest.parameters.script; - } - else { - script = "let func = function() {" + - aRequest.parameters.script + - "};" + - "func.apply(null, __marionetteParams);"; - } - this.executeScriptInSandbox(this.sandbox, script, directInject, - false, command_id, timeout); - } - catch (e) { - let error = createStackMessage(e, - "execute_script", - aRequest.parameters.filename, - aRequest.parameters.line, - script); - this.sendError(error[0], 17, error[1], command_id); - } - }, - - /** - * Set the timeout for asynchronous script execution - * - * @param object aRequest - * 'ms' member is time in milliseconds to set timeout - */ - setScriptTimeout: function MDA_setScriptTimeout(aRequest) { - this.command_id = this.getCommandId(); - let timeout = parseInt(aRequest.parameters.ms); - if(isNaN(timeout)){ - this.sendError("Not a Number", 500, null, this.command_id); - } - else { - this.scriptTimeout = timeout; - this.sendOk(this.command_id); - } - }, - - /** - * execute pure JS script. Used to execute 'mochitest'-style Marionette tests. - * - * @param object aRequest - * 'script' member holds the script to execute - * 'args' member holds the arguments to the script - * 'timeout' member will be used as the script timeout if it is given - */ - executeJSScript: function MDA_executeJSScript(aRequest) { - let timeout = aRequest.parameters.scriptTimeout ? aRequest.parameters.scriptTimeout : this.scriptTimeout; - let command_id = this.command_id = this.getCommandId(); - - //all pure JS scripts will need to call Marionette.finish() to complete the test. - if (aRequest.newSandbox == undefined) { - //if client does not send a value in newSandbox, - //then they expect the same behaviour as webdriver - aRequest.newSandbox = true; - } - if (this.context == "chrome") { - if (aRequest.parameters.async) { - this.executeWithCallback(aRequest, aRequest.parameters.async); - } - else { - this.execute(aRequest, true); - } - } - else { - this.sendAsync("executeJSScript", - { - script: aRequest.parameters.script, - args: aRequest.parameters.args, - newSandbox: aRequest.parameters.newSandbox, - async: aRequest.parameters.async, - timeout: timeout, - inactivityTimeout: aRequest.parameters.inactivityTimeout, - specialPowers: aRequest.parameters.specialPowers, - filename: aRequest.parameters.filename, - line: aRequest.parameters.line, - }, - command_id); - } - }, - - /** - * This function is used by executeAsync and executeJSScript to execute a script - * in a sandbox. - * - * For executeJSScript, it will return a message only when the finish() method is called. - * For executeAsync, it will return a response when marionetteScriptFinished/arguments[arguments.length-1] - * method is called, or if it times out. - * - * @param object aRequest - * 'script' member holds the script to execute - * 'args' member holds the arguments for the script - * @param boolean directInject - * if true, it will be run directly and not as a - * function body - */ - executeWithCallback: function MDA_executeWithCallback(aRequest, directInject) { - let inactivityTimeout = aRequest.parameters.inactivityTimeout; - let timeout = aRequest.parameters.scriptTimeout ? aRequest.parameters.scriptTimeout : this.scriptTimeout; - let command_id = this.command_id = this.getCommandId(); - let script; - let newSandbox = aRequest.parameters.newSandbox; - if (newSandbox == undefined) { - //if client does not send a value in newSandbox, - //then they expect the same behaviour as webdriver - newSandbox = true; - } - - if (this.context == "content") { - this.sendAsync("executeAsyncScript", - { - script: aRequest.parameters.script, - args: aRequest.parameters.args, - id: this.command_id, - newSandbox: newSandbox, - timeout: timeout, - inactivityTimeout: inactivityTimeout, - specialPowers: aRequest.parameters.specialPowers, - filename: aRequest.parameters.filename, - line: aRequest.parameters.line - }, - command_id); - return; - } - - // handle the inactivity timeout - let that = this; - if (inactivityTimeout) { - this.inactivityTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - if (this.inactivityTimer != null) { - this.inactivityTimer.initWithCallback(function() { - chromeAsyncReturnFunc("timed out due to inactivity", 28); - }, inactivityTimeout, Ci.nsITimer.TYPE_ONE_SHOT); - } - this.heartbeatCallback = function resetInactivityTimer() { - that.inactivityTimer.cancel(); - that.inactivityTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - if (that.inactivityTimer != null) { - that.inactivityTimer.initWithCallback(function() { - chromeAsyncReturnFunc("timed out due to inactivity", 28); - }, inactivityTimeout, Ci.nsITimer.TYPE_ONE_SHOT); - } - } - } - - let curWindow = this.getCurrentWindow(); - let original_onerror = curWindow.onerror; - that.timeout = timeout; - - function chromeAsyncReturnFunc(value, status, stacktrace) { - if (that._emu_cbs && Object.keys(that._emu_cbs).length) { - value = "Emulator callback still pending when finish() called"; - status = 500; - that._emu_cbs = null; - } - - if (value == undefined) - value = null; - - if (command_id == that.command_id) { - if (that.timer != null) { - that.timer.cancel(); - that.timer = null; - } - - curWindow.onerror = original_onerror; - - if (status == 0 || status == undefined) { - that.sendToClient({from: that.actorID, value: that.curBrowser.elementManager.wrapValue(value), status: status}, - that.command_id); - } - else { - let error_msg = {message: value, status: status, stacktrace: stacktrace}; - that.sendToClient({from: that.actorID, error: error_msg}, - that.command_id); - } - } - - if (that.inactivityTimer != null) { - that.inactivityTimer.cancel(); - } - } - - // NB: curWindow.onerror is not hooked by default due to the inability to - // differentiate content exceptions from chrome exceptions. See bug - // 1128760 for more details. A 'debug_script' flag can be set to - // reenable onerror hooking to help debug test scripts. - if (aRequest.parameters.debug_script) { - curWindow.onerror = function (errorMsg, url, lineNumber) { - chromeAsyncReturnFunc(errorMsg + " at: " + url + " line: " + lineNumber, 17); - return true; - }; - } - - function chromeAsyncFinish() { - chromeAsyncReturnFunc(that.sandbox.generate_results(), 0); - } - - if (!this.sandbox || newSandbox) { - let marionette = new Marionette(this, curWindow, "chrome", - this.marionetteLog, - timeout, this.heartbeatCallback, this.testName); - this.sandbox = this.createExecuteSandbox(curWindow, - marionette, - aRequest.parameters.specialPowers, - command_id); - if (!this.sandbox) - return; - } - this.applyArgumentsToSandbox(curWindow, this.sandbox, aRequest.parameters.args, - command_id) - - try { - - this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - if (this.timer != null) { - this.timer.initWithCallback(function() { - chromeAsyncReturnFunc("timed out", 28); - }, that.timeout, Ci.nsITimer.TYPE_ONE_SHOT); - } - - this.sandbox.returnFunc = chromeAsyncReturnFunc; - this.sandbox.finish = chromeAsyncFinish; - - if (directInject) { - script = aRequest.parameters.script; - } - else { - script = '__marionetteParams.push(returnFunc);' - + 'let marionetteScriptFinished = returnFunc;' - + 'let __marionetteFunc = function() {' + aRequest.parameters.script + '};' - + '__marionetteFunc.apply(null, __marionetteParams);'; - } - - this.executeScriptInSandbox(this.sandbox, script, directInject, - true, command_id, timeout); - } catch (e) { - let error = createStackMessage(e, - "execute_async_script", - aRequest.parameters.filename, - aRequest.parameters.line, - script); - chromeAsyncReturnFunc(error[0], 17, error[1]); - } - }, - - /** - * Navigate to to given URL. - * - * This will follow redirects issued by the server. When the method - * returns is based on the page load strategy that the user has - * selected. - * - * Documents that contain a META tag with the "http-equiv" attribute - * set to "refresh" will return if the timeout is greater than 1 - * second and the other criteria for determining whether a page is - * loaded are met. When the refresh period is 1 second or less and - * the page load strategy is "normal" or "conservative", it will - * wait for the page to complete loading before returning. - * - * If any modal dialog box, such as those opened on - * window.onbeforeunload or window.alert, is opened at any point in - * the page load, it will return immediately. - * - * If a 401 response is seen by the browser, it will return - * immediately. That is, if BASIC, DIGEST, NTLM or similar - * authentication is required, the page load is assumed to be - * complete. This does not include FORM-based authentication. - * - * @param object aRequest where url property holds the - * URL to navigate to - */ - get: function MDA_get(aRequest) { - let command_id = this.command_id = this.getCommandId(); - - if (this.context != "chrome") { - // If a remoteness update interrupts our page load, this will never return - // We need to re-issue this request to correctly poll for readyState and - // send errors. - this.curBrowser.pendingCommands.push(() => { - aRequest.parameters.command_id = command_id; - this.messageManager.broadcastAsyncMessage( - "Marionette:pollForReadyState" + this.curBrowser.curFrameId, - aRequest.parameters); - }); - aRequest.command_id = command_id; - aRequest.parameters.pageTimeout = this.pageTimeout; - this.sendAsync("get", aRequest.parameters, command_id); - return; - } - - // At least on desktop, navigating in chrome scope does not - // correspond to something a user can do, and leaves marionette - // and the browser in an unusable state. Return a generic error insted. - // TODO: Error codes need to be refined as a part of bug 1100545 and - // bug 945729. - if (appName == "Firefox") { - this.sendError("Cannot navigate in chrome context", 13, null, command_id); - return; - } - - this.getCurrentWindow().location.href = aRequest.parameters.url; - let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - let start = new Date().getTime(); - let end = null; - - function checkLoad() { - end = new Date().getTime(); - let elapse = end - start; - if (this.pageTimeout == null || elapse <= this.pageTimeout){ - if (curWindow.document.readyState == "complete") { - sendOk(command_id); - return; - } - else{ - checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT); - } - } - else{ - sendError("Error loading page", 13, null, command_id); - return; - } - } - checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT); - }, - - /** - * Get a string representing the current URL. - * - * On Desktop this returns a string representation of the URL of the - * current top level browsing context. This is equivalent to - * document.location.href. - * - * When in the context of the chrome, this returns the canonical URL - * of the current resource. - */ - getCurrentUrl: function MDA_getCurrentUrl() { - let isB2G = appName == "B2G"; - this.command_id = this.getCommandId(); - if (this.context === "chrome") { - this.sendResponse(this.getCurrentWindow().location.href, this.command_id); - } - else { - this.sendAsync("getCurrentUrl", {isB2G: isB2G}, this.command_id); - } - }, - - /** - * Gets the current title of the window - */ - getTitle: function MDA_getTitle() { - this.command_id = this.getCommandId(); - if (this.context == "chrome"){ - var curWindow = this.getCurrentWindow(); - var title = curWindow.document.documentElement.getAttribute('title'); - this.sendResponse(title, this.command_id); - } - else { - this.sendAsync("getTitle", {}, this.command_id); - } - }, - - /** - * Gets the current type of the window - */ - getWindowType: function MDA_getWindowType() { - this.command_id = this.getCommandId(); - var curWindow = this.getCurrentWindow(); - var type = curWindow.document.documentElement.getAttribute('windowtype'); - this.sendResponse(type, this.command_id); - }, - - /** - * Gets the page source of the content document - */ - getPageSource: function MDA_getPageSource(){ - this.command_id = this.getCommandId(); - if (this.context == "chrome"){ - let curWindow = this.getCurrentWindow(); - let XMLSerializer = curWindow.XMLSerializer; - let pageSource = new XMLSerializer().serializeToString(curWindow.document); - this.sendResponse(pageSource, this.command_id); - } - else { - this.sendAsync("getPageSource", {}, this.command_id); - } - }, - - /** - * Go back in history - */ - goBack: function MDA_goBack() { - this.command_id = this.getCommandId(); - this.sendAsync("goBack", {}, this.command_id); - }, - - /** - * Go forward in history - */ - goForward: function MDA_goForward() { - this.command_id = this.getCommandId(); - this.sendAsync("goForward", {}, this.command_id); - }, - - /** - * Refresh the page - */ - refresh: function MDA_refresh() { - this.command_id = this.getCommandId(); - this.sendAsync("refresh", {}, this.command_id); - }, - - /** - * Get the current window's handle. On desktop this typically corresponds to - * the currently selected tab. - * - * Return an opaque server-assigned identifier to this window that - * uniquely identifies it within this Marionette instance. This can - * be used to switch to this window at a later point. - * - * @return unique window handle (string) - */ - getWindowHandle: function MDA_getWindowHandle() { - this.command_id = this.getCommandId(); - // curFrameId always holds the current tab. - if (this.curBrowser.curFrameId && appName != 'B2G') { - this.sendResponse(this.curBrowser.curFrameId, this.command_id); - return; - } - for (let i in this.browsers) { - if (this.curBrowser == this.browsers[i]) { - this.sendResponse(i, this.command_id); - return; - } - } - }, - - /** - * Forces an update for the given browser's id. - */ - updateIdForBrowser: function (browser, newId) { - this._browserIds.set(browser.permanentKey, newId); - }, - - /** - * Retrieves a listener id for the given xul browser element. In case - * the browser is not known, an attempt is made to retrieve the id from - * a CPOW, and null is returned if this fails. - */ - getIdForBrowser: function (browser) { - if (browser === null) { - return null; - } - let permKey = browser.permanentKey; - if (this._browserIds.has(permKey)) { - return this._browserIds.get(permKey); - } - - let winId = browser.outerWindowID; - if (winId) { - winId += ""; - this._browserIds.set(permKey, winId); - return winId; - } - return null; - }, - - /** - * Get a list of top-level browsing contexts. On desktop this typically - * corresponds to the set of open tabs. - * - * Each window handle is assigned by the server and is guaranteed unique, - * however the return array does not have a specified ordering. - * - * @return array of unique window handles as strings - */ - getWindowHandles: function MDA_getWindowHandles() { - this.command_id = this.getCommandId(); - let res = []; - let winEn = this.getWinEnumerator(); - while (winEn.hasMoreElements()) { - let win = winEn.getNext(); - if (win.gBrowser && appName != 'B2G') { - let tabbrowser = win.gBrowser; - for (let i = 0; i < tabbrowser.browsers.length; ++i) { - let winId = this.getIdForBrowser(tabbrowser.getBrowserAtIndex(i)); - if (winId !== null) { - res.push(winId); - } - } - } else { - // XUL Windows, at least, do not have gBrowser. - let winId = win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils) - .outerWindowID; - winId += (appName == "B2G") ? "-b2g" : ""; - res.push(winId); - } - } - this.sendResponse(res, this.command_id); - }, - - /** - * Get the current window's handle. This corresponds to a window that - * may itself contain tabs. - * - * Return an opaque server-assigned identifier to this window that - * uniquely identifies it within this Marionette instance. This can - * be used to switch to this window at a later point. - * - * @return unique window handle (string) - */ - getChromeWindowHandle: function MDA_getChromeWindowHandle() { - this.command_id = this.getCommandId(); - for (let i in this.browsers) { - if (this.curBrowser == this.browsers[i]) { - this.sendResponse(i, this.command_id); - return; - } - } - }, - - /** - * Returns identifiers for each open chrome window for tests interested in - * managing a set of chrome windows and tabs separately. - * - * @return array of unique window handles as strings - */ - getChromeWindowHandles: function MDA_getChromeWindowHandles() { - this.command_id = this.getCommandId(); - let res = []; - let winEn = this.getWinEnumerator(); - while (winEn.hasMoreElements()) { - let foundWin = winEn.getNext(); - let winId = foundWin.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils) - .outerWindowID; - winId = winId + ((appName == "B2G") ? "-b2g" : ""); - res.push(winId); - } - this.sendResponse(res, this.command_id); - }, - - /** - * Get the current window position. - */ - getWindowPosition: function MDA_getWindowPosition() { - this.command_id = this.getCommandId(); - let curWindow = this.getCurrentWindow(); - this.sendResponse({ x: curWindow.screenX, y: curWindow.screenY}, this.command_id); - }, - - /** - * Set the window position of the browser on the OS Window Manager - * - * @param object aRequest - * 'x': the x co-ordinate of the top/left of the window that - * it will be moved to - * 'y': the y co-ordinate of the top/left of the window that - * it will be moved to - */ - setWindowPosition: function MDA_setWindowPosition(aRequest) { - let command_id = this.command_id = this.getCommandId(); - if (appName !== "Firefox") { - this.sendError("Unable to set the window position on mobile", 61, null, - command_id); - - } - else { - let x = parseInt(aRequest.parameters.x);; - let y = parseInt(aRequest.parameters.y); - - if (isNaN(x) || isNaN(y)) { - this.sendError("x and y arguments should be integers", 13, null, command_id); - return; - } - let curWindow = this.getCurrentWindow(); - curWindow.moveTo(x, y); - this.sendOk(command_id); - } - }, - - /** - * Switch to a window based on name or server-assigned id. - * Searches based on name, then id. - * - * @param object aRequest - * 'name' member holds the name or id of the window to switch to - */ - switchToWindow: function MDA_switchToWindow(aRequest) { - let command_id = this.command_id = this.getCommandId(); - - let checkWindow = function (win, outerId, contentWindowId, ind) { - if (aRequest.parameters.name == win.name || - aRequest.parameters.name == contentWindowId || - aRequest.parameters.name == outerId) { - // As in content, switching to a new window invalidates a sandbox for reuse. - this.sandbox = null; - if (this.browsers[outerId] === undefined) { - //enable Marionette in that browser window - this.startBrowser(win, false); - } else { - utils.window = win; - this.curBrowser = this.browsers[outerId]; - if (contentWindowId) { - // The updated id corresponds to switching to a new tab. - this.curBrowser.switchToTab(ind); - } - this.sendOk(command_id); - } - return true; - } - return false; - } - - let winEn = this.getWinEnumerator(); - while (winEn.hasMoreElements()) { - let win = winEn.getNext(); - let outerId = win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils) - .outerWindowID; - outerId += (appName == "B2G") ? "-b2g" : ""; - if (win.gBrowser && appName != 'B2G') { - let tabbrowser = win.gBrowser; - for (let i = 0; i < tabbrowser.browsers.length; ++i) { - let browser = tabbrowser.getBrowserAtIndex(i); - let contentWindowId = this.getIdForBrowser(browser); - if (contentWindowId !== null && - checkWindow.call(this, win, outerId, contentWindowId, i)) { - return; - } - } - } else { - // A chrome window is always a valid target for switching in the case - // a handle was obtained by getChromeWindowHandles. - if (checkWindow.call(this, win, outerId)) { - return; - } - } - } - this.sendError("Unable to locate window " + aRequest.parameters.name, 23, null, - command_id); - }, - - getActiveFrame: function MDA_getActiveFrame() { - this.command_id = this.getCommandId(); - - if (this.context == "chrome") { - if (this.curFrame) { - let frameUid = this.curBrowser.elementManager.addToKnownElements(this.curFrame.frameElement); - this.sendResponse(frameUid, this.command_id); - } else { - // no current frame, we're at toplevel - this.sendResponse(null, this.command_id); - } - } else { - // not chrome - this.sendResponse(this.currentFrameElement, this.command_id); - } - }, - - /** - * Switch to a given frame within the current window - * - * @param object aRequest - * 'element' is the element to switch to - * 'id' if element is not set, then this - * holds either the id, name or index - * of the frame to switch to - */ - switchToFrame: function MDA_switchToFrame(aRequest) { - let command_id = this.command_id = this.getCommandId(); - let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - let curWindow = this.getCurrentWindow(); - let checkLoad = function() { - let errorRegex = /about:.+(error)|(blocked)\?/; - let curWindow = this.getCurrentWindow(); - if (curWindow.document.readyState == "complete") { - this.sendOk(command_id); - return; - } - else if (curWindow.document.readyState == "interactive" && errorRegex.exec(curWindow.document.baseURI)) { - this.sendError("Error loading page", 13, null, command_id); - return; - } - - checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT); - } - if (this.context == "chrome") { - let foundFrame = null; - if ((aRequest.parameters.id == null) && (aRequest.parameters.element == null)) { - this.curFrame = null; - if (aRequest.parameters.focus) { - this.mainFrame.focus(); - } - checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT); - return; - } - if (aRequest.parameters.element != undefined) { - if (this.curBrowser.elementManager.seenItems[aRequest.parameters.element]) { - let wantedFrame = this.curBrowser.elementManager.getKnownElement(aRequest.parameters.element, curWindow); //HTMLIFrameElement - // Deal with an embedded xul:browser case - if (wantedFrame.tagName == "xul:browser" || wantedFrame.tagName == "browser") { - curWindow = wantedFrame.contentWindow; - this.curFrame = curWindow; - if (aRequest.parameters.focus) { - this.curFrame.focus(); - } - checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT); - return; - } - // else, assume iframe - let frames = curWindow.document.getElementsByTagName("iframe"); - let numFrames = frames.length; - for (let i = 0; i < numFrames; i++) { - if (XPCNativeWrapper(frames[i]) == XPCNativeWrapper(wantedFrame)) { - curWindow = frames[i].contentWindow; - this.curFrame = curWindow; - if (aRequest.parameters.focus) { - this.curFrame.focus(); - } - checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT); - return; - } - } - } - } - switch(typeof(aRequest.parameters.id)) { - case "string" : - let foundById = null; - let frames = curWindow.document.getElementsByTagName("iframe"); - let numFrames = frames.length; - for (let i = 0; i < numFrames; i++) { - //give precedence to name - let frame = frames[i]; - if (frame.getAttribute("name") == aRequest.parameters.id) { - foundFrame = i; - curWindow = frame.contentWindow; - break; - } else if ((foundById == null) && (frame.id == aRequest.parameters.id)) { - foundById = i; - } - } - if ((foundFrame == null) && (foundById != null)) { - foundFrame = foundById; - curWindow = frames[foundById].contentWindow; - } - break; - case "number": - if (curWindow.frames[aRequest.parameters.id] != undefined) { - foundFrame = aRequest.parameters.id; - curWindow = curWindow.frames[foundFrame].frameElement.contentWindow; - } - break; - } - if (foundFrame != null) { - this.curFrame = curWindow; - if (aRequest.parameters.focus) { - this.curFrame.focus(); - } - checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT); - } else { - this.sendError("Unable to locate frame: " + aRequest.parameters.id, 8, null, - command_id); - } - } - else { - if ((!aRequest.parameters.id) && (!aRequest.parameters.element) && - (this.curBrowser.frameManager.currentRemoteFrame !== null)) { - // We're currently using a ChromeMessageSender for a remote frame, so this - // request indicates we need to switch back to the top-level (parent) frame. - // We'll first switch to the parent's (global) ChromeMessageBroadcaster, so - // we send the message to the right listener. - this.switchToGlobalMessageManager(); - } - aRequest.command_id = command_id; - this.sendAsync("switchToFrame", aRequest.parameters, command_id); - } - }, - - /** - * Set timeout for searching for elements - * - * @param object aRequest - * 'ms' holds the search timeout in milliseconds - */ - setSearchTimeout: function MDA_setSearchTimeout(aRequest) { - this.command_id = this.getCommandId(); - let timeout = parseInt(aRequest.parameters.ms); - if (isNaN(timeout)) { - this.sendError("Not a Number", 500, null, this.command_id); - } - else { - this.searchTimeout = timeout; - this.sendOk(this.command_id); - } - }, - - /** - * Set timeout for page loading, searching and scripts - * - * @param object aRequest - * 'type' hold the type of timeout - * 'ms' holds the timeout in milliseconds - */ - timeouts: function MDA_timeouts(aRequest){ - /*setTimeout*/ - this.command_id = this.getCommandId(); - let timeout_type = aRequest.parameters.type; - let timeout = parseInt(aRequest.parameters.ms); - if (isNaN(timeout)) { - this.sendError("Not a Number", 500, null, this.command_id); - } - else { - if (timeout_type == "implicit") { - this.setSearchTimeout(aRequest); - } - else if (timeout_type == "script") { - this.setScriptTimeout(aRequest); - } - else { - this.pageTimeout = timeout; - this.sendOk(this.command_id); - } - } - }, - - /** - * Single Tap - * - * @param object aRequest - 'element' represents the ID of the element to single tap on - */ - singleTap: function MDA_singleTap(aRequest) { - this.command_id = this.getCommandId(); - let serId = aRequest.parameters.id; - let x = aRequest.parameters.x; - let y = aRequest.parameters.y; - if (this.context == "chrome") { - this.sendError("Command 'singleTap' is not available in chrome context", 500, null, this.command_id); - } - else { - this.addFrameCloseListener("tap"); - this.sendAsync("singleTap", - { - id: serId, - corx: x, - cory: y - }, - this.command_id); - } - }, - - /** - * actionChain - * - * @param object aRequest - * 'value' represents a nested array: inner array represents each event; outer array represents collection of events - */ - actionChain: function MDA_actionChain(aRequest) { - this.command_id = this.getCommandId(); - if (this.context == "chrome") { - this.sendError("Command 'actionChain' is not available in chrome context", 500, null, this.command_id); - } - else { - this.addFrameCloseListener("action chain"); - this.sendAsync("actionChain", - { - chain: aRequest.parameters.chain, - nextId: aRequest.parameters.nextId - }, - this.command_id); - } - }, - - /** - * multiAction - * - * @param object aRequest - * 'value' represents a nested array: inner array represents each event; - * middle array represents collection of events for each finger - * outer array represents all the fingers - */ - - multiAction: function MDA_multiAction(aRequest) { - this.command_id = this.getCommandId(); - if (this.context == "chrome") { - this.sendError("Command 'multiAction' is not available in chrome context", 500, null, this.command_id); - } - else { - this.addFrameCloseListener("multi action chain"); - this.sendAsync("multiAction", - { - value: aRequest.parameters.value, - maxlen: aRequest.parameters.max_length - }, - this.command_id); - } - }, - - /** - * Find an element using the indicated search strategy. - * - * @param object aRequest - * 'using' member indicates which search method to use - * 'value' member is the value the client is looking for - */ - findElement: function MDA_findElement(aRequest) { - let command_id = this.command_id = this.getCommandId(); - if (this.context == "chrome") { - let id; - try { - let on_success = this.sendResponse.bind(this); - let on_error = this.sendError.bind(this); - id = this.curBrowser.elementManager.find( - this.getCurrentWindow(), - aRequest.parameters, - this.searchTimeout, - on_success, - on_error, - false, - command_id); - } - catch (e) { - this.sendError(e.message, e.code, e.stack, command_id); - return; - } - } - else { - this.sendAsync("findElementContent", - { - value: aRequest.parameters.value, - using: aRequest.parameters.using, - element: aRequest.parameters.element, - searchTimeout: this.searchTimeout - }, - command_id); - } - }, - - /** - * Find element using the indicated search strategy - * starting from a known element. Used for WebDriver Compatibility only. - * @param {object} aRequest - * 'using' member indicates which search method to use - * 'value' member is the value the client is looking for - * 'id' member is the value of the element to start from - */ - findChildElement: function MDA_findChildElement(aRequest) { - let command_id = this.command_id = this.getCommandId(); - this.sendAsync("findElementContent", - { - value: aRequest.parameters.value, - using: aRequest.parameters.using, - element: aRequest.parameters.id, - searchTimeout: this.searchTimeout - }, - command_id); - }, - - /** - * Find elements using the indicated search strategy. - * - * @param object aRequest - * 'using' member indicates which search method to use - * 'value' member is the value the client is looking for - */ - findElements: function MDA_findElements(aRequest) { - let command_id = this.command_id = this.getCommandId(); - if (this.context == "chrome") { - let id; - try { - let on_success = this.sendResponse.bind(this); - let on_error = this.sendError.bind(this); - id = this.curBrowser.elementManager.find(this.getCurrentWindow(), - aRequest.parameters, - this.searchTimeout, - on_success, - on_error, - true, - command_id); - } - catch (e) { - this.sendError(e.message, e.code, e.stack, command_id); - return; - } - } - else { - this.sendAsync("findElementsContent", - { - value: aRequest.parameters.value, - using: aRequest.parameters.using, - element: aRequest.parameters.element, - searchTimeout: this.searchTimeout - }, - command_id); - } - }, - - /** - * Find elements using the indicated search strategy - * starting from a known element. Used for WebDriver Compatibility only. - * @param {object} aRequest - * 'using' member indicates which search method to use - * 'value' member is the value the client is looking for - * 'id' member is the value of the element to start from - */ - findChildElements: function MDA_findChildElement(aRequest) { - let command_id = this.command_id = this.getCommandId(); - this.sendAsync("findElementsContent", - { - value: aRequest.parameters.value, - using: aRequest.parameters.using, - element: aRequest.parameters.id, - searchTimeout: this.searchTimeout - }, - command_id); - }, - - /** - * Return the active element on the page - */ - getActiveElement: function MDA_getActiveElement(){ - let command_id = this.command_id = this.getCommandId(); - this.sendAsync("getActiveElement", {}, command_id); - }, - - /** - * Send click event to element - * - * @param object aRequest - * 'id' member holds the reference id to - * the element that will be clicked - */ - clickElement: function MDA_clickElementent(aRequest) { - let command_id = this.command_id = this.getCommandId(); - if (this.context == "chrome") { - try { - //NOTE: click atom fails, fall back to click() action - let el = this.curBrowser.elementManager.getKnownElement( - aRequest.parameters.id, this.getCurrentWindow()); - el.click(); - this.sendOk(command_id); - } - catch (e) { - this.sendError(e.message, e.code, e.stack, command_id); - } - } - else { - // We need to protect against the click causing an OOP frame to close. - // This fires the mozbrowserclose event when it closes so we need to - // listen for it and then just send an error back. The person making the - // call should be aware something isnt right and handle accordingly - this.addFrameCloseListener("click"); - this.sendAsync("clickElement", - { id: aRequest.parameters.id }, - command_id); - } - }, - - /** - * Get a given attribute of an element - * - * @param object aRequest - * 'id' member holds the reference id to - * the element that will be inspected - * 'name' member holds the name of the attribute to retrieve - */ - getElementAttribute: function MDA_getElementAttribute(aRequest) { - let command_id = this.command_id = this.getCommandId(); - if (this.context == "chrome") { - try { - let el = this.curBrowser.elementManager.getKnownElement( - aRequest.parameters.id, this.getCurrentWindow()); - this.sendResponse(utils.getElementAttribute(el, aRequest.parameters.name), - command_id); - } - catch (e) { - this.sendError(e.message, e.code, e.stack, command_id); - } - } - else { - this.sendAsync("getElementAttribute", - { - id: aRequest.parameters.id, - name: aRequest.parameters.name - }, - command_id); - } - }, - - /** - * Get the text of an element, if any. Includes the text of all child elements. - * - * @param object aRequest - * 'id' member holds the reference id to - * the element that will be inspected - */ - getElementText: function MDA_getElementText(aRequest) { - let command_id = this.command_id = this.getCommandId(); - if (this.context == "chrome") { - //Note: for chrome, we look at text nodes, and any node with a "label" field - try { - let el = this.curBrowser.elementManager.getKnownElement( - aRequest.parameters.id, this.getCurrentWindow()); - let lines = []; - this.getVisibleText(el, lines); - lines = lines.join("\n"); - this.sendResponse(lines, command_id); - } - catch (e) { - this.sendError(e.message, e.code, e.stack, command_id); - } - } - else { - this.sendAsync("getElementText", - { id: aRequest.parameters.id }, - command_id); - } - }, - - /** - * Get the tag name of the element. - * - * @param object aRequest - * 'id' member holds the reference id to - * the element that will be inspected - */ - getElementTagName: function MDA_getElementTagName(aRequest) { - let command_id = this.command_id = this.getCommandId(); - if (this.context == "chrome") { - try { - let el = this.curBrowser.elementManager.getKnownElement( - aRequest.parameters.id, this.getCurrentWindow()); - this.sendResponse(el.tagName.toLowerCase(), command_id); - } - catch (e) { - this.sendError(e.message, e.code, e.stack, command_id); - } - } - else { - this.sendAsync("getElementTagName", - { id: aRequest.parameters.id }, - command_id); - } - }, - - /** - * Check if element is displayed - * - * @param object aRequest - * 'id' member holds the reference id to - * the element that will be checked - */ - isElementDisplayed: function MDA_isElementDisplayed(aRequest) { - let command_id = this.command_id = this.getCommandId(); - if (this.context == "chrome") { - try { - let el = this.curBrowser.elementManager.getKnownElement( - aRequest.parameters.id, this.getCurrentWindow()); - this.sendResponse(utils.isElementDisplayed(el), command_id); - } - catch (e) { - this.sendError(e.message, e.code, e.stack, command_id); - } - } - else { - this.sendAsync("isElementDisplayed", - { id:aRequest.parameters.id }, - command_id); - } - }, - - /** - * Return the property of the computed style of an element - * - * @param object aRequest - * 'id' member holds the reference id to - * the element that will be checked - * 'propertyName' is the CSS rule that is being requested - */ - getElementValueOfCssProperty: function MDA_getElementValueOfCssProperty(aRequest){ - let command_id = this.command_id = this.getCommandId(); - let curWin = this.getCurrentWindow(); - if (this.context == "chrome") { - try { - let el = this.curBrowser.elementManager.getKnownElement(aRequest.parameters.id, curWin); - this.sendResponse(curWin.document.defaultView.getComputedStyle(el, null).getPropertyValue( - aRequest.parameters.propertyName), command_id); - } catch (e) { - this.sendError(e.message, e.code, e.stack, command_id); - } - } - else { - this.sendAsync("getElementValueOfCssProperty", - {id: aRequest.parameters.id, propertyName: aRequest.parameters.propertyName}, - command_id); - } - }, - - /** - * Submit a form on a content page by either using form or element in a form - * @param object aRequest - * 'id' member holds the reference id to - * the element that will be checked - */ - submitElement: function MDA_submitElement(aRequest) { - let command_id = this.command_id = this.getCommandId(); - if (this.context == "chrome") { - this.sendError("Command 'submitElement' is not available in chrome context", 500, null, this.command_id); - } - else { - this.sendAsync("submitElement", {id: aRequest.parameters.id}, command_id); - } - }, - - /** - * Check if element is enabled - * - * @param object aRequest - * 'id' member holds the reference id to - * the element that will be checked - */ - isElementEnabled: function(aRequest) { - let command_id = this.command_id = this.getCommandId(); - let id = aRequest.parameters.id; - if (this.context == "chrome") { - try { - // Selenium atom doesn't quite work here - let win = this.getCurrentWindow(); - let el = this.curBrowser.elementManager.getKnownElement(id, win); - this.sendResponse(!!!el.disabled, command_id); - } catch (e) { - this.sendError(e.message, e.code, e.stack, command_id); - } - } else { - this.sendAsync("isElementEnabled", {id: id}, command_id); - } - }, - - /** - * Check if element is selected - * - * @param object aRequest - * 'id' member holds the reference id to - * the element that will be checked - */ - isElementSelected: function MDA_isElementSelected(aRequest) { - let command_id = this.command_id = this.getCommandId(); - if (this.context == "chrome") { - try { - //Selenium atom doesn't quite work here - let el = this.curBrowser.elementManager.getKnownElement( - aRequest.parameters.id, this.getCurrentWindow()); - if (el.checked != undefined) { - this.sendResponse(!!el.checked, command_id); - } - else if (el.selected != undefined) { - this.sendResponse(!!el.selected, command_id); - } - else { - this.sendResponse(true, command_id); - } - } - catch (e) { - this.sendError(e.message, e.code, e.stack, command_id); - } - } - else { - this.sendAsync("isElementSelected", - { id:aRequest.parameters.id }, - command_id); - } - }, - - getElementSize: function MDA_getElementSize(aRequest) { - let command_id = this.command_id = this.getCommandId(); - if (this.context == "chrome") { - try { - let el = this.curBrowser.elementManager.getKnownElement( - aRequest.parameters.id, this.getCurrentWindow()); - let clientRect = el.getBoundingClientRect(); - this.sendResponse({width: clientRect.width, height: clientRect.height}, - command_id); - } - catch (e) { - this.sendError(e.message, e.code, e.stack, command_id); - } - } - else { - this.sendAsync("getElementSize", - { id:aRequest.parameters.id }, - command_id); - } - }, - - getElementRect: function MDA_getElementRect(aRequest) { - let command_id = this.command_id = this.getCommandId(); - if (this.context == "chrome") { - try { - let el = this.curBrowser.elementManager.getKnownElement( - aRequest.parameters.id, this.getCurrentWindow()); - let clientRect = el.getBoundingClientRect(); - this.sendResponse({x: clientRect.x + this.getCurrentWindow().pageXOffset, - y: clientRect.y + this.getCurrentWindow().pageYOffset, - width: clientRect.width, height: clientRect.height}, - command_id); - } - catch (e) { - this.sendError(e.message, e.code, e.stack, command_id); - } - } - else { - this.sendAsync("getElementRect", - { id:aRequest.parameters.id }, - command_id); - } - }, - - /** - * Send key presses to element after focusing on it - * - * @param object aRequest - * 'id' member holds the reference id to - * the element that will be checked - * 'value' member holds the value to send to the element - */ - sendKeysToElement: function MDA_sendKeysToElement(aRequest) { - let command_id = this.command_id = this.getCommandId(); - if (this.context == "chrome") { - let currentWindow = this.getCurrentWindow(); - let el = this.curBrowser.elementManager.getKnownElement( - aRequest.parameters.id, currentWindow); - utils.sendKeysToElement(currentWindow, el, aRequest.parameters.value, - this.sendOk.bind(this), this.sendError.bind(this), - command_id, this.context); - } - else { - this.sendAsync("sendKeysToElement", - { - id:aRequest.parameters.id, - value: aRequest.parameters.value - }, - command_id); - } - }, - - /** - * Sets the test name - * - * The test name is used in logging messages. - */ - setTestName: function MDA_setTestName(aRequest) { - this.command_id = this.getCommandId(); - this.testName = aRequest.parameters.value; - this.sendAsync("setTestName", - { value: aRequest.parameters.value }, - this.command_id); - }, - - /** - * Clear the text of an element - * - * @param object aRequest - * 'id' member holds the reference id to - * the element that will be cleared - */ - clearElement: function MDA_clearElement(aRequest) { - let command_id = this.command_id = this.getCommandId(); - if (this.context == "chrome") { - //the selenium atom doesn't work here - try { - let el = this.curBrowser.elementManager.getKnownElement( - aRequest.parameters.id, this.getCurrentWindow()); - if (el.nodeName == "textbox") { - el.value = ""; - } - else if (el.nodeName == "checkbox") { - el.checked = false; - } - this.sendOk(command_id); - } - catch (e) { - this.sendError(e.message, e.code, e.stack, command_id); - } - } - else { - this.sendAsync("clearElement", - { id:aRequest.parameters.id }, - command_id); - } - }, - - /** - * Get an element's location on the page. - * - * The returned point will contain the x and y coordinates of the - * top left-hand corner of the given element. The point (0,0) - * refers to the upper-left corner of the document. - * - * @return a point containing x and y coordinates as properties - */ - getElementLocation: function MDA_getElementLocation(aRequest) { - this.command_id = this.getCommandId(); - this.sendAsync("getElementLocation", {id: aRequest.parameters.id}, - this.command_id); - }, - - /** - * Add a cookie to the document. - */ - addCookie: function MDA_addCookie(aRequest) { - this.command_id = this.getCommandId(); - this.sendAsync("addCookie", - { cookie:aRequest.parameters.cookie }, - this.command_id); - }, - - /** - * Get all the cookies for the current domain. - * - * This is the equivalent of calling "document.cookie" and parsing - * the result. - */ - getCookies: function MDA_getCookies() { - this.command_id = this.getCommandId(); - this.sendAsync("getCookies", {}, this.command_id); - }, - - /** - * Delete all cookies that are visible to a document - */ - deleteAllCookies: function MDA_deleteAllCookies() { - this.command_id = this.getCommandId(); - this.sendAsync("deleteAllCookies", {}, this.command_id); - }, - - /** - * Delete a cookie by name - */ - deleteCookie: function MDA_deleteCookie(aRequest) { - this.command_id = this.getCommandId(); - this.sendAsync("deleteCookie", - { name:aRequest.parameters.name }, - this.command_id); - }, - - /** - * Close the current window, ending the session if it's the last - * window currently open. - * - * On B2G this method is a noop and will return immediately. - */ - close: function MDA_close() { - let command_id = this.command_id = this.getCommandId(); - if (appName == "B2G") { - // We can't close windows so just return - this.sendOk(command_id); - } - else { - // Get the total number of windows - let numOpenWindows = 0; - let winEnum = this.getWinEnumerator(); - while (winEnum.hasMoreElements()) { - let win = winEnum.getNext(); - // Return windows and tabs. - if (win.gBrowser) { - numOpenWindows += win.gBrowser.browsers.length; - } else { - numOpenWindows += 1; - } - } - - // if there is only 1 window left, delete the session - if (numOpenWindows === 1) { - try { - this.sessionTearDown(); - } - catch (e) { - this.sendError("Could not clear session", 500, - e.name + ": " + e.message, command_id); - return; - } - this.sendOk(command_id); - return; - } - - try { - if (this.messageManager != this.globalMessageManager) { - this.messageManager.removeDelayedFrameScript(FRAME_SCRIPT); - } - if (this.curBrowser.tab) { - this.curBrowser.closeTab(); - } else { - this.getCurrentWindow().close(); - } - this.sendOk(command_id); - } - catch (e) { - this.sendError("Could not close window: " + e.message, 13, e.stack, - command_id); - } - } - }, - - /** - * Close the currently selected chrome window, ending the session if it's the last - * window currently open. - * - * On B2G this method is a noop and will return immediately. - */ - closeChromeWindow: function MDA_closeChromeWindow() { - let command_id = this.command_id = this.getCommandId(); - if (appName == "B2G") { - // We can't close windows so just return - this.sendOk(command_id); - } - else { - // Get the total number of windows - let numOpenWindows = 0; - let winEnum = this.getWinEnumerator(); - while (winEnum.hasMoreElements()) { - numOpenWindows += 1; - winEnum.getNext(); - } - - // if there is only 1 window left, delete the session - if (numOpenWindows === 1) { - try { - this.sessionTearDown(); - } - catch (e) { - this.sendError("Could not clear session", 500, - e.name + ": " + e.message, command_id); - return; - } - this.sendOk(command_id); - return; - } - - try { - this.messageManager.removeDelayedFrameScript(FRAME_SCRIPT); - this.getCurrentWindow().close(); - this.sendOk(command_id); - } - catch (e) { - this.sendError("Could not close window: " + e.message, 13, e.stack, - command_id); - } - } - }, - - /** - * Deletes the session. - * - * If it is a desktop environment, it will close all listeners - * - * If it is a B2G environment, it will make the main content listener sleep, and close - * all other listeners. The main content listener persists after disconnect (it's the homescreen), - * and can safely be reused. - */ - sessionTearDown: function MDA_sessionTearDown() { - if (this.curBrowser != null) { - if (appName == "B2G") { - this.globalMessageManager.broadcastAsyncMessage( - "Marionette:sleepSession" + this.curBrowser.mainContentId, {}); - this.curBrowser.knownFrames.splice( - this.curBrowser.knownFrames.indexOf(this.curBrowser.mainContentId), 1); - } - else { - //don't set this pref for B2G since the framescript can be safely reused - Services.prefs.setBoolPref("marionette.contentListener", false); - } - //delete session in each frame in each browser - for (let win in this.browsers) { - let browser = this.browsers[win]; - for (let i in browser.knownFrames) { - this.globalMessageManager.broadcastAsyncMessage("Marionette:deleteSession" + browser.knownFrames[i], {}); - } - } - let winEnum = this.getWinEnumerator(); - while (winEnum.hasMoreElements()) { - winEnum.getNext().messageManager.removeDelayedFrameScript(FRAME_SCRIPT); - } - this.curBrowser.frameManager.removeSpecialPowers(); - this.curBrowser.frameManager.removeMessageManagerListeners(this.globalMessageManager); - } - this.switchToGlobalMessageManager(); - // reset frame to the top-most frame - this.curFrame = null; - if (this.mainFrame) { - this.mainFrame.focus(); - } - this.sessionId = null; - this.deleteFile('marionetteChromeScripts'); - this.deleteFile('marionetteContentScripts'); - - if (this.observing !== null) { - for (let topic in this.observing) { - Services.obs.removeObserver(this.observing[topic], topic); - } - this.observing = null; - } - }, - - /** - * Processes the 'deleteSession' request from the client by tearing down - * the session and responding 'ok'. - */ - deleteSession: function MDA_deleteSession() { - let command_id = this.command_id = this.getCommandId(); - try { - this.sessionTearDown(); - } - catch (e) { - this.sendError("Could not delete session", 500, e.name + ": " + e.message, command_id); - return; - } - this.sendOk(command_id); - }, - - /** - * Quits the application with the provided flags and tears down the - * current session. - */ - quitApplication: function MDA_quitApplication (aRequest) { - let command_id = this.command_id = this.getCommandId(); - if (appName != "Firefox") { - this.sendError("In app initiated quit only supported on Firefox", 500, null, command_id); - } - - let flagsArray = aRequest.parameters.flags; - let flags = Ci.nsIAppStartup.eAttemptQuit; - for (let k of flagsArray) { - flags |= Ci.nsIAppStartup[k]; - } - - // Close the listener so we can't re-connect until after the restart. - this.server.closeListener(); - this.quitFlags = flags; - - // This notifies the client it's safe to begin attempting to reconnect. - // The actual quit will happen when the current socket connection is closed. - this.sendOk(command_id); - }, - - /** - * Returns the current status of the Application Cache - */ - getAppCacheStatus: function MDA_getAppCacheStatus(aRequest) { - this.command_id = this.getCommandId(); - this.sendAsync("getAppCacheStatus", {}, this.command_id); - }, - - _emu_cb_id: 0, - _emu_cbs: null, - runEmulatorCmd: function runEmulatorCmd(cmd, callback) { - if (callback) { - if (!this._emu_cbs) { - this._emu_cbs = {}; - } - this._emu_cbs[this._emu_cb_id] = callback; - } - this.sendToClient({emulator_cmd: cmd, id: this._emu_cb_id}, -1); - this._emu_cb_id += 1; - }, - - runEmulatorShell: function runEmulatorShell(args, callback) { - if (callback) { - if (!this._emu_cbs) { - this._emu_cbs = {}; - } - this._emu_cbs[this._emu_cb_id] = callback; - } - this.sendToClient({emulator_shell: args, id: this._emu_cb_id}, -1); - this._emu_cb_id += 1; - }, - - emulatorCmdResult: function emulatorCmdResult(message) { - if (this.context != "chrome") { - this.sendAsync("emulatorCmdResult", message, -1); - return; - } - - if (!this._emu_cbs) { - return; - } - - let cb = this._emu_cbs[message.id]; - delete this._emu_cbs[message.id]; - if (!cb) { - return; - } - try { - cb(message.result); - } - catch(e) { - this.sendError(e.message, e.code, e.stack, -1); - return; - } - }, - - importScript: function MDA_importScript(aRequest) { - let command_id = this.command_id = this.getCommandId(); - let converter = - Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]. - createInstance(Components.interfaces.nsIScriptableUnicodeConverter); - converter.charset = "UTF-8"; - let result = {}; - let data = converter.convertToByteArray(aRequest.parameters.script, result); - let ch = Components.classes["@mozilla.org/security/hash;1"] - .createInstance(Components.interfaces.nsICryptoHash); - ch.init(ch.MD5); - ch.update(data, data.length); - let hash = ch.finish(true); - if (this.importedScriptHashes[this.context].indexOf(hash) > -1) { - //we have already imported this script - this.sendOk(command_id); - return; - } - this.importedScriptHashes[this.context].push(hash); - if (this.context == "chrome") { - let file; - if (this.importedScripts.exists()) { - file = FileUtils.openFileOutputStream(this.importedScripts, - FileUtils.MODE_APPEND | FileUtils.MODE_WRONLY); - } - else { - //Note: The permission bits here don't actually get set (bug 804563) - this.importedScripts.createUnique( - Components.interfaces.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8)); - file = FileUtils.openFileOutputStream(this.importedScripts, - FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE); - this.importedScripts.permissions = parseInt("0666", 8); //actually set permissions - } - file.write(aRequest.parameters.script, aRequest.parameters.script.length); - file.close(); - this.sendOk(command_id); - } - else { - this.sendAsync("importScript", - { script: aRequest.parameters.script }, - command_id); - } - }, - - clearImportedScripts: function MDA_clearImportedScripts(aRequest) { - let command_id = this.command_id = this.getCommandId(); - try { - if (this.context == "chrome") { - this.deleteFile('marionetteChromeScripts'); - } - else { - this.deleteFile('marionetteContentScripts'); - } - } - catch (e) { - this.sendError("Could not clear imported scripts", 500, e.name + ": " + e.message, command_id); - return; - } - this.sendOk(command_id); - }, - - /** - * Takes a screenshot of a web element, current frame, or viewport. - * - * The screen capture is returned as a lossless PNG image encoded as - * a base 64 string. - * - * If called in the content context, the id argument is not null - * and refers to a present and visible web element's ID, the capture area - * will be limited to the bounding box of that element. Otherwise, the - * capture area will be the bounding box of the current frame. - * - * If called in the chrome context, the screenshot will always represent the - * entire viewport. - * - * @param {string} [id] Reference to a web element. - * @param {string} [highlights] List of web elements to highlight. - * @return {string} PNG image encoded as base 64 string. - */ - takeScreenshot: function MDA_takeScreenshot(aRequest) { - this.command_id = this.getCommandId(); - if (this.context == "chrome") { - var win = this.getCurrentWindow(); - var canvas = win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); - var doc; - if (appName == "B2G") { - doc = win.document.body; - } else { - doc = win.document.getElementsByTagName('window')[0]; - } - var docRect = doc.getBoundingClientRect(); - var width = docRect.width; - var height = docRect.height; - - // Convert width and height from CSS pixels (potentially fractional) - // to device pixels (integer). - var scale = win.devicePixelRatio; - canvas.setAttribute("width", Math.round(width * scale)); - canvas.setAttribute("height", Math.round(height * scale)); - - var context = canvas.getContext("2d"); - var flags; - if (appName == "B2G") { - flags = - context.DRAWWINDOW_DRAW_CARET | - context.DRAWWINDOW_DRAW_VIEW | - context.DRAWWINDOW_USE_WIDGET_LAYERS; - } else { - // Bug 1075168 - CanvasRenderingContext2D image is distorted - // when using certain flags in chrome context. - flags = - context.DRAWWINDOW_DRAW_VIEW | - context.DRAWWINDOW_USE_WIDGET_LAYERS; - } - context.scale(scale, scale); - context.drawWindow(win, 0, 0, width, height, "rgb(255,255,255)", flags); - var dataUrl = canvas.toDataURL("image/png", ""); - var data = dataUrl.substring(dataUrl.indexOf(",") + 1); - this.sendResponse(data, this.command_id); - } - else { - this.sendAsync("takeScreenshot", - {id: aRequest.parameters.id, - highlights: aRequest.parameters.highlights, - full: aRequest.parameters.full}, - this.command_id); - } - }, - - /** - * Get the current browser orientation. - * - * Will return one of the valid primary orientation values - * portrait-primary, landscape-primary, portrait-secondary, or - * landscape-secondary. - */ - getScreenOrientation: function MDA_getScreenOrientation(aRequest) { - this.command_id = this.getCommandId(); - let curWindow = this.getCurrentWindow(); - let or = curWindow.screen.mozOrientation; - this.sendResponse(or, this.command_id); - }, - - /** - * Set the current browser orientation. - * - * The supplied orientation should be given as one of the valid - * orientation values. If the orientation is unknown, an error will - * be raised. - * - * Valid orientations are "portrait" and "landscape", which fall - * back to "portrait-primary" and "landscape-primary" respectively, - * and "portrait-secondary" as well as "landscape-secondary". - */ - setScreenOrientation: function MDA_setScreenOrientation(aRequest) { - const ors = ["portrait", "landscape", - "portrait-primary", "landscape-primary", - "portrait-secondary", "landscape-secondary"]; - - this.command_id = this.getCommandId(); - let or = String(aRequest.parameters.orientation); - - let mozOr = or.toLowerCase(); - if (ors.indexOf(mozOr) < 0) { - this.sendError("Unknown screen orientation: " + or, 500, null, - this.command_id); - return; - } - - let curWindow = this.getCurrentWindow(); - if (!curWindow.screen.mozLockOrientation(mozOr)) { - this.sendError("Unable to set screen orientation: " + or, 500, - null, this.command_id); - } - this.sendOk(this.command_id); - }, - - /** - * Get the size of the browser window currently in focus. - * - * Will return the current browser window size in pixels. Refers to - * window outerWidth and outerHeight values, which include scroll bars, - * title bars, etc. - * - */ - getWindowSize: function MDA_getWindowSize(aRequest) { - this.command_id = this.getCommandId(); - let curWindow = this.getCurrentWindow(); - let curWidth = curWindow.outerWidth; - let curHeight = curWindow.outerHeight; - this.sendResponse({width: curWidth, height: curHeight}, this.command_id); - }, - - /** - * Set the size of the browser window currently in focus. - * - * Not supported on B2G. The supplied width and height values refer to - * the window outerWidth and outerHeight values, which include scroll - * bars, title bars, etc. - * - * An error will be returned if the requested window size would result - * in the window being in the maximized state. - */ - setWindowSize: function MDA_setWindowSize(aRequest) { - this.command_id = this.getCommandId(); - - if (appName !== "Firefox") { - this.sendError("Not supported on mobile", 405, null, this.command_id); - return; - } - - try { - var width = parseInt(aRequest.parameters.width); - var height = parseInt(aRequest.parameters.height); - } - catch(e) { - this.sendError(e.message, e.code, e.stack, this.command_id); - return; - } - - let curWindow = this.getCurrentWindow(); - if (width >= curWindow.screen.availWidth && height >= curWindow.screen.availHeight) { - this.sendError("Invalid requested size, cannot maximize", 405, null, this.command_id); - return; - } - - curWindow.resizeTo(width, height); - this.sendOk(this.command_id); - }, - - /** - * Maximizes the Browser Window as if the user pressed the maximise button - * - * Not Supported on B2G or Fennec - */ - maximizeWindow: function MDA_maximizeWindow (aRequest) { - this.command_id = this.getCommandId(); - - if (appName !== "Firefox") { - this.sendError("Not supported for mobile", 405, null, this.command_id); - return; - } - - let curWindow = this.getCurrentWindow(); - curWindow.moveTo(0,0); - curWindow.resizeTo(curWindow.screen.availWidth, curWindow.screen.availHeight); - this.sendOk(this.command_id); - }, - - /** - * Returns the ChromeWindow associated with an open dialog window if it is - * currently attached to the dom. - */ - get activeDialogWindow () { - if (this._dialogWindowRef !== null) { - let dialogWin = this._dialogWindowRef.get(); - if (dialogWin && dialogWin.parent) { - return dialogWin; - } - } - return null; - }, - - get activeDialogUI () { - let dialogWin = this.activeDialogWindow; - if (dialogWin) { - return dialogWin.Dialog.ui; - } - return this.curBrowser.getTabModalUI(); - }, - - /** - * Dismisses a currently displayed tab modal, or returns no such alert if - * no modal is displayed. - */ - dismissDialog: function MDA_dismissDialog() { - this.command_id = this.getCommandId(); - if (this.activeDialogUI === null) { - this.sendError("No tab modal was open when attempting to dismiss the dialog", - 27, null, this.command_id); - return; - } - - let {button0, button1} = this.activeDialogUI; - (button1 ? button1 : button0).click(); - this.sendOk(this.command_id); - }, - - /** - * Accepts a currently displayed tab modal, or returns no such alert if - * no modal is displayed. - */ - acceptDialog: function MDA_acceptDialog() { - this.command_id = this.getCommandId(); - if (this.activeDialogUI === null) { - this.sendError("No tab modal was open when attempting to accept the dialog", - 27, null, this.command_id); - return; - } - - let {button0} = this.activeDialogUI; - button0.click(); - this.sendOk(this.command_id); - }, - - /** - * Returns the message shown in a currently displayed modal, or returns a no such - * alert error if no modal is currently displayed. - */ - getTextFromDialog: function MDA_getTextFromDialog() { - this.command_id = this.getCommandId(); - if (this.activeDialogUI === null) { - this.sendError("No tab modal was open when attempting to get the dialog text", - 27, null, this.command_id); - return; - } - - let {infoBody} = this.activeDialogUI; - this.sendResponse(infoBody.textContent, this.command_id); - }, - - /** - * Sends keys to the input field of a currently displayed modal, or returns a - * no such alert error if no modal is currently displayed. If a tab modal is currently - * displayed but has no means for text input, an element not visible error is returned. - */ - sendKeysToDialog: function MDA_sendKeysToDialog(aRequest) { - this.command_id = this.getCommandId(); - if (this.activeDialogUI === null) { - this.sendError("No tab modal was open when attempting to send keys to a dialog", - 27, null, this.command_id); - return; - } - - // See toolkit/components/prompts/contentb/commonDialog.js - let {loginContainer, loginTextbox} = this.activeDialogUI; - if (loginContainer.hidden) { - this.sendError("This prompt does not accept text input", - 11, null, this.command_id); - } - - let win = this.activeDialogWindow ? this.activeDialogWindow : this.getCurrentWindow(); - utils.sendKeysToElement(win, loginTextbox, aRequest.parameters.value, - this.sendOk.bind(this), this.sendError.bind(this), - this.command_id, "chrome"); - }, - - /** - * Helper function to convert an outerWindowID into a UID that Marionette - * tracks. - */ - generateFrameId: function MDA_generateFrameId(id) { - let uid = id + (appName == "B2G" ? "-b2g" : ""); - return uid; - }, - - /** - * Handle a dialog opening by shortcutting the current request to prevent the client - * from hanging entirely. This is inspired by selenium's mode of dealing with this, - * but is significantly lighter weight, and may necessitate a different framework - * for handling this as more features are required. - */ - handleDialogLoad: function MDA_handleModalLoad(subject, topic) { - // We shouldn't return to the client due to the modal associated with the - // jsdebugger. - let clickToStart; - try { - clickToStart = Services.prefs.getBoolPref('marionette.debugging.clicktostart'); - } catch (e) { } - if (clickToStart) { - Services.prefs.setBoolPref('marionette.debugging.clicktostart', false); - return; - } - - if (topic == "common-dialog-loaded") { - this._dialogWindowRef = Cu.getWeakReference(subject); - } - - if (this.command_id) { - this.sendAsync("cancelRequest", {}); - // This is a shortcut to get the client to accept our response whether - // the expected key is 'ok' (in case a click or similar got us here) - // or 'value' (in case an execute script or similar got us here). - this.sendToClient({from:this.actorID, ok: true, value: null}, this.command_id); - } - }, - - /** - * Receives all messages from content messageManager - */ - receiveMessage: function MDA_receiveMessage(message) { - // We need to just check if we need to remove the mozbrowserclose listener - if (this.mozBrowserClose !== null){ - let curWindow = this.getCurrentWindow(); - curWindow.removeEventListener('mozbrowserclose', this.mozBrowserClose, true); - this.mozBrowserClose = null; - } - - switch (message.name) { - case "Marionette:done": - this.sendResponse(message.json.value, message.json.command_id); - break; - case "Marionette:ok": - this.sendOk(message.json.command_id); - break; - case "Marionette:error": - this.sendError(message.json.message, message.json.status, message.json.stacktrace, message.json.command_id); - break; - case "Marionette:log": - //log server-side messages - logger.info(message.json.message); - break; - case "Marionette:shareData": - //log messages from tests - if (message.json.log) { - this.marionetteLog.addLogs(message.json.log); - } - break; - case "Marionette:runEmulatorCmd": - case "Marionette:runEmulatorShell": - this.sendToClient(message.json, -1); - break; - case "Marionette:switchToFrame": - this.oopFrameId = this.curBrowser.frameManager.switchToFrame(message); - this.messageManager = this.curBrowser.frameManager.currentRemoteFrame.messageManager.get(); - break; - case "Marionette:switchToModalOrigin": - this.curBrowser.frameManager.switchToModalOrigin(message); - this.messageManager = this.curBrowser.frameManager.currentRemoteFrame.messageManager.get(); - break; - case "Marionette:switchedToFrame": - logger.info("Switched to frame: " + JSON.stringify(message.json)); - if (message.json.restorePrevious) { - this.currentFrameElement = this.previousFrameElement; - } - else { - if (message.json.storePrevious) { - // we don't arbitrarily save previousFrameElement, since - // we allow frame switching after modals appear, which would - // override this value and we'd lose our reference - this.previousFrameElement = this.currentFrameElement; - } - this.currentFrameElement = message.json.frameValue; - } - break; - case "Marionette:getVisibleCookies": - let [currentPath, host] = message.json.value; - let isForCurrentPath = function(aPath) { - return currentPath.indexOf(aPath) != -1; - } - let results = []; - let enumerator = cookieManager.enumerator; - while (enumerator.hasMoreElements()) { - let cookie = enumerator.getNext().QueryInterface(Ci['nsICookie']); - // Take the hostname and progressively shorten - let hostname = host; - do { - if ((cookie.host == '.' + hostname || cookie.host == hostname) - && isForCurrentPath(cookie.path)) { - results.push({ - 'name': cookie.name, - 'value': cookie.value, - 'path': cookie.path, - 'host': cookie.host, - 'secure': cookie.isSecure, - 'expiry': cookie.expires - }); - break; - } - hostname = hostname.replace(/^.*?\./, ''); - } while (hostname.indexOf('.') != -1); - } - return results; - case "Marionette:addCookie": - let cookieToAdd = message.json.value; - Services.cookies.add(cookieToAdd.domain, cookieToAdd.path, cookieToAdd.name, - cookieToAdd.value, cookieToAdd.secure, false, false, - cookieToAdd.expiry); - return true; - case "Marionette:deleteCookie": - let cookieToDelete = message.json.value; - cookieManager.remove(cookieToDelete.host, cookieToDelete.name, - cookieToDelete.path, false); - return true; - case "Marionette:register": - // This code processes the content listener's registration information - // and either accepts the listener, or ignores it - let nullPrevious = (this.curBrowser.curFrameId == null); - let listenerWindow = null; - try { - listenerWindow = Services.wm.getOuterWindowWithId(message.json.value); - } catch (ex) { } - - //go in here if we're already in a remote frame. - if (this.curBrowser.frameManager.currentRemoteFrame !== null && - (!listenerWindow || - this.messageManager == this.curBrowser.frameManager.currentRemoteFrame.messageManager.get())) { - // The outerWindowID from an OOP frame will not be meaningful to - // the parent process here, since each process maintains its own - // independent window list. So, it will either be null (!listenerWindow) - // if we're already in a remote frame, - // or it will point to some random window, which will hopefully - // cause an href mismatch. Currently this only happens - // in B2G for OOP frames registered in Marionette:switchToFrame, so - // we'll acknowledge the switchToFrame message here. - // XXX: Should have a better way of determining that this message - // is from a remote frame. - this.curBrowser.frameManager.currentRemoteFrame.targetFrameId = this.generateFrameId(message.json.value); - this.sendOk(this.command_id); - } - - let browserType; - try { - browserType = message.target.getAttribute("type"); - } catch (ex) { - // browserType remains undefined. - } - let reg = {}; - // this will be sent to tell the content process if it is the main content - let mainContent = (this.curBrowser.mainContentId == null); - if (!browserType || browserType != "content") { - //curBrowser holds all the registered frames in knownFrames - let uid = this.generateFrameId(message.json.value); - reg.id = uid; - reg.remotenessChange = this.curBrowser.register(uid, message.target); - } - // set to true if we updated mainContentId - mainContent = ((mainContent == true) && (this.curBrowser.mainContentId != null)); - if (mainContent) { - this.mainContentFrameId = this.curBrowser.curFrameId; - } - this.curBrowser.elementManager.seenItems[reg.id] = Cu.getWeakReference(listenerWindow); - if (nullPrevious && (this.curBrowser.curFrameId != null)) { - if (!this.sendAsync("newSession", - { B2G: (appName == "B2G"), - raisesAccessibilityExceptions: - this.sessionCapabilities.raisesAccessibilityExceptions }, - this.newSessionCommandId)) { - return; - } - if (this.curBrowser.newSession) { - this.getSessionCapabilities(); - this.newSessionCommandId = null; - } - } - if (this.curBrowser.frameRegsPending) { - if (this.curBrowser.frameRegsPending > 0) { - this.curBrowser.frameRegsPending -= 1; - } - if (this.curBrowser.frameRegsPending === 0) { - // In case of a freshly registered window, we're responsible here - // for sending the ack. - this.sendOk(this.command_id); - } - } - return [reg, mainContent]; - case "Marionette:emitTouchEvent": - let globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"] - .getService(Ci.nsIMessageBroadcaster); - globalMessageManager.broadcastAsyncMessage( - "MarionetteMainListener:emitTouchEvent", message.json); - return; - case "Marionette:listenersAttached": - if (message.json.listenerId === this.curBrowser.curFrameId) { - // If remoteness gets updated we need to call newSession. In the case - // of desktop this just sets up a small amount of state that doesn't - // change over the course of a session. - let newSessionValues = { - B2G: (appName == "B2G"), - raisesAccessibilityExceptions: this.sessionCapabilities.raisesAccessibilityExceptions - }; - this.sendAsync("newSession", newSessionValues); - this.curBrowser.flushPendingCommands(); - } - return; - } - } -}; - -MarionetteServerConnection.prototype.requestTypes = { - "getMarionetteID": MarionetteServerConnection.prototype.getMarionetteID, - "sayHello": MarionetteServerConnection.prototype.sayHello, - "newSession": MarionetteServerConnection.prototype.newSession, - "getSessionCapabilities": MarionetteServerConnection.prototype.getSessionCapabilities, - "log": MarionetteServerConnection.prototype.log, - "getLogs": MarionetteServerConnection.prototype.getLogs, - "setContext": MarionetteServerConnection.prototype.setContext, - "getContext": MarionetteServerConnection.prototype.getContext, - "executeScript": MarionetteServerConnection.prototype.execute, - "setScriptTimeout": MarionetteServerConnection.prototype.setScriptTimeout, - "timeouts": MarionetteServerConnection.prototype.timeouts, - "singleTap": MarionetteServerConnection.prototype.singleTap, - "actionChain": MarionetteServerConnection.prototype.actionChain, - "multiAction": MarionetteServerConnection.prototype.multiAction, - "executeAsyncScript": MarionetteServerConnection.prototype.executeWithCallback, - "executeJSScript": MarionetteServerConnection.prototype.executeJSScript, - "setSearchTimeout": MarionetteServerConnection.prototype.setSearchTimeout, - "findElement": MarionetteServerConnection.prototype.findElement, - "findChildElement": MarionetteServerConnection.prototype.findChildElements, // Needed for WebDriver compat - "findElements": MarionetteServerConnection.prototype.findElements, - "findChildElements":MarionetteServerConnection.prototype.findChildElements, // Needed for WebDriver compat - "clickElement": MarionetteServerConnection.prototype.clickElement, - "getElementAttribute": MarionetteServerConnection.prototype.getElementAttribute, - "getElementText": MarionetteServerConnection.prototype.getElementText, - "getElementTagName": MarionetteServerConnection.prototype.getElementTagName, - "isElementDisplayed": MarionetteServerConnection.prototype.isElementDisplayed, - "getElementValueOfCssProperty": MarionetteServerConnection.prototype.getElementValueOfCssProperty, - "submitElement": MarionetteServerConnection.prototype.submitElement, - "getElementSize": MarionetteServerConnection.prototype.getElementSize, //deprecated - "getElementRect": MarionetteServerConnection.prototype.getElementRect, - "isElementEnabled": MarionetteServerConnection.prototype.isElementEnabled, - "isElementSelected": MarionetteServerConnection.prototype.isElementSelected, - "sendKeysToElement": MarionetteServerConnection.prototype.sendKeysToElement, - "getElementLocation": MarionetteServerConnection.prototype.getElementLocation, // deprecated - "getElementPosition": MarionetteServerConnection.prototype.getElementLocation, // deprecated - "clearElement": MarionetteServerConnection.prototype.clearElement, - "getTitle": MarionetteServerConnection.prototype.getTitle, - "getWindowType": MarionetteServerConnection.prototype.getWindowType, - "getPageSource": MarionetteServerConnection.prototype.getPageSource, - "get": MarionetteServerConnection.prototype.get, - "goUrl": MarionetteServerConnection.prototype.get, // deprecated - "getCurrentUrl": MarionetteServerConnection.prototype.getCurrentUrl, - "getUrl": MarionetteServerConnection.prototype.getCurrentUrl, // deprecated - "goBack": MarionetteServerConnection.prototype.goBack, - "goForward": MarionetteServerConnection.prototype.goForward, - "refresh": MarionetteServerConnection.prototype.refresh, - "getWindowHandle": MarionetteServerConnection.prototype.getWindowHandle, - "getCurrentWindowHandle": MarionetteServerConnection.prototype.getWindowHandle, // Selenium 2 compat - "getChromeWindowHandle": MarionetteServerConnection.prototype.getChromeWindowHandle, - "getCurrentChromeWindowHandle": MarionetteServerConnection.prototype.getChromeWindowHandle, - "getWindow": MarionetteServerConnection.prototype.getWindowHandle, // deprecated - "getWindowHandles": MarionetteServerConnection.prototype.getWindowHandles, - "getChromeWindowHandles": MarionetteServerConnection.prototype.getChromeWindowHandles, - "getCurrentWindowHandles": MarionetteServerConnection.prototype.getWindowHandles, // Selenium 2 compat - "getWindows": MarionetteServerConnection.prototype.getWindowHandles, // deprecated - "getWindowPosition": MarionetteServerConnection.prototype.getWindowPosition, - "setWindowPosition": MarionetteServerConnection.prototype.setWindowPosition, - "getActiveFrame": MarionetteServerConnection.prototype.getActiveFrame, - "switchToFrame": MarionetteServerConnection.prototype.switchToFrame, - "switchToWindow": MarionetteServerConnection.prototype.switchToWindow, - "deleteSession": MarionetteServerConnection.prototype.deleteSession, - "quitApplication": MarionetteServerConnection.prototype.quitApplication, - "emulatorCmdResult": MarionetteServerConnection.prototype.emulatorCmdResult, - "importScript": MarionetteServerConnection.prototype.importScript, - "clearImportedScripts": MarionetteServerConnection.prototype.clearImportedScripts, - "getAppCacheStatus": MarionetteServerConnection.prototype.getAppCacheStatus, - "close": MarionetteServerConnection.prototype.close, - "closeWindow": MarionetteServerConnection.prototype.close, // deprecated - "closeChromeWindow": MarionetteServerConnection.prototype.closeChromeWindow, - "setTestName": MarionetteServerConnection.prototype.setTestName, - "takeScreenshot": MarionetteServerConnection.prototype.takeScreenshot, - "screenShot": MarionetteServerConnection.prototype.takeScreenshot, // deprecated - "screenshot": MarionetteServerConnection.prototype.takeScreenshot, // Selenium 2 compat - "addCookie": MarionetteServerConnection.prototype.addCookie, - "getCookies": MarionetteServerConnection.prototype.getCookies, - "getAllCookies": MarionetteServerConnection.prototype.getCookies, // deprecated - "deleteAllCookies": MarionetteServerConnection.prototype.deleteAllCookies, - "deleteCookie": MarionetteServerConnection.prototype.deleteCookie, - "getActiveElement": MarionetteServerConnection.prototype.getActiveElement, - "getScreenOrientation": MarionetteServerConnection.prototype.getScreenOrientation, - "setScreenOrientation": MarionetteServerConnection.prototype.setScreenOrientation, - "getWindowSize": MarionetteServerConnection.prototype.getWindowSize, - "setWindowSize": MarionetteServerConnection.prototype.setWindowSize, - "maximizeWindow": MarionetteServerConnection.prototype.maximizeWindow, - "dismissDialog": MarionetteServerConnection.prototype.dismissDialog, - "acceptDialog": MarionetteServerConnection.prototype.acceptDialog, - "getTextFromDialog": MarionetteServerConnection.prototype.getTextFromDialog, - "sendKeysToDialog": MarionetteServerConnection.prototype.sendKeysToDialog -}; - -/** - * Creates a BrowserObj. BrowserObjs handle interactions with the - * browser, according to the current environment (desktop, b2g, etc.) - * - * @param nsIDOMWindow win - * The window whose browser needs to be accessed - */ - -function BrowserObj(win, server) { - this.DESKTOP = "desktop"; - this.B2G = "B2G"; - this.browser; - this.window = win; - this.knownFrames = []; - this.curFrameId = null; - this.startPage = "about:blank"; - this.mainContentId = null; // used in B2G to identify the homescreen content page - this.newSession = true; //used to set curFrameId upon new session - this.elementManager = new ElementManager([NAME, LINK_TEXT, PARTIAL_LINK_TEXT]); - this.setBrowser(win); - this.frameManager = new FrameManager(server); //We should have one FM per BO so that we can handle modals in each Browser - - // A reference to the tab corresponding to the current window handle, if any. - this.tab = null; - this.pendingCommands = []; - - //register all message listeners - this.frameManager.addMessageManagerListeners(server.messageManager); - this.getIdForBrowser = server.getIdForBrowser.bind(server); - this.updateIdForBrowser = server.updateIdForBrowser.bind(server); - this._curFrameId = null; - this._browserWasRemote = null; - this._hasRemotenessChange = false; -} - -BrowserObj.prototype = { - - /** - * This function intercepts commands interacting with content and queues - * or executes them as needed. - * - * No commands interacting with content are safe to process until - * the new listener script is loaded and registers itself. - * This occurs when a command whose effect is asynchronous (such - * as goBack) results in a remoteness change and new commands - * are subsequently posted to the server. - */ - executeWhenReady: function (callback) { - if (this.hasRemotenessChange()) { - this.pendingCommands.push(callback); - } else { - callback(); - } - }, - - /** - * Re-sets this BrowserObject's current tab and updates remoteness tracking. - */ - switchToTab: function (ind) { - if (this.browser) { - this.browser.selectTabAtIndex(ind); - this.tab = this.browser.selectedTab; - } - this._browserWasRemote = this.browser.getBrowserForTab(this.tab).isRemoteBrowser; - this._hasRemotenessChange = false; - }, - - /** - * Retrieves the current tabmodal ui object. According to the browser associated - * with the currently selected tab. - */ - getTabModalUI: function MDA__getTabModaUI () { - let browserForTab = this.browser.getBrowserForTab(this.tab); - if (!browserForTab.hasAttribute('tabmodalPromptShowing')) { - return null; - } - // The modal is a direct sibling of the browser element. See tabbrowser.xml's - // getTabModalPromptBox. - let modals = browserForTab.parentNode - .getElementsByTagNameNS(XUL_NS, 'tabmodalprompt'); - return modals[0].ui; - }, - - /** - * Set the browser if the application is not B2G - * - * @param nsIDOMWindow win - * current window reference - */ - setBrowser: function BO_setBrowser(win) { - switch (appName) { - case "Firefox": - if (!isMulet()) { - this.browser = win.gBrowser; - } else { - // this is Mulet - appName = "B2G"; - } - break; - case "Fennec": - this.browser = win.BrowserApp; - break; - } - }, - - // The current frame id is managed per browser element on desktop in case - // the id needs to be refreshed. The currently selected window is identified - // within BrowserObject by a tab. - get curFrameId () { - if (appName != "Firefox") { - return this._curFrameId; - } - if (this.tab) { - let browser = this.browser.getBrowserForTab(this.tab); - return this.getIdForBrowser(browser); - } - return null; - }, - - set curFrameId (id) { - if (appName != "Firefox") { - this._curFrameId = id; - } - }, - - /** - * Called when we start a session with this browser. - */ - startSession: function BO_startSession(newSession, win, callback) { - callback(win, newSession); - }, - - /** - * Closes current tab - */ - closeTab: function BO_closeTab() { - if (this.browser && - this.browser.removeTab && - this.tab != null && (appName != "B2G")) { - this.browser.removeTab(this.tab); - } - }, - - /** - * Opens a tab with given uri - * - * @param string uri - * URI to open - */ - addTab: function BO_addTab(uri) { - return this.browser.addTab(uri, true); - }, - - /** - * Registers a new frame, and sets its current frame id to this frame - * if it is not already assigned, and if a) we already have a session - * or b) we're starting a new session and it is the right start frame. - * - * @param string uid - * frame uid for use by marionette - * @param the XUL that was the target of the originating message. - */ - register: function BO_register(uid, target) { - let remotenessChange = this.hasRemotenessChange(); - if (this.curFrameId === null || remotenessChange) { - if (this.browser) { - // If we're setting up a new session on Firefox, we only process the - // registration for this frame if it belongs to the current tab. - if (!this.tab) { - this.switchToTab(this.browser.selectedIndex); - } - - let browser = this.browser.getBrowserForTab(this.tab); - if (target == browser) { - this.updateIdForBrowser(browser, uid); - this.mainContentId = uid; - } - } else { - this._curFrameId = uid; - this.mainContentId = uid; - } - } - - this.knownFrames.push(uid); //used to delete sessions - return remotenessChange; - }, - - /** - * When navigating between pages results in changing a browser's process, we - * need to take measures not to lose contact with a listener script. This - * function does the necessary bookkeeping. - */ - hasRemotenessChange: function () { - // None of these checks are relevant on b2g or if we don't have a tab yet, - // and may not apply on Fennec. - if (appName != "Firefox" || this.tab === null) { - return false; - } - if (this._hasRemotenessChange) { - return true; - } - let currentIsRemote = this.browser.getBrowserForTab(this.tab).isRemoteBrowser; - this._hasRemotenessChange = this._browserWasRemote !== currentIsRemote; - this._browserWasRemote = currentIsRemote; - return this._hasRemotenessChange; - }, - - /** - * Flushes any pending commands queued when a remoteness change is being - * processed and mark this remotenessUpdate as complete. - */ - flushPendingCommands: function () { - if (!this._hasRemotenessChange) { - return; - } - - this._hasRemotenessChange = false; - this.pendingCommands.forEach((callback) => { - callback(); - }); - this.pendingCommands = []; - } - -} - -/** - * Marionette server -- this class holds a reference to a socket and creates - * MarionetteServerConnection objects as needed. - */ -this.MarionetteServer = function MarionetteServer(port, forceLocal) { - let flags = Ci.nsIServerSocket.KeepWhenOffline; - if (forceLocal) { - flags |= Ci.nsIServerSocket.LoopbackOnly; - } - let socket = new ServerSocket(port, flags, 0); - logger.info("Listening on port " + socket.port + "\n"); - socket.asyncListen(this); - this.listener = socket; - this.nextConnID = 0; - this.connections = {}; -}; - -MarionetteServer.prototype = { - onSocketAccepted: function(serverSocket, clientSocket) - { - logger.debug("accepted connection on " + clientSocket.host + ":" + clientSocket.port); - - let input = clientSocket.openInputStream(0, 0, 0); - let output = clientSocket.openOutputStream(0, 0, 0); - let aTransport = new DebuggerTransport(input, output); - let connID = "conn" + this.nextConnID++ + '.'; - let conn = new MarionetteServerConnection(connID, aTransport, this); - this.connections[connID] = conn; - - // Create a root actor for the connection and send the hello packet. - conn.sayHello(); - aTransport.ready(); - }, - - closeListener: function() { - this.listener.close(); - this.listener = null; - }, - - _connectionClosed: function DS_connectionClosed(aConnection) { - delete this.connections[aConnection.prefix]; - } -}; diff --git a/testing/marionette/marionette-simpletest.js b/testing/marionette/marionette-simpletest.js index 1e9b7a6980..7b77881692 100644 --- a/testing/marionette/marionette-simpletest.js +++ b/testing/marionette/marionette-simpletest.js @@ -1,12 +1,18 @@ /* 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/. */ + +let {utils: Cu} = Components; + +Cu.import("chrome://marionette/content/error.js"); + +this.EXPORTED_SYMBOLS = ["Marionette"]; + /* * The Marionette object, passed to the script context. */ - -this.Marionette = function Marionette(scope, window, context, logObj, timeout, - heartbeatCallback, testName) { +this.Marionette = function(scope, window, context, logObj, timeout, + heartbeatCallback, testName) { this.scope = scope; this.window = window; this.tests = []; @@ -19,12 +25,23 @@ this.Marionette = function Marionette(scope, window, context, logObj, timeout, this.TEST_UNEXPECTED_PASS = "TEST-UNEXPECTED-PASS"; this.TEST_PASS = "TEST-PASS"; this.TEST_KNOWN_FAIL = "TEST-KNOWN-FAIL"; -} +}; Marionette.prototype = { - exports: ['ok', 'is', 'isnot', 'todo', 'log', 'getLogs', 'generate_results', 'waitFor', - 'runEmulatorCmd', 'runEmulatorShell', 'TEST_PASS', 'TEST_KNOWN_FAIL', - 'TEST_UNEXPECTED_FAIL', 'TEST_UNEXPECTED_PASS'], + exports: [ + "ok", + "is", + "isnot", + "todo", + "log", + "getLogs", + "generate_results", + "waitFor", + "TEST_PASS", + "TEST_KNOWN_FAIL", + "TEST_UNEXPECTED_FAIL", + "TEST_UNEXPECTED_PASS" + ], addTest: function Marionette__addTest(condition, name, passString, failString, diag, state) { @@ -184,16 +201,4 @@ Marionette.prototype = { } this.window.setTimeout(this.waitFor.bind(this), 100, callback, test, deadline); }, - - runEmulatorCmd: function runEmulatorCmd(cmd, callback) { - this.heartbeatCallback(); - this.scope.runEmulatorCmd(cmd, callback); - }, - - runEmulatorShell: function runEmulatorShell(args, callback) { - this.heartbeatCallback(); - this.scope.runEmulatorShell(args, callback); - }, - }; - diff --git a/testing/marionette/server.js b/testing/marionette/server.js new file mode 100644 index 0000000000..82743bdc6e --- /dev/null +++ b/testing/marionette/server.js @@ -0,0 +1,171 @@ +/* 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 {Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu} = Components; + +const loader = Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader); +const ServerSocket = CC("@mozilla.org/network/server-socket;1", "nsIServerSocket", "initSpecialConnection"); + +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +Cu.import("chrome://marionette/content/dispatcher.js"); +Cu.import("chrome://marionette/content/driver.js"); +Cu.import("chrome://marionette/content/marionette-elements.js"); +Cu.import("chrome://marionette/content/marionette-simpletest.js"); + +// Bug 1083711: Load transport.js as an SDK module instead of subscript +loader.loadSubScript("resource://gre/modules/devtools/transport/transport.js"); + +// Preserve this import order: +let events = {}; +loader.loadSubScript("chrome://marionette/content/EventUtils.js", events); +loader.loadSubScript("chrome://marionette/content/ChromeUtils.js", events); +loader.loadSubScript("chrome://marionette/content/marionette-frame-manager.js"); + +const logger = Log.repository.getLogger("Marionette"); + +this.EXPORTED_SYMBOLS = ["MarionetteServer"]; +const SPECIAL_POWERS_PREF = "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer"; +const CONTENT_LISTENER_PREF = "marionette.contentListener"; + +/** + * Bootstraps Marionette and handles incoming client connections. + * + * Once started, it opens a TCP socket sporting the debugger transport + * protocol on the provided port. For every new client a Dispatcher is + * created. + * + * @param {number} port + * Port for server to listen to. + * @param {boolean} forceLocal + * Listen only to connections from loopback if true. If false, + * accept all connections. + */ +this.MarionetteServer = function(port, forceLocal) { + this.port = port; + this.forceLocal = forceLocal; + this.conns = {}; + this.nextConnId = 0; + this.alive = false; +}; + +/** + * Initialises the Marionette server by loading in the special powers + * which is required to provide some automation-only features. + */ +MarionetteServer.prototype.init = function() { + // SpecialPowers requires insecure automation-only features that we put behind a pref + Services.prefs.setBoolPref(SPECIAL_POWERS_PREF, true); + let specialpowers = {}; + loader.loadSubScript( + "chrome://specialpowers/content/SpecialPowersObserver.js", specialpowers); + specialpowers.specialPowersObserver = new specialpowers.SpecialPowersObserver(); + specialpowers.specialPowersObserver.init(); +}; + +/** + * Function that takes an Emulator and produces a GeckoDriver. + * + * Determines application name and device type to initialise the driver + * with. Also bypasses offline status if the device is a qemu or panda + * type device. + * + * @return {GeckoDriver} + * A driver instance. + */ +MarionetteServer.prototype.driverFactory = function(emulator) { + let appName = isMulet() ? "B2G" : Services.appinfo.name; + let qemu = "0"; + let device = null; + let bypassOffline = false; + + try { + Cu.import("resource://gre/modules/systemlibs.js"); + qemu = libcutils.property_get("ro.kernel.qemu"); + logger.debug("B2G emulator: " + (qemu == "1" ? "yes" : "no")); + device = libcutils.property_get("ro.product.device"); + logger.debug("Device detected is " + device); + bypassOffline = (qemu == "1" || device == "panda"); + } catch (e) {} + + if (qemu == "1") { + device = "qemu"; + } + if (!device) { + device = "desktop"; + } + + Services.prefs.setBoolPref(CONTENT_LISTENER_PREF, false); + + if (bypassOffline) { + logger.debug("Bypassing offline status"); + Services.prefs.setBoolPref("network.gonk.manage-offline-status", false); + Services.io.manageOfflineStatus = false; + Services.io.offline = false; + } + + return new GeckoDriver(appName, device, emulator); +}; + +MarionetteServer.prototype.start = function() { + if (this.alive) { + return; + } + this.init(); + let flags = Ci.nsIServerSocket.KeepWhenOffline; + if (this.forceLocal) { + flags |= Ci.nsIServerSocket.LoopbackOnly; + } + this.listener = new ServerSocket(this.port, flags, 0); + this.listener.asyncListen(this); + this.alive = true; +}; + +MarionetteServer.prototype.stop = function() { + if (!this.alive) { + return; + } + this.closeListener(); + this.alive = false; +}; + +MarionetteServer.prototype.closeListener = function() { + this.listener.close(); + this.listener = null; +}; + +MarionetteServer.prototype.onSocketAccepted = function( + serverSocket, clientSocket) { + let input = clientSocket.openInputStream(0, 0, 0); + let output = clientSocket.openOutputStream(0, 0, 0); + let transport = new DebuggerTransport(input, output); + let connId = "conn" + this.nextConnId++; + + let dispatcher = new Dispatcher(connId, transport, this.driverFactory); + dispatcher.onclose = this.onConnectionClosed.bind(this); + this.conns[connId] = dispatcher; + + logger.info(`Accepted connection ${connId} from ${clientSocket.host}:${clientSocket.port}`); + + // Create a root actor for the connection and send the hello packet + dispatcher.sayHello(); + transport.ready(); +}; + +MarionetteServer.prototype.onConnectionClosed = function(conn) { + let id = conn.id; + delete this.conns[id]; + logger.info(`Closed connection ${id}`); +}; + +function isMulet() { + try { + return Services.prefs.getBoolPref("b2g.is_mulet"); + } catch (e) { + return false; + } +} diff --git a/testing/specialpowers/content/SpecialPowersObserverAPI.js b/testing/specialpowers/content/SpecialPowersObserverAPI.js index 12536e44b5..4dba50f95a 100644 --- a/testing/specialpowers/content/SpecialPowersObserverAPI.js +++ b/testing/specialpowers/content/SpecialPowersObserverAPI.js @@ -1,6 +1,7 @@ /* 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"; Components.utils.import("resource://gre/modules/Services.jsm"); @@ -13,18 +14,16 @@ if (typeof(Cc) == 'undefined') { var Cc = Components.classes; } -/** - * Special Powers Exception - used to throw exceptions nicely - **/ -this.SpecialPowersException = function SpecialPowersException(aMsg) { +this.SpecialPowersError = function(aMsg) { + Error.call(this); + let {stack} = new Error(); this.message = aMsg; - this.name = "SpecialPowersException"; + this.name = "SpecialPowersError"; } +SpecialPowersError.prototype = Object.create(Error.prototype); -SpecialPowersException.prototype = { - toString: function SPE_toString() { - return this.name + ': "' + this.message + '"'; - } +SpecialPowersError.prototype.toString = function() { + return `${this.name}: ${this.message}`; }; this.SpecialPowersObserverAPI = function SpecialPowersObserverAPI() { @@ -221,7 +220,7 @@ SpecialPowersObserverAPI.prototype = { } if (status == 404) { - throw new SpecialPowersException( + throw new SpecialPowersError( "Error while executing chrome script '" + aUrl + "':\n" + "The script doesn't exists. Ensure you have registered it in " + "'support-files' in your mochitest.ini."); @@ -247,19 +246,19 @@ SpecialPowersObserverAPI.prototype = { if (aMessage.json.op == "get") { if (!prefName || !prefType) - throw new SpecialPowersException("Invalid parameters for get in SPPrefService"); + throw new SpecialPowersError("Invalid parameters for get in SPPrefService"); // return null if the pref doesn't exist if (prefs.getPrefType(prefName) == prefs.PREF_INVALID) return null; } else if (aMessage.json.op == "set") { if (!prefName || !prefType || prefValue === null) - throw new SpecialPowersException("Invalid parameters for set in SPPrefService"); + throw new SpecialPowersError("Invalid parameters for set in SPPrefService"); } else if (aMessage.json.op == "clear") { if (!prefName) - throw new SpecialPowersException("Invalid parameters for clear in SPPrefService"); + throw new SpecialPowersError("Invalid parameters for clear in SPPrefService"); } else { - throw new SpecialPowersException("Invalid operation for SPPrefService"); + throw new SpecialPowersError("Invalid operation for SPPrefService"); } // Now we make the call @@ -306,7 +305,7 @@ SpecialPowersObserverAPI.prototype = { case "find-crash-dump-files": return this._findCrashDumpFiles(aMessage.json.crashDumpFilesToIgnore); default: - throw new SpecialPowersException("Invalid operation for SPProcessCrashService"); + throw new SpecialPowersError("Invalid operation for SPProcessCrashService"); } return undefined; // See comment at the beginning of this function. } @@ -338,8 +337,8 @@ SpecialPowersObserverAPI.prototype = { return false; break; default: - throw new SpecialPowersException("Invalid operation for " + - "SPPermissionManager"); + throw new SpecialPowersError( + "Invalid operation for SPPermissionManager"); } return undefined; // See comment at the beginning of this function. } @@ -404,7 +403,7 @@ SpecialPowersObserverAPI.prototype = { return true; } default: - throw new SpecialPowersException("Invalid operation for SPWebAppsService"); + throw new SpecialPowersError("Invalid operation for SPWebAppsService"); } return undefined; // See comment at the beginning of this function. } @@ -417,7 +416,7 @@ SpecialPowersObserverAPI.prototype = { Services.obs.notifyObservers(null, topic, data); break; default: - throw new SpecialPowersException("Invalid operation for SPObserverervice"); + throw new SpecialPowersError("Invalid operation for SPObserverervice"); } return undefined; // See comment at the beginning of this function. } @@ -470,9 +469,10 @@ SpecialPowersObserverAPI.prototype = { try { Components.utils.evalInSandbox(jsScript, sb, "1.8", url, 1); } catch(e) { - throw new SpecialPowersException("Error while executing chrome " + - "script '" + url + "':\n" + e + "\n" + - e.fileName + ":" + e.lineNumber); + throw new SpecialPowersError( + "Error while executing chrome script '" + url + "':\n" + + e + "\n" + + e.fileName + ":" + e.lineNumber); } return undefined; // See comment at the beginning of this function. } @@ -497,8 +497,8 @@ SpecialPowersObserverAPI.prototype = { let msg = aMessage.data; let op = msg.op; - if (op != 'clear' && op != 'getUsage') { - throw new SpecialPowersException('Invalid operation for SPQuotaManager'); + if (op != 'clear' && op != 'getUsage' && op != 'reset') { + throw new SpecialPowersError('Invalid operation for SPQuotaManager'); } let uri = this._getURI(msg.uri); @@ -511,6 +511,8 @@ SpecialPowersObserverAPI.prototype = { } else { qm.clearStoragesForURI(uri); } + } else if (op == 'reset') { + qm.reset(); } // We always use the getUsageForURI callback even if we're clearing @@ -548,13 +550,12 @@ SpecialPowersObserverAPI.prototype = { } default: - throw new SpecialPowersException("Unrecognized Special Powers API"); + throw new SpecialPowersError("Unrecognized Special Powers API"); } // We throw an exception before reaching this explicit return because // we should never be arriving here anyway. - throw new SpecialPowersException("Unreached code"); + throw new SpecialPowersError("Unreached code"); return undefined; } }; - diff --git a/testing/specialpowers/content/specialpowersAPI.js b/testing/specialpowers/content/specialpowersAPI.js index b000c190de..b3a51d2a34 100644 --- a/testing/specialpowers/content/specialpowersAPI.js +++ b/testing/specialpowers/content/specialpowersAPI.js @@ -1889,6 +1889,13 @@ SpecialPowersAPI.prototype = { this._quotaManagerRequest('getUsage', uri, appId, inBrowser, callback); }, + // Technically this restarts the QuotaManager for all URIs, but we need + // a specific one to perform the synchronized callback when the reset is + // complete. + resetStorageForURI: function(uri, callback, appId, inBrowser) { + this._quotaManagerRequest('reset', uri, appId, inBrowser, callback); + }, + _quotaManagerRequest: function(op, uri, appId, inBrowser, callback) { const messageTopic = "SPQuotaManager";