Add support for unregistering a service worker
[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 "Exception.h"
32 #include "IDLTypes.h"
33 #include "JSDOMPromiseDeferred.h"
34 #include "JSServiceWorkerRegistration.h"
35 #include "Logging.h"
36 #include "NavigatorBase.h"
37 #include "ResourceError.h"
38 #include "ScopeGuard.h"
39 #include "ScriptExecutionContext.h"
40 #include "SecurityOrigin.h"
41 #include "ServiceWorker.h"
42 #include "ServiceWorkerJob.h"
43 #include "ServiceWorkerJobData.h"
44 #include "ServiceWorkerProvider.h"
45 #include "URL.h"
46 #include <wtf/RunLoop.h>
47
48 namespace WebCore {
49
50 ServiceWorkerContainer::ServiceWorkerContainer(ScriptExecutionContext& context, NavigatorBase& navigator)
51     : ActiveDOMObject(&context)
52     , m_navigator(navigator)
53 {
54     suspendIfNeeded();
55
56     m_readyPromise.reject(Exception { UnknownError, ASCIILiteral("serviceWorker.ready() is not yet implemented") });
57 }
58
59 ServiceWorkerContainer::~ServiceWorkerContainer()
60 {
61 #ifndef NDEBUG
62     ASSERT(m_creationThread == currentThread());
63 #endif
64 }
65
66 void ServiceWorkerContainer::refEventTarget()
67 {
68     m_navigator.ref();
69 }
70
71 void ServiceWorkerContainer::derefEventTarget()
72 {
73     m_navigator.deref();
74 }
75
76 ServiceWorker* ServiceWorkerContainer::controller() const
77 {
78     auto* context = scriptExecutionContext();
79     if (!context || !context->selectedServiceWorkerIdentifier()) {
80         m_controller = nullptr;
81         return nullptr;
82     }
83     if (!m_controller || m_controller->identifier() != context->selectedServiceWorkerIdentifier())
84         m_controller = ServiceWorker::create(*context, context->selectedServiceWorkerIdentifier());
85     return m_controller.get();
86 }
87
88 void ServiceWorkerContainer::addRegistration(const String& relativeScriptURL, const RegistrationOptions& options, Ref<DeferredPromise>&& promise)
89 {
90     auto* context = scriptExecutionContext();
91     if (!context || !context->sessionID().isValid()) {
92         ASSERT_NOT_REACHED();
93         promise->reject(Exception(InvalidStateError));
94         return;
95     }
96
97     if (relativeScriptURL.isEmpty()) {
98         promise->reject(Exception { TypeError, ASCIILiteral("serviceWorker.register() cannot be called with an empty script URL") });
99         return;
100     }
101
102     if (!m_swConnection)
103         m_swConnection = &ServiceWorkerProvider::singleton().serviceWorkerConnectionForSession(scriptExecutionContext()->sessionID());
104
105     ServiceWorkerJobData jobData(m_swConnection->identifier());
106
107     jobData.scriptURL = context->completeURL(relativeScriptURL);
108     if (!jobData.scriptURL.isValid()) {
109         promise->reject(Exception { TypeError, ASCIILiteral("serviceWorker.register() must be called with a valid relative script URL") });
110         return;
111     }
112
113     // FIXME: The spec disallows scripts outside of HTTP(S), but we'll likely support app custom URL schemes in WebKit.
114     if (!jobData.scriptURL.protocolIsInHTTPFamily()) {
115         promise->reject(Exception { TypeError, ASCIILiteral("serviceWorker.register() must be called with a script URL whose protocol is either HTTP or HTTPS") });
116         return;
117     }
118
119     String path = jobData.scriptURL.path();
120     if (path.containsIgnoringASCIICase("%2f") || path.containsIgnoringASCIICase("%5c")) {
121         promise->reject(Exception { TypeError, ASCIILiteral("serviceWorker.register() must be called with a script URL whose path does not contain '%2f' or '%5c'") });
122         return;
123     }
124
125     String scope = options.scope.isEmpty() ? ASCIILiteral("./") : options.scope;
126     if (!scope.isEmpty())
127         jobData.scopeURL = context->completeURL(scope);
128
129     if (!jobData.scopeURL.isNull() && !jobData.scopeURL.protocolIsInHTTPFamily()) {
130         promise->reject(Exception { TypeError, ASCIILiteral("Scope URL provided to serviceWorker.register() must be either HTTP or HTTPS") });
131         return;
132     }
133
134     path = jobData.scopeURL.path();
135     if (path.containsIgnoringASCIICase("%2f") || path.containsIgnoringASCIICase("%5c")) {
136         promise->reject(Exception { TypeError, ASCIILiteral("Scope URL provided to serviceWorker.register() cannot have a path that contains '%2f' or '%5c'") });
137         return;
138     }
139
140     jobData.clientCreationURL = context->url();
141     jobData.topOrigin = SecurityOriginData::fromSecurityOrigin(context->topOrigin());
142     jobData.type = ServiceWorkerJobType::Register;
143     jobData.registrationOptions = options;
144
145     scheduleJob(ServiceWorkerJob::create(*this, WTFMove(promise), WTFMove(jobData)));
146 }
147
148 void ServiceWorkerContainer::removeRegistration(const URL& scopeURL, Ref<DeferredPromise>&& promise)
149 {
150     auto* context = scriptExecutionContext();
151     if (!context || !context->sessionID().isValid()) {
152         ASSERT_NOT_REACHED();
153         promise->reject(Exception(InvalidStateError));
154         return;
155     }
156
157     if (!m_swConnection) {
158         ASSERT_NOT_REACHED();
159         promise->reject(Exception(InvalidStateError));
160         return;
161     }
162
163     ServiceWorkerJobData jobData(m_swConnection->identifier());
164     jobData.clientCreationURL = context->url();
165     jobData.topOrigin = SecurityOriginData::fromSecurityOrigin(context->topOrigin());
166     jobData.type = ServiceWorkerJobType::Unregister;
167     jobData.scopeURL = scopeURL;
168
169     scheduleJob(ServiceWorkerJob::create(*this, WTFMove(promise), WTFMove(jobData)));
170 }
171
172 void ServiceWorkerContainer::scheduleJob(Ref<ServiceWorkerJob>&& job)
173 {
174     ASSERT(m_swConnection);
175
176     ServiceWorkerJob& rawJob = job.get();
177     auto result = m_jobMap.add(rawJob.data().identifier(), WTFMove(job));
178     ASSERT_UNUSED(result, result.isNewEntry);
179
180     m_swConnection->scheduleJob(rawJob);
181 }
182
183 void ServiceWorkerContainer::getRegistration(const String&, Ref<DeferredPromise>&& promise)
184 {
185     promise->reject(Exception { UnknownError, ASCIILiteral("serviceWorker.getRegistration() is not yet implemented") });
186 }
187
188 void ServiceWorkerContainer::getRegistrations(Ref<DeferredPromise>&& promise)
189 {
190     promise->reject(Exception { UnknownError, ASCIILiteral("serviceWorker.getRegistrations() is not yet implemented") });
191 }
192
193 void ServiceWorkerContainer::startMessages()
194 {
195 }
196
197 void ServiceWorkerContainer::jobFailedWithException(ServiceWorkerJob& job, const Exception& exception)
198 {
199     job.promise().reject(exception);
200     jobDidFinish(job);
201 }
202
203 void ServiceWorkerContainer::jobResolvedWithRegistration(ServiceWorkerJob& job, ServiceWorkerRegistrationData&& data)
204 {
205     ScopeGuard guard([this, &job] {
206         jobDidFinish(job);
207     });
208
209     auto* context = scriptExecutionContext();
210     if (!context) {
211         LOG_ERROR("ServiceWorkerContainer::jobResolvedWithRegistration called but the containers ScriptExecutionContext is gone");
212         return;
213     }
214
215     // FIXME: Implement proper selection of service workers.
216     context->setSelectedServiceWorkerIdentifier(data.identifier);
217
218     auto registration = ServiceWorkerRegistration::create(*context, WTFMove(data));
219     job.promise().resolve<IDLInterface<ServiceWorkerRegistration>>(registration.get());
220 }
221
222 void ServiceWorkerContainer::jobResolvedWithUnregistrationResult(ServiceWorkerJob& job, bool unregistrationResult)
223 {
224     ScopeGuard guard([this, &job] {
225         jobDidFinish(job);
226     });
227
228     auto* context = scriptExecutionContext();
229     if (!context) {
230         LOG_ERROR("ServiceWorkerContainer::jobResolvedWithUnregistrationResult called but the containers ScriptExecutionContext is gone");
231         return;
232     }
233
234     // FIXME: Implement proper selection of service workers.
235     if (unregistrationResult)
236         context->setSelectedServiceWorkerIdentifier(0);
237
238     job.promise().resolve<IDLBoolean>(unregistrationResult);
239 }
240
241 void ServiceWorkerContainer::startScriptFetchForJob(ServiceWorkerJob& job)
242 {
243     LOG(ServiceWorker, "SeviceWorkerContainer %p starting script fetch for job %" PRIu64, this, job.data().identifier());
244
245     auto* context = scriptExecutionContext();
246     if (!context) {
247         LOG_ERROR("ServiceWorkerContainer::jobResolvedWithRegistration called but the container's ScriptExecutionContext is gone");
248         m_swConnection->failedFetchingScript(job, { errorDomainWebKitInternal, 0, job.data().scriptURL, ASCIILiteral("Attempt to fetch service worker script with no ScriptExecutionContext") });
249         jobDidFinish(job);
250         return;
251     }
252
253     job.fetchScriptWithContext(*context);
254 }
255
256 void ServiceWorkerContainer::jobFinishedLoadingScript(ServiceWorkerJob& job, const String& script)
257 {
258     LOG(ServiceWorker, "SeviceWorkerContainer %p finished fetching script for job %" PRIu64, this, job.data().identifier());
259
260     m_swConnection->finishedFetchingScript(job, script);
261 }
262
263 void ServiceWorkerContainer::jobFailedLoadingScript(ServiceWorkerJob& job, const ResourceError& error)
264 {
265     LOG(ServiceWorker, "SeviceWorkerContainer %p failed fetching script for job %" PRIu64, this, job.data().identifier());
266
267     m_swConnection->failedFetchingScript(job, error);
268 }
269
270 void ServiceWorkerContainer::jobDidFinish(ServiceWorkerJob& job)
271 {
272     auto taken = m_jobMap.take(job.data().identifier());
273     ASSERT_UNUSED(taken, !taken || taken.get() == &job);
274 }
275
276 uint64_t ServiceWorkerContainer::connectionIdentifier()
277 {
278     ASSERT(m_swConnection);
279     return m_swConnection->identifier();
280 }
281
282 const char* ServiceWorkerContainer::activeDOMObjectName() const
283 {
284     return "ServiceWorkerContainer";
285 }
286
287 bool ServiceWorkerContainer::canSuspendForDocumentSuspension() const
288 {
289     return true;
290 }
291
292 } // namespace WebCore
293
294 #endif // ENABLE(SERVICE_WORKER)