Implement "UpdateWorkerState" and use it
[WebKit-https.git] / Source / WebCore / workers / service / ServiceWorkerContainer.cpp
1 /*
2  * Copyright (C) 2017 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27 #include "ServiceWorkerContainer.h"
28
29 #if ENABLE(SERVICE_WORKER)
30
31 #include "EventNames.h"
32 #include "Exception.h"
33 #include "IDLTypes.h"
34 #include "JSDOMPromiseDeferred.h"
35 #include "JSServiceWorkerRegistration.h"
36 #include "Logging.h"
37 #include "Microtasks.h"
38 #include "NavigatorBase.h"
39 #include "ResourceError.h"
40 #include "ScriptExecutionContext.h"
41 #include "SecurityOrigin.h"
42 #include "ServiceWorker.h"
43 #include "ServiceWorkerJob.h"
44 #include "ServiceWorkerJobData.h"
45 #include "ServiceWorkerProvider.h"
46 #include "URL.h"
47 #include <wtf/RunLoop.h>
48 #include <wtf/Scope.h>
49
50 namespace WebCore {
51
52 ServiceWorkerContainer::ServiceWorkerContainer(ScriptExecutionContext& context, NavigatorBase& navigator)
53     : ActiveDOMObject(&context)
54     , m_navigator(navigator)
55 {
56     suspendIfNeeded();
57
58     m_readyPromise.reject(Exception { UnknownError, ASCIILiteral("serviceWorker.ready() is not yet implemented") });
59 }
60
61 ServiceWorkerContainer::~ServiceWorkerContainer()
62 {
63 #ifndef NDEBUG
64     ASSERT(m_creationThread == currentThread());
65 #endif
66 }
67
68 void ServiceWorkerContainer::refEventTarget()
69 {
70     m_navigator.ref();
71 }
72
73 void ServiceWorkerContainer::derefEventTarget()
74 {
75     m_navigator.deref();
76 }
77
78 ServiceWorker* ServiceWorkerContainer::controller() const
79 {
80     auto* context = scriptExecutionContext();
81     return context ? context->activeServiceWorker() : nullptr;
82 }
83
84 void ServiceWorkerContainer::addRegistration(const String& relativeScriptURL, const RegistrationOptions& options, Ref<DeferredPromise>&& promise)
85 {
86     auto* context = scriptExecutionContext();
87     if (!context || !context->sessionID().isValid()) {
88         ASSERT_NOT_REACHED();
89         promise->reject(Exception(InvalidStateError));
90         return;
91     }
92
93     if (relativeScriptURL.isEmpty()) {
94         promise->reject(Exception { TypeError, ASCIILiteral("serviceWorker.register() cannot be called with an empty script URL") });
95         return;
96     }
97
98     if (!m_swConnection)
99         m_swConnection = &ServiceWorkerProvider::singleton().serviceWorkerConnectionForSession(scriptExecutionContext()->sessionID());
100
101     ServiceWorkerJobData jobData(m_swConnection->identifier());
102
103     jobData.scriptURL = context->completeURL(relativeScriptURL);
104     if (!jobData.scriptURL.isValid()) {
105         promise->reject(Exception { TypeError, ASCIILiteral("serviceWorker.register() must be called with a valid relative script URL") });
106         return;
107     }
108
109     // FIXME: The spec disallows scripts outside of HTTP(S), but we'll likely support app custom URL schemes in WebKit.
110     if (!jobData.scriptURL.protocolIsInHTTPFamily()) {
111         promise->reject(Exception { TypeError, ASCIILiteral("serviceWorker.register() must be called with a script URL whose protocol is either HTTP or HTTPS") });
112         return;
113     }
114
115     String path = jobData.scriptURL.path();
116     if (path.containsIgnoringASCIICase("%2f") || path.containsIgnoringASCIICase("%5c")) {
117         promise->reject(Exception { TypeError, ASCIILiteral("serviceWorker.register() must be called with a script URL whose path does not contain '%2f' or '%5c'") });
118         return;
119     }
120
121     String scope = options.scope.isEmpty() ? ASCIILiteral("./") : options.scope;
122     if (!scope.isEmpty())
123         jobData.scopeURL = context->completeURL(scope);
124
125     if (!jobData.scopeURL.isNull() && !jobData.scopeURL.protocolIsInHTTPFamily()) {
126         promise->reject(Exception { TypeError, ASCIILiteral("Scope URL provided to serviceWorker.register() must be either HTTP or HTTPS") });
127         return;
128     }
129
130     path = jobData.scopeURL.path();
131     if (path.containsIgnoringASCIICase("%2f") || path.containsIgnoringASCIICase("%5c")) {
132         promise->reject(Exception { TypeError, ASCIILiteral("Scope URL provided to serviceWorker.register() cannot have a path that contains '%2f' or '%5c'") });
133         return;
134     }
135
136     jobData.clientCreationURL = context->url();
137     jobData.topOrigin = SecurityOriginData::fromSecurityOrigin(context->topOrigin());
138     jobData.type = ServiceWorkerJobType::Register;
139     jobData.registrationOptions = options;
140
141     scheduleJob(ServiceWorkerJob::create(*this, WTFMove(promise), WTFMove(jobData)));
142 }
143
144 void ServiceWorkerContainer::removeRegistration(const URL& scopeURL, Ref<DeferredPromise>&& promise)
145 {
146     auto* context = scriptExecutionContext();
147     if (!context || !context->sessionID().isValid()) {
148         ASSERT_NOT_REACHED();
149         promise->reject(Exception(InvalidStateError));
150         return;
151     }
152
153     if (!m_swConnection) {
154         ASSERT_NOT_REACHED();
155         promise->reject(Exception(InvalidStateError));
156         return;
157     }
158
159     ServiceWorkerJobData jobData(m_swConnection->identifier());
160     jobData.clientCreationURL = context->url();
161     jobData.topOrigin = SecurityOriginData::fromSecurityOrigin(context->topOrigin());
162     jobData.type = ServiceWorkerJobType::Unregister;
163     jobData.scopeURL = scopeURL;
164
165     scheduleJob(ServiceWorkerJob::create(*this, WTFMove(promise), WTFMove(jobData)));
166 }
167
168 void ServiceWorkerContainer::updateRegistration(const URL& scopeURL, const URL& scriptURL, WorkerType, Ref<DeferredPromise>&& promise)
169 {
170     auto* context = scriptExecutionContext();
171     if (!context || !context->sessionID().isValid()) {
172         ASSERT_NOT_REACHED();
173         promise->reject(Exception(InvalidStateError));
174         return;
175     }
176
177     if (!m_swConnection) {
178         ASSERT_NOT_REACHED();
179         promise->reject(Exception(InvalidStateError));
180         return;
181     }
182
183     ServiceWorkerJobData jobData(m_swConnection->identifier());
184     jobData.clientCreationURL = context->url();
185     jobData.topOrigin = SecurityOriginData::fromSecurityOrigin(context->topOrigin());
186     jobData.type = ServiceWorkerJobType::Update;
187     jobData.scopeURL = scopeURL;
188     jobData.scriptURL = scriptURL;
189
190     scheduleJob(ServiceWorkerJob::create(*this, WTFMove(promise), WTFMove(jobData)));
191 }
192
193 void ServiceWorkerContainer::scheduleJob(Ref<ServiceWorkerJob>&& job)
194 {
195     ASSERT(m_swConnection);
196
197     setPendingActivity(this);
198
199     ServiceWorkerJob& rawJob = job.get();
200     auto result = m_jobMap.add(rawJob.data().identifier(), WTFMove(job));
201     ASSERT_UNUSED(result, result.isNewEntry);
202
203     m_swConnection->scheduleJob(rawJob);
204 }
205
206 void ServiceWorkerContainer::getRegistration(const String& clientURL, Ref<DeferredPromise>&& promise)
207 {
208     auto* context = scriptExecutionContext();
209     if (!context) {
210         ASSERT_NOT_REACHED();
211         promise->reject(Exception { InvalidStateError });
212         return;
213     }
214
215     URL parsedURL = context->completeURL(clientURL);
216     if (!protocolHostAndPortAreEqual(parsedURL, context->url())) {
217         promise->reject(Exception { SecurityError, ASCIILiteral("Origin of clientURL is not client's origin") });
218         return;
219     }
220
221     if (!m_swConnection)
222         m_swConnection = &ServiceWorkerProvider::singleton().serviceWorkerConnectionForSession(context->sessionID());
223
224     return m_swConnection->matchRegistration(context->topOrigin(), parsedURL, [promise = WTFMove(promise), protectingThis = makePendingActivity(*this), this] (auto&& result) mutable {
225         if (m_isStopped)
226             return;
227
228         if (!result) {
229             promise->resolve();
230             return;
231         }
232
233         RefPtr<ServiceWorkerRegistration> registration = m_registrations.get(result->key);
234         if (!registration) {
235             auto& context = *scriptExecutionContext();
236             // FIXME: We should probably not be constructing ServiceWorkerRegistration objects here. Instead, we should make
237             // sure that ServiceWorkerRegistration objects stays alive as long as their SWServerRegistration on server side.
238             registration = ServiceWorkerRegistration::create(context, *this, WTFMove(result.value()));
239         }
240         promise->resolve<IDLInterface<ServiceWorkerRegistration>>(registration.releaseNonNull());
241     });
242 }
243
244 void ServiceWorkerContainer::updateRegistrationState(const ServiceWorkerRegistrationKey& key, ServiceWorkerRegistrationState state, const std::optional<ServiceWorkerIdentifier>& serviceWorkerIdentifier)
245 {
246     if (auto* registration = m_registrations.get(key))
247         registration->updateStateFromServer(state, serviceWorkerIdentifier);
248 }
249
250 void ServiceWorkerContainer::getRegistrations(RegistrationsPromise&& promise)
251 {
252     // FIXME: Implement getRegistrations algorithm, for now pretend there is no registration.
253     promise.resolve({ });
254 }
255
256 void ServiceWorkerContainer::startMessages()
257 {
258 }
259
260 void ServiceWorkerContainer::jobFailedWithException(ServiceWorkerJob& job, const Exception& exception)
261 {
262     job.promise().reject(exception);
263     jobDidFinish(job);
264 }
265
266 void ServiceWorkerContainer::fireUpdateFoundEvent(const ServiceWorkerRegistrationKey& key)
267 {
268     if (isStopped())
269         return;
270
271     auto* registration = m_registrations.get(key);
272     if (!registration)
273         return;
274
275     scriptExecutionContext()->postTask([container = makeRef(*this), registration = makeRef(*registration)] (ScriptExecutionContext&) {
276         if (container->isStopped())
277             return;
278         registration->dispatchEvent(Event::create(eventNames().updatefoundEvent, false, false));
279     });
280 }
281
282 class FirePostInstallEventsMicrotask final : public Microtask {
283     WTF_MAKE_FAST_ALLOCATED;
284 public:
285     explicit FirePostInstallEventsMicrotask(Ref<ServiceWorkerContainer>&& container, Ref<ServiceWorkerRegistration>&& registration)
286         : m_container(WTFMove(container))
287         , m_registration(WTFMove(registration))
288     {
289     }
290 private:
291     Result run() final
292     {
293         auto* serviceWorker = m_registration->installing();
294         if (!serviceWorker)
295             return Result::Done;
296
297         callOnMainThread([container = WTFMove(m_container), registration = WTFMove(m_registration), serviceWorker = makeRef(*serviceWorker)] () mutable {
298             if (container->isStopped())
299                 return;
300             registration->setInstallingWorker(nullptr);
301             registration->setWaitingWorker(serviceWorker.copyRef());
302             serviceWorker->updateWorkerState(ServiceWorker::State::Installed);
303             callOnMainThread([container = WTFMove(container), registration = WTFMove(registration), serviceWorker = WTFMove(serviceWorker)] () mutable {
304                 if (container->isStopped())
305                     return;
306                 registration->setWaitingWorker(nullptr);
307                 registration->setActiveWorker(serviceWorker.copyRef());
308                 serviceWorker->updateWorkerState(ServiceWorker::State::Activating);
309                 callOnMainThread([container = WTFMove(container), serviceWorker = WTFMove(serviceWorker)] () mutable {
310                     if (container->isStopped())
311                         return;
312                     serviceWorker->updateWorkerState(ServiceWorker::State::Activated);
313                 });
314             });
315         });
316         return Result::Done;
317     }
318
319     Ref<ServiceWorkerContainer> m_container;
320     Ref<ServiceWorkerRegistration> m_registration;
321 };
322
323 // FIXME: This method is only use to mimick service worker activation and will do away once we implement it.
324 void ServiceWorkerContainer::firePostInstallEvents(const ServiceWorkerRegistrationKey& key)
325 {
326     if (isStopped())
327         return;
328
329     auto* registration = m_registrations.get(key);
330     if (!registration)
331         return;
332
333     MicrotaskQueue::mainThreadQueue().append(std::make_unique<FirePostInstallEventsMicrotask>(*this, *registration));
334 }
335
336 void ServiceWorkerContainer::jobResolvedWithRegistration(ServiceWorkerJob& job, ServiceWorkerRegistrationData&& data)
337 {
338     auto guard = WTF::makeScopeExit([this, &job] {
339         jobDidFinish(job);
340     });
341
342     auto* context = scriptExecutionContext();
343     if (!context) {
344         LOG_ERROR("ServiceWorkerContainer::jobResolvedWithRegistration called but the containers ScriptExecutionContext is gone");
345         return;
346     }
347
348     // FIXME: Implement proper selection of service workers.
349     auto* installingServiceWorker = context->activeServiceWorker();
350     ASSERT(data.installingServiceWorkerIdentifier);
351     if (!installingServiceWorker || installingServiceWorker->identifier() != *data.installingServiceWorkerIdentifier) {
352         context->setActiveServiceWorker(ServiceWorker::create(*context, *data.installingServiceWorkerIdentifier, data.scriptURL));
353         installingServiceWorker = context->activeServiceWorker();
354     }
355
356     RefPtr<ServiceWorkerRegistration> registration = m_registrations.get(data.key);
357     if (!registration) {
358         // Currently the only registrations that can be created for the first time here should be Installing.
359         ASSERT(data.installingServiceWorkerIdentifier);
360         auto installingIdentifier = *data.installingServiceWorkerIdentifier;
361         
362         registration = ServiceWorkerRegistration::create(*context, *this, WTFMove(data));
363         registration->updateStateFromServer(ServiceWorkerRegistrationState::Installing, installingIdentifier);
364         ASSERT(registration->installing());
365
366         installingServiceWorker = registration->installing();
367     }
368
369     installingServiceWorker->updateWorkerState(ServiceWorkerState::Installing, ServiceWorker::DoNotFireStateChangeEvent);
370     registration->setInstallingWorker(installingServiceWorker);
371
372     LOG(ServiceWorker, "Container %p resolved job with registration %p", this, registration.get());
373
374     job.promise().resolve<IDLInterface<ServiceWorkerRegistration>>(*registration);
375 }
376
377 void ServiceWorkerContainer::jobResolvedWithUnregistrationResult(ServiceWorkerJob& job, bool unregistrationResult)
378 {
379     auto guard = WTF::makeScopeExit([this, &job] {
380         jobDidFinish(job);
381     });
382
383     auto* context = scriptExecutionContext();
384     if (!context) {
385         LOG_ERROR("ServiceWorkerContainer::jobResolvedWithUnregistrationResult called but the containers ScriptExecutionContext is gone");
386         return;
387     }
388
389     // FIXME: Implement proper selection of service workers.
390     if (unregistrationResult)
391         context->setActiveServiceWorker(nullptr);
392
393     job.promise().resolve<IDLBoolean>(unregistrationResult);
394 }
395
396 void ServiceWorkerContainer::startScriptFetchForJob(ServiceWorkerJob& job)
397 {
398     LOG(ServiceWorker, "SeviceWorkerContainer %p starting script fetch for job %" PRIu64, this, job.data().identifier());
399
400     auto* context = scriptExecutionContext();
401     if (!context) {
402         LOG_ERROR("ServiceWorkerContainer::jobResolvedWithRegistration called but the container's ScriptExecutionContext is gone");
403         m_swConnection->failedFetchingScript(job, { errorDomainWebKitInternal, 0, job.data().scriptURL, ASCIILiteral("Attempt to fetch service worker script with no ScriptExecutionContext") });
404         jobDidFinish(job);
405         return;
406     }
407
408     job.fetchScriptWithContext(*context);
409 }
410
411 void ServiceWorkerContainer::jobFinishedLoadingScript(ServiceWorkerJob& job, const String& script)
412 {
413     LOG(ServiceWorker, "SeviceWorkerContainer %p finished fetching script for job %" PRIu64, this, job.data().identifier());
414
415     m_swConnection->finishedFetchingScript(job, script);
416 }
417
418 void ServiceWorkerContainer::jobFailedLoadingScript(ServiceWorkerJob& job, const ResourceError& error, std::optional<Exception>&& exception)
419 {
420     LOG(ServiceWorker, "SeviceWorkerContainer %p failed fetching script for job %" PRIu64, this, job.data().identifier());
421
422     if (exception)
423         job.promise().reject(*exception);
424
425     m_swConnection->failedFetchingScript(job, error);
426 }
427
428 void ServiceWorkerContainer::jobDidFinish(ServiceWorkerJob& job)
429 {
430     auto taken = m_jobMap.take(job.data().identifier());
431     ASSERT_UNUSED(taken, !taken || taken->ptr() == &job);
432
433     unsetPendingActivity(this);
434 }
435
436 uint64_t ServiceWorkerContainer::connectionIdentifier()
437 {
438     ASSERT(m_swConnection);
439     return m_swConnection->identifier();
440 }
441
442 const char* ServiceWorkerContainer::activeDOMObjectName() const
443 {
444     return "ServiceWorkerContainer";
445 }
446
447 bool ServiceWorkerContainer::canSuspendForDocumentSuspension() const
448 {
449     return !hasPendingActivity();
450 }
451
452 } // namespace WebCore
453
454 #endif // ENABLE(SERVICE_WORKER)