Backed out changeset 06f41c22f3a6 (bug 1888460) for causing linux xpcshell failures...
[gecko.git] / dom / serviceworkers / ServiceWorkerUpdateJob.cpp
bloba6726fbf47f2e4d2a9116212b618c3c6b4e00257
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "ServiceWorkerUpdateJob.h"
9 #include "mozilla/Telemetry.h"
10 #include "nsIScriptError.h"
11 #include "nsIURL.h"
12 #include "nsNetUtil.h"
13 #include "nsProxyRelease.h"
14 #include "ServiceWorkerManager.h"
15 #include "ServiceWorkerPrivate.h"
16 #include "ServiceWorkerRegistrationInfo.h"
17 #include "ServiceWorkerScriptCache.h"
18 #include "mozilla/dom/WorkerCommon.h"
20 namespace mozilla::dom {
22 using serviceWorkerScriptCache::OnFailure;
24 namespace {
26 /**
27 * The spec mandates slightly different behaviors for computing the scope
28 * prefix string in case a Service-Worker-Allowed header is specified versus
29 * when it's not available.
31 * With the header:
32 * "Set maxScopeString to "/" concatenated with the strings in maxScope's
33 * path (including empty strings), separated from each other by "/"."
34 * Without the header:
35 * "Set maxScopeString to "/" concatenated with the strings, except the last
36 * string that denotes the script's file name, in registration's registering
37 * script url's path (including empty strings), separated from each other by
38 * "/"."
40 * In simpler terms, if the header is not present, we should only use the
41 * "directory" part of the pathname, and otherwise the entire pathname should be
42 * used. ScopeStringPrefixMode allows the caller to specify the desired
43 * behavior.
45 enum ScopeStringPrefixMode { eUseDirectory, eUsePath };
47 nsresult GetRequiredScopeStringPrefix(nsIURI* aScriptURI, nsACString& aPrefix,
48 ScopeStringPrefixMode aPrefixMode) {
49 nsresult rv;
50 if (aPrefixMode == eUseDirectory) {
51 nsCOMPtr<nsIURL> scriptURL(do_QueryInterface(aScriptURI));
52 if (NS_WARN_IF(!scriptURL)) {
53 return NS_ERROR_FAILURE;
56 rv = scriptURL->GetDirectory(aPrefix);
57 if (NS_WARN_IF(NS_FAILED(rv))) {
58 return rv;
60 } else if (aPrefixMode == eUsePath) {
61 rv = aScriptURI->GetPathQueryRef(aPrefix);
62 if (NS_WARN_IF(NS_FAILED(rv))) {
63 return rv;
65 } else {
66 MOZ_ASSERT_UNREACHABLE("Invalid value for aPrefixMode");
68 return NS_OK;
71 } // anonymous namespace
73 class ServiceWorkerUpdateJob::CompareCallback final
74 : public serviceWorkerScriptCache::CompareCallback {
75 RefPtr<ServiceWorkerUpdateJob> mJob;
77 ~CompareCallback() = default;
79 public:
80 explicit CompareCallback(ServiceWorkerUpdateJob* aJob) : mJob(aJob) {
81 MOZ_ASSERT(mJob);
84 virtual void ComparisonResult(nsresult aStatus, bool aInCacheAndEqual,
85 OnFailure aOnFailure,
86 const nsAString& aNewCacheName,
87 const nsACString& aMaxScope,
88 nsLoadFlags aLoadFlags) override {
89 mJob->ComparisonResult(aStatus, aInCacheAndEqual, aOnFailure, aNewCacheName,
90 aMaxScope, aLoadFlags);
93 NS_INLINE_DECL_REFCOUNTING(ServiceWorkerUpdateJob::CompareCallback, override)
96 class ServiceWorkerUpdateJob::ContinueUpdateRunnable final
97 : public LifeCycleEventCallback {
98 nsMainThreadPtrHandle<ServiceWorkerUpdateJob> mJob;
99 bool mSuccess;
101 public:
102 explicit ContinueUpdateRunnable(
103 const nsMainThreadPtrHandle<ServiceWorkerUpdateJob>& aJob)
104 : mJob(aJob), mSuccess(false) {
105 MOZ_ASSERT(NS_IsMainThread());
108 void SetResult(bool aResult) override { mSuccess = aResult; }
110 NS_IMETHOD
111 Run() override {
112 MOZ_ASSERT(NS_IsMainThread());
113 mJob->ContinueUpdateAfterScriptEval(mSuccess);
114 mJob = nullptr;
115 return NS_OK;
119 class ServiceWorkerUpdateJob::ContinueInstallRunnable final
120 : public LifeCycleEventCallback {
121 nsMainThreadPtrHandle<ServiceWorkerUpdateJob> mJob;
122 bool mSuccess;
124 public:
125 explicit ContinueInstallRunnable(
126 const nsMainThreadPtrHandle<ServiceWorkerUpdateJob>& aJob)
127 : mJob(aJob), mSuccess(false) {
128 MOZ_ASSERT(NS_IsMainThread());
131 void SetResult(bool aResult) override { mSuccess = aResult; }
133 NS_IMETHOD
134 Run() override {
135 MOZ_ASSERT(NS_IsMainThread());
136 mJob->ContinueAfterInstallEvent(mSuccess);
137 mJob = nullptr;
138 return NS_OK;
142 ServiceWorkerUpdateJob::ServiceWorkerUpdateJob(
143 nsIPrincipal* aPrincipal, const nsACString& aScope, nsCString aScriptSpec,
144 ServiceWorkerUpdateViaCache aUpdateViaCache)
145 : ServiceWorkerUpdateJob(Type::Update, aPrincipal, aScope,
146 std::move(aScriptSpec), aUpdateViaCache) {}
148 already_AddRefed<ServiceWorkerRegistrationInfo>
149 ServiceWorkerUpdateJob::GetRegistration() const {
150 MOZ_ASSERT(NS_IsMainThread());
151 RefPtr<ServiceWorkerRegistrationInfo> ref = mRegistration;
152 return ref.forget();
155 ServiceWorkerUpdateJob::ServiceWorkerUpdateJob(
156 Type aType, nsIPrincipal* aPrincipal, const nsACString& aScope,
157 nsCString aScriptSpec, ServiceWorkerUpdateViaCache aUpdateViaCache)
158 : ServiceWorkerJob(aType, aPrincipal, aScope, std::move(aScriptSpec)),
159 mUpdateViaCache(aUpdateViaCache),
160 mOnFailure(serviceWorkerScriptCache::OnFailure::DoNothing) {}
162 ServiceWorkerUpdateJob::~ServiceWorkerUpdateJob() = default;
164 void ServiceWorkerUpdateJob::FailUpdateJob(ErrorResult& aRv) {
165 MOZ_ASSERT(NS_IsMainThread());
166 MOZ_ASSERT(aRv.Failed());
168 // Cleanup after a failed installation. This essentially implements
169 // step 13 of the Install algorithm.
171 // https://w3c.github.io/ServiceWorker/#installation-algorithm
173 // The spec currently only runs this after an install event fails,
174 // but we must handle many more internal errors. So we check for
175 // cleanup on every non-successful exit.
176 if (mRegistration) {
177 // Some kinds of failures indicate there is something broken in the
178 // currently installed registration. In these cases we want to fully
179 // unregister.
180 if (mOnFailure == OnFailure::Uninstall) {
181 mRegistration->ClearAsCorrupt();
184 // Otherwise just clear the workers we may have created as part of the
185 // update process.
186 else {
187 mRegistration->ClearEvaluating();
188 mRegistration->ClearInstalling();
191 RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
192 if (swm) {
193 swm->MaybeRemoveRegistration(mRegistration);
195 // Also clear the registration on disk if we are forcing uninstall
196 // due to a particularly bad failure.
197 if (mOnFailure == OnFailure::Uninstall) {
198 swm->MaybeSendUnregister(mRegistration->Principal(),
199 mRegistration->Scope());
204 mRegistration = nullptr;
206 Finish(aRv);
209 void ServiceWorkerUpdateJob::FailUpdateJob(nsresult aRv) {
210 ErrorResult rv(aRv);
211 FailUpdateJob(rv);
214 void ServiceWorkerUpdateJob::AsyncExecute() {
215 MOZ_ASSERT(NS_IsMainThread());
216 MOZ_ASSERT(GetType() == Type::Update);
218 RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
219 if (Canceled() || !swm) {
220 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
221 return;
224 // Invoke Update algorithm:
225 // https://w3c.github.io/ServiceWorker/#update-algorithm
227 // "Let registration be the result of running the Get Registration algorithm
228 // passing job’s scope url as the argument."
229 RefPtr<ServiceWorkerRegistrationInfo> registration =
230 swm->GetRegistration(mPrincipal, mScope);
232 if (!registration) {
233 ErrorResult rv;
234 rv.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(mScope, "uninstalled");
235 FailUpdateJob(rv);
236 return;
239 // "Let newestWorker be the result of running Get Newest Worker algorithm
240 // passing registration as the argument."
241 RefPtr<ServiceWorkerInfo> newest = registration->Newest();
243 // "If job’s job type is update, and newestWorker is not null and its script
244 // url does not equal job’s script url, then:
245 // 1. Invoke Reject Job Promise with job and TypeError.
246 // 2. Invoke Finish Job with job and abort these steps."
247 if (newest && !newest->ScriptSpec().Equals(mScriptSpec)) {
248 ErrorResult rv;
249 rv.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(mScope, "changed");
250 FailUpdateJob(rv);
251 return;
254 SetRegistration(registration);
255 Update();
258 void ServiceWorkerUpdateJob::SetRegistration(
259 ServiceWorkerRegistrationInfo* aRegistration) {
260 MOZ_ASSERT(NS_IsMainThread());
262 MOZ_ASSERT(!mRegistration);
263 MOZ_ASSERT(aRegistration);
264 mRegistration = aRegistration;
267 void ServiceWorkerUpdateJob::Update() {
268 MOZ_ASSERT(NS_IsMainThread());
269 MOZ_ASSERT(!Canceled());
271 // SetRegistration() must be called before Update().
272 MOZ_ASSERT(mRegistration);
273 MOZ_ASSERT(!mRegistration->GetInstalling());
275 // Begin the script download and comparison steps starting at step 5
276 // of the Update algorithm.
278 RefPtr<ServiceWorkerInfo> workerInfo = mRegistration->Newest();
279 nsAutoString cacheName;
281 // If the script has not changed, we need to perform a byte-for-byte
282 // comparison.
283 if (workerInfo && workerInfo->ScriptSpec().Equals(mScriptSpec)) {
284 cacheName = workerInfo->CacheName();
287 RefPtr<CompareCallback> callback = new CompareCallback(this);
289 nsresult rv = serviceWorkerScriptCache::Compare(
290 mRegistration, mPrincipal, cacheName, NS_ConvertUTF8toUTF16(mScriptSpec),
291 callback);
292 if (NS_WARN_IF(NS_FAILED(rv))) {
293 FailUpdateJob(rv);
294 return;
298 ServiceWorkerUpdateViaCache ServiceWorkerUpdateJob::GetUpdateViaCache() const {
299 return mUpdateViaCache;
302 void ServiceWorkerUpdateJob::ComparisonResult(nsresult aStatus,
303 bool aInCacheAndEqual,
304 OnFailure aOnFailure,
305 const nsAString& aNewCacheName,
306 const nsACString& aMaxScope,
307 nsLoadFlags aLoadFlags) {
308 MOZ_ASSERT(NS_IsMainThread());
310 mOnFailure = aOnFailure;
312 RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
313 if (NS_WARN_IF(Canceled() || !swm)) {
314 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
315 return;
318 // Handle failure of the download or comparison. This is part of Update
319 // step 5 as "If the algorithm asynchronously completes with null, then:".
320 if (NS_WARN_IF(NS_FAILED(aStatus))) {
321 FailUpdateJob(aStatus);
322 return;
325 // The spec validates the response before performing the byte-for-byte check.
326 // Here we perform the comparison in another module and then validate the
327 // script URL and scope. Make sure to do this validation before accepting
328 // an byte-for-byte match since the service-worker-allowed header might have
329 // changed since the last time it was installed.
331 // This is step 2 the "validate response" section of Update algorithm step 5.
332 // Step 1 is performed in the serviceWorkerScriptCache code.
334 nsCOMPtr<nsIURI> scriptURI;
335 nsresult rv = NS_NewURI(getter_AddRefs(scriptURI), mScriptSpec);
336 if (NS_WARN_IF(NS_FAILED(rv))) {
337 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
338 return;
341 nsCOMPtr<nsIURI> maxScopeURI;
342 if (!aMaxScope.IsEmpty()) {
343 rv = NS_NewURI(getter_AddRefs(maxScopeURI), aMaxScope, nullptr, scriptURI);
344 if (NS_WARN_IF(NS_FAILED(rv))) {
345 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
346 return;
350 nsAutoCString defaultAllowedPrefix;
351 rv = GetRequiredScopeStringPrefix(scriptURI, defaultAllowedPrefix,
352 eUseDirectory);
353 if (NS_WARN_IF(NS_FAILED(rv))) {
354 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
355 return;
358 nsAutoCString maxPrefix(defaultAllowedPrefix);
359 if (maxScopeURI) {
360 rv = GetRequiredScopeStringPrefix(maxScopeURI, maxPrefix, eUsePath);
361 if (NS_WARN_IF(NS_FAILED(rv))) {
362 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
363 return;
367 nsCOMPtr<nsIURI> scopeURI;
368 rv = NS_NewURI(getter_AddRefs(scopeURI), mRegistration->Scope(), nullptr,
369 scriptURI);
370 if (NS_WARN_IF(NS_FAILED(rv))) {
371 FailUpdateJob(NS_ERROR_FAILURE);
372 return;
375 nsAutoCString scopeString;
376 rv = scopeURI->GetPathQueryRef(scopeString);
377 if (NS_WARN_IF(NS_FAILED(rv))) {
378 FailUpdateJob(NS_ERROR_FAILURE);
379 return;
382 if (!StringBeginsWith(scopeString, maxPrefix)) {
383 nsAutoString message;
384 NS_ConvertUTF8toUTF16 reportScope(mRegistration->Scope());
385 NS_ConvertUTF8toUTF16 reportMaxPrefix(maxPrefix);
387 rv = nsContentUtils::FormatLocalizedString(
388 message, nsContentUtils::eDOM_PROPERTIES,
389 "ServiceWorkerScopePathMismatch", reportScope, reportMaxPrefix);
390 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to format localized string");
391 swm->ReportToAllClients(mScope, message, u""_ns, u""_ns, 0, 0,
392 nsIScriptError::errorFlag);
393 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
394 return;
397 // The response has been validated, so now we can consider if its a
398 // byte-for-byte match. This is step 6 of the Update algorithm.
399 if (aInCacheAndEqual) {
400 Finish(NS_OK);
401 return;
404 Telemetry::Accumulate(Telemetry::SERVICE_WORKER_UPDATED, 1);
406 // Begin step 7 of the Update algorithm to evaluate the new script.
407 nsLoadFlags flags = aLoadFlags;
408 if (GetUpdateViaCache() == ServiceWorkerUpdateViaCache::None) {
409 flags |= nsIRequest::VALIDATE_ALWAYS;
412 RefPtr<ServiceWorkerInfo> sw = new ServiceWorkerInfo(
413 mRegistration->Principal(), mRegistration->Scope(), mRegistration->Id(),
414 mRegistration->Version(), mScriptSpec, aNewCacheName, flags);
416 // If the registration is corrupt enough to force an uninstall if the
417 // upgrade fails, then we want to make sure the upgrade takes effect
418 // if it succeeds. Therefore force the skip-waiting flag on to replace
419 // the broken worker after install.
420 if (aOnFailure == OnFailure::Uninstall) {
421 sw->SetSkipWaitingFlag();
424 mRegistration->SetEvaluating(sw);
426 nsMainThreadPtrHandle<ServiceWorkerUpdateJob> handle(
427 new nsMainThreadPtrHolder<ServiceWorkerUpdateJob>(
428 "ServiceWorkerUpdateJob", this));
429 RefPtr<LifeCycleEventCallback> callback = new ContinueUpdateRunnable(handle);
431 ServiceWorkerPrivate* workerPrivate = sw->WorkerPrivate();
432 MOZ_ASSERT(workerPrivate);
433 rv = workerPrivate->CheckScriptEvaluation(callback);
435 if (NS_WARN_IF(NS_FAILED(rv))) {
436 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
437 return;
441 void ServiceWorkerUpdateJob::ContinueUpdateAfterScriptEval(
442 bool aScriptEvaluationResult) {
443 MOZ_ASSERT(NS_IsMainThread());
445 RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
446 if (Canceled() || !swm) {
447 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
448 return;
451 // Step 7.5 of the Update algorithm verifying that the script evaluated
452 // successfully.
454 if (NS_WARN_IF(!aScriptEvaluationResult)) {
455 ErrorResult error;
456 error.ThrowTypeError<MSG_SW_SCRIPT_THREW>(mScriptSpec,
457 mRegistration->Scope());
458 FailUpdateJob(error);
459 return;
462 Install();
465 void ServiceWorkerUpdateJob::Install() {
466 MOZ_ASSERT(NS_IsMainThread());
467 MOZ_DIAGNOSTIC_ASSERT(!Canceled());
469 MOZ_ASSERT(!mRegistration->GetInstalling());
471 // Begin step 2 of the Install algorithm.
473 // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#installation-algorithm
475 mRegistration->TransitionEvaluatingToInstalling();
477 // Step 6 of the Install algorithm resolving the job promise.
478 InvokeResultCallbacks(NS_OK);
480 // Queue a task to fire an event named updatefound at all the
481 // ServiceWorkerRegistration.
482 mRegistration->FireUpdateFound();
484 nsMainThreadPtrHandle<ServiceWorkerUpdateJob> handle(
485 new nsMainThreadPtrHolder<ServiceWorkerUpdateJob>(
486 "ServiceWorkerUpdateJob", this));
487 RefPtr<LifeCycleEventCallback> callback = new ContinueInstallRunnable(handle);
489 // Send the install event to the worker thread
490 ServiceWorkerPrivate* workerPrivate =
491 mRegistration->GetInstalling()->WorkerPrivate();
492 nsresult rv = workerPrivate->SendLifeCycleEvent(u"install"_ns, callback);
493 if (NS_WARN_IF(NS_FAILED(rv))) {
494 ContinueAfterInstallEvent(false /* aSuccess */);
498 void ServiceWorkerUpdateJob::ContinueAfterInstallEvent(
499 bool aInstallEventSuccess) {
500 if (Canceled()) {
501 return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
504 // If we haven't been canceled we should have a registration. There appears
505 // to be a path where it gets cleared before we call into here. Assert
506 // to try to catch this condition, but don't crash in release.
507 MOZ_DIAGNOSTIC_ASSERT(mRegistration);
508 if (!mRegistration) {
509 return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
512 // Continue executing the Install algorithm at step 12.
514 // "If installFailed is true"
515 if (NS_WARN_IF(!aInstallEventSuccess)) {
516 // The installing worker is cleaned up by FailUpdateJob().
517 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
518 return;
521 // Abort the update Job if the installWorker is null (e.g. when an extension
522 // is shutting down and all its workers have been terminated).
523 if (!mRegistration->GetInstalling()) {
524 return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
527 mRegistration->TransitionInstallingToWaiting();
529 Finish(NS_OK);
531 // Step 20 calls for explicitly waiting for queued event tasks to fire.
532 // Instead, we simply queue a runnable to execute Activate. This ensures the
533 // events are flushed from the queue before proceeding.
535 // Step 22 of the Install algorithm. Activate is executed after the
536 // completion of this job. The controlling client and skipWaiting checks are
537 // performed in TryToActivate().
538 mRegistration->TryToActivateAsync();
541 } // namespace mozilla::dom