| /* |
| * Copyright (C) 2008-2017 Apple Inc. All Rights Reserved. |
| * Copyright (C) 2009, 2011 Google Inc. All Rights Reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| * |
| */ |
| |
| #include "config.h" |
| #include "WorkerGlobalScope.h" |
| |
| #include "CSSValueList.h" |
| #include "CSSValuePool.h" |
| #include "ContentSecurityPolicy.h" |
| #include "Crypto.h" |
| #include "IDBConnectionProxy.h" |
| #include "ImageBitmapOptions.h" |
| #include "InspectorInstrumentation.h" |
| #include "Performance.h" |
| #include "RuntimeEnabledFeatures.h" |
| #include "ScheduledAction.h" |
| #include "ScriptSourceCode.h" |
| #include "SecurityOrigin.h" |
| #include "SecurityOriginPolicy.h" |
| #include "ServiceWorkerGlobalScope.h" |
| #include "SocketProvider.h" |
| #include "WorkerEventLoop.h" |
| #include "WorkerInspectorController.h" |
| #include "WorkerLoaderProxy.h" |
| #include "WorkerLocation.h" |
| #include "WorkerMessagingProxy.h" |
| #include "WorkerNavigator.h" |
| #include "WorkerReportingProxy.h" |
| #include "WorkerSWClientConnection.h" |
| #include "WorkerScriptLoader.h" |
| #include "WorkerThread.h" |
| #include <JavaScriptCore/ScriptArguments.h> |
| #include <JavaScriptCore/ScriptCallStack.h> |
| #include <wtf/IsoMallocInlines.h> |
| |
| namespace WebCore { |
| using namespace Inspector; |
| |
| WTF_MAKE_ISO_ALLOCATED_IMPL(WorkerGlobalScope); |
| |
| WorkerGlobalScope::WorkerGlobalScope(const URL& url, Ref<SecurityOrigin>&& origin, const String& identifier, const String& userAgent, bool isOnline, WorkerThread& thread, bool shouldBypassMainWorldContentSecurityPolicy, Ref<SecurityOrigin>&& topOrigin, MonotonicTime timeOrigin, IDBClient::IDBConnectionProxy* connectionProxy, SocketProvider* socketProvider) |
| : m_url(url) |
| , m_identifier(identifier) |
| , m_userAgent(userAgent) |
| , m_thread(thread) |
| , m_script(makeUnique<WorkerScriptController>(this)) |
| , m_inspectorController(makeUnique<WorkerInspectorController>(*this)) |
| , m_isOnline(isOnline) |
| , m_shouldBypassMainWorldContentSecurityPolicy(shouldBypassMainWorldContentSecurityPolicy) |
| , m_topOrigin(WTFMove(topOrigin)) |
| #if ENABLE(INDEXED_DATABASE) |
| , m_connectionProxy(connectionProxy) |
| #endif |
| , m_socketProvider(socketProvider) |
| , m_performance(Performance::create(this, timeOrigin)) |
| { |
| #if !ENABLE(INDEXED_DATABASE) |
| UNUSED_PARAM(connectionProxy); |
| #endif |
| |
| if (m_topOrigin->hasUniversalAccess()) |
| origin->grantUniversalAccess(); |
| if (m_topOrigin->needsStorageAccessFromFileURLsQuirk()) |
| origin->grantStorageAccessFromFileURLsQuirk(); |
| |
| setSecurityOriginPolicy(SecurityOriginPolicy::create(WTFMove(origin))); |
| setContentSecurityPolicy(makeUnique<ContentSecurityPolicy>(URL { m_url }, *this)); |
| } |
| |
| WorkerGlobalScope::~WorkerGlobalScope() |
| { |
| ASSERT(thread().thread() == &Thread::current()); |
| // We need to remove from the contexts map very early in the destructor so that calling postTask() on this WorkerGlobalScope from another thread is safe. |
| removeFromContextsMap(); |
| |
| m_performance = nullptr; |
| m_crypto = nullptr; |
| |
| // Notify proxy that we are going away. This can free the WorkerThread object, so do not access it after this. |
| thread().workerReportingProxy().workerGlobalScopeDestroyed(); |
| } |
| |
| EventLoopTaskGroup& WorkerGlobalScope::eventLoop() |
| { |
| ASSERT(isContextThread()); |
| if (UNLIKELY(!m_defaultTaskGroup)) { |
| m_eventLoop = WorkerEventLoop::create(*this); |
| m_defaultTaskGroup = makeUnique<EventLoopTaskGroup>(*m_eventLoop); |
| if (activeDOMObjectsAreStopped()) |
| m_defaultTaskGroup->stopAndDiscardAllTasks(); |
| } |
| return *m_defaultTaskGroup; |
| } |
| |
| String WorkerGlobalScope::origin() const |
| { |
| auto* securityOrigin = this->securityOrigin(); |
| return securityOrigin ? securityOrigin->toString() : emptyString(); |
| } |
| |
| void WorkerGlobalScope::prepareForTermination() |
| { |
| #if ENABLE(INDEXED_DATABASE) |
| stopIndexedDatabase(); |
| #endif |
| |
| if (m_defaultTaskGroup) |
| m_defaultTaskGroup->stopAndDiscardAllTasks(); |
| stopActiveDOMObjects(); |
| |
| if (m_cacheStorageConnection) |
| m_cacheStorageConnection->clearPendingRequests(); |
| |
| m_inspectorController->workerTerminating(); |
| |
| // Event listeners would keep DOMWrapperWorld objects alive for too long. Also, they have references to JS objects, |
| // which become dangling once Heap is destroyed. |
| removeAllEventListeners(); |
| |
| // MicrotaskQueue and RejectedPromiseTracker reference Heap. |
| if (m_eventLoop) |
| m_eventLoop->clearMicrotaskQueue(); |
| removeRejectedPromiseTracker(); |
| } |
| |
| void WorkerGlobalScope::removeAllEventListeners() |
| { |
| EventTarget::removeAllEventListeners(); |
| m_performance->removeAllEventListeners(); |
| m_performance->removeAllObservers(); |
| } |
| |
| bool WorkerGlobalScope::isSecureContext() const |
| { |
| if (!RuntimeEnabledFeatures::sharedFeatures().secureContextChecksEnabled()) |
| return true; |
| |
| return securityOrigin() && securityOrigin()->isPotentiallyTrustworthy(); |
| } |
| |
| void WorkerGlobalScope::applyContentSecurityPolicyResponseHeaders(const ContentSecurityPolicyResponseHeaders& contentSecurityPolicyResponseHeaders) |
| { |
| contentSecurityPolicy()->didReceiveHeaders(contentSecurityPolicyResponseHeaders, String { }); |
| } |
| |
| URL WorkerGlobalScope::completeURL(const String& url, ForceUTF8) const |
| { |
| // Always return a null URL when passed a null string. |
| // FIXME: Should we change the URL constructor to have this behavior? |
| if (url.isNull()) |
| return URL(); |
| // Always use UTF-8 in Workers. |
| return URL(m_url, url); |
| } |
| |
| String WorkerGlobalScope::userAgent(const URL&) const |
| { |
| return m_userAgent; |
| } |
| |
| void WorkerGlobalScope::disableEval(const String& errorMessage) |
| { |
| m_script->disableEval(errorMessage); |
| } |
| |
| void WorkerGlobalScope::disableWebAssembly(const String& errorMessage) |
| { |
| m_script->disableWebAssembly(errorMessage); |
| } |
| |
| SocketProvider* WorkerGlobalScope::socketProvider() |
| { |
| return m_socketProvider.get(); |
| } |
| |
| #if ENABLE(INDEXED_DATABASE) |
| |
| IDBClient::IDBConnectionProxy* WorkerGlobalScope::idbConnectionProxy() |
| { |
| #if ENABLE(INDEXED_DATABASE_IN_WORKERS) |
| return m_connectionProxy.get(); |
| #else |
| return nullptr; |
| #endif |
| } |
| |
| void WorkerGlobalScope::stopIndexedDatabase() |
| { |
| #if ENABLE(INDEXED_DATABASE_IN_WORKERS) |
| if (m_connectionProxy) |
| m_connectionProxy->forgetActivityForCurrentThread(); |
| #endif |
| } |
| |
| void WorkerGlobalScope::suspend() |
| { |
| #if ENABLE(INDEXED_DATABASE_IN_WORKERS) |
| if (m_connectionProxy) |
| m_connectionProxy->setContextSuspended(*scriptExecutionContext(), true); |
| #endif |
| } |
| |
| void WorkerGlobalScope::resume() |
| { |
| #if ENABLE(INDEXED_DATABASE_IN_WORKERS) |
| if (m_connectionProxy) |
| m_connectionProxy->setContextSuspended(*scriptExecutionContext(), false); |
| #endif |
| } |
| |
| #endif // ENABLE(INDEXED_DATABASE) |
| |
| WorkerLocation& WorkerGlobalScope::location() const |
| { |
| if (!m_location) |
| m_location = WorkerLocation::create(m_url); |
| return *m_location; |
| } |
| |
| void WorkerGlobalScope::close() |
| { |
| if (m_closing) |
| return; |
| |
| // Let current script run to completion but prevent future script evaluations. |
| // After m_closing is set, all the tasks in the queue continue to be fetched but only |
| // tasks with isCleanupTask()==true will be executed. |
| m_closing = true; |
| postTask({ ScriptExecutionContext::Task::CleanupTask, [] (ScriptExecutionContext& context) { |
| ASSERT_WITH_SECURITY_IMPLICATION(is<WorkerGlobalScope>(context)); |
| WorkerGlobalScope& workerGlobalScope = downcast<WorkerGlobalScope>(context); |
| // Notify parent that this context is closed. Parent is responsible for calling WorkerThread::stop(). |
| workerGlobalScope.thread().workerReportingProxy().workerGlobalScopeClosed(); |
| } }); |
| } |
| |
| WorkerNavigator& WorkerGlobalScope::navigator() |
| { |
| if (!m_navigator) |
| m_navigator = WorkerNavigator::create(*this, m_userAgent, m_isOnline); |
| return *m_navigator; |
| } |
| |
| void WorkerGlobalScope::setIsOnline(bool isOnline) |
| { |
| m_isOnline = isOnline; |
| if (m_navigator) |
| m_navigator->setIsOnline(isOnline); |
| } |
| |
| void WorkerGlobalScope::postTask(Task&& task) |
| { |
| thread().runLoop().postTask(WTFMove(task)); |
| } |
| |
| ExceptionOr<int> WorkerGlobalScope::setTimeout(JSC::JSGlobalObject& state, std::unique_ptr<ScheduledAction> action, int timeout, Vector<JSC::Strong<JSC::Unknown>>&& arguments) |
| { |
| // FIXME: Should this check really happen here? Or should it happen when code is about to eval? |
| if (action->type() == ScheduledAction::Type::Code) { |
| if (!contentSecurityPolicy()->allowEval(&state)) |
| return 0; |
| } |
| |
| action->addArguments(WTFMove(arguments)); |
| |
| return DOMTimer::install(*this, WTFMove(action), Seconds::fromMilliseconds(timeout), true); |
| } |
| |
| void WorkerGlobalScope::clearTimeout(int timeoutId) |
| { |
| DOMTimer::removeById(*this, timeoutId); |
| } |
| |
| ExceptionOr<int> WorkerGlobalScope::setInterval(JSC::JSGlobalObject& state, std::unique_ptr<ScheduledAction> action, int timeout, Vector<JSC::Strong<JSC::Unknown>>&& arguments) |
| { |
| // FIXME: Should this check really happen here? Or should it happen when code is about to eval? |
| if (action->type() == ScheduledAction::Type::Code) { |
| if (!contentSecurityPolicy()->allowEval(&state)) |
| return 0; |
| } |
| |
| action->addArguments(WTFMove(arguments)); |
| |
| return DOMTimer::install(*this, WTFMove(action), Seconds::fromMilliseconds(timeout), false); |
| } |
| |
| void WorkerGlobalScope::clearInterval(int timeoutId) |
| { |
| DOMTimer::removeById(*this, timeoutId); |
| } |
| |
| ExceptionOr<void> WorkerGlobalScope::importScripts(const Vector<String>& urls) |
| { |
| ASSERT(contentSecurityPolicy()); |
| |
| Vector<URL> completedURLs; |
| completedURLs.reserveInitialCapacity(urls.size()); |
| for (auto& entry : urls) { |
| URL url = completeURL(entry); |
| if (!url.isValid()) |
| return Exception { SyntaxError }; |
| completedURLs.uncheckedAppend(WTFMove(url)); |
| } |
| |
| FetchOptions::Cache cachePolicy = FetchOptions::Cache::Default; |
| |
| #if ENABLE(SERVICE_WORKER) |
| bool isServiceWorkerGlobalScope = is<ServiceWorkerGlobalScope>(*this); |
| if (isServiceWorkerGlobalScope) { |
| // FIXME: We need to add support for the 'imported scripts updated' flag as per: |
| // https://w3c.github.io/ServiceWorker/#importscripts |
| auto& serviceWorkerGlobalScope = downcast<ServiceWorkerGlobalScope>(*this); |
| auto& registration = serviceWorkerGlobalScope.registration(); |
| if (registration.updateViaCache() == ServiceWorkerUpdateViaCache::None || registration.needsUpdate()) |
| cachePolicy = FetchOptions::Cache::NoCache; |
| } |
| #endif |
| |
| for (auto& url : completedURLs) { |
| // FIXME: Convert this to check the isolated world's Content Security Policy once webkit.org/b/104520 is solved. |
| bool shouldBypassMainWorldContentSecurityPolicy = this->shouldBypassMainWorldContentSecurityPolicy(); |
| if (!shouldBypassMainWorldContentSecurityPolicy && !contentSecurityPolicy()->allowScriptFromSource(url)) |
| return Exception { NetworkError }; |
| |
| auto scriptLoader = WorkerScriptLoader::create(); |
| auto cspEnforcement = shouldBypassMainWorldContentSecurityPolicy ? ContentSecurityPolicyEnforcement::DoNotEnforce : ContentSecurityPolicyEnforcement::EnforceScriptSrcDirective; |
| if (auto exception = scriptLoader->loadSynchronously(this, url, FetchOptions::Mode::NoCors, cachePolicy, cspEnforcement, resourceRequestIdentifier())) |
| return WTFMove(*exception); |
| |
| InspectorInstrumentation::scriptImported(*this, scriptLoader->identifier(), scriptLoader->script()); |
| |
| NakedPtr<JSC::Exception> exception; |
| m_script->evaluate(ScriptSourceCode(scriptLoader->script(), URL(scriptLoader->responseURL())), exception); |
| if (exception) { |
| m_script->setException(exception); |
| return { }; |
| } |
| } |
| |
| return { }; |
| } |
| |
| EventTarget* WorkerGlobalScope::errorEventTarget() |
| { |
| return this; |
| } |
| |
| void WorkerGlobalScope::logExceptionToConsole(const String& errorMessage, const String& sourceURL, int lineNumber, int columnNumber, RefPtr<ScriptCallStack>&&) |
| { |
| thread().workerReportingProxy().postExceptionToWorkerObject(errorMessage, lineNumber, columnNumber, sourceURL); |
| } |
| |
| void WorkerGlobalScope::addConsoleMessage(std::unique_ptr<Inspector::ConsoleMessage>&& message) |
| { |
| if (!isContextThread()) { |
| postTask(AddConsoleMessageTask(message->source(), message->level(), message->message())); |
| return; |
| } |
| |
| InspectorInstrumentation::addMessageToConsole(*this, WTFMove(message)); |
| } |
| |
| void WorkerGlobalScope::addConsoleMessage(MessageSource source, MessageLevel level, const String& message, unsigned long requestIdentifier) |
| { |
| addMessage(source, level, message, { }, 0, 0, nullptr, nullptr, requestIdentifier); |
| } |
| |
| void WorkerGlobalScope::addMessage(MessageSource source, MessageLevel level, const String& messageText, const String& sourceURL, unsigned lineNumber, unsigned columnNumber, RefPtr<ScriptCallStack>&& callStack, JSC::JSGlobalObject* state, unsigned long requestIdentifier) |
| { |
| if (!isContextThread()) { |
| postTask(AddConsoleMessageTask(source, level, messageText)); |
| return; |
| } |
| |
| std::unique_ptr<Inspector::ConsoleMessage> message; |
| if (callStack) |
| message = makeUnique<Inspector::ConsoleMessage>(source, MessageType::Log, level, messageText, callStack.releaseNonNull(), requestIdentifier); |
| else |
| message = makeUnique<Inspector::ConsoleMessage>(source, MessageType::Log, level, messageText, sourceURL, lineNumber, columnNumber, state, requestIdentifier); |
| InspectorInstrumentation::addMessageToConsole(*this, WTFMove(message)); |
| } |
| |
| bool WorkerGlobalScope::isContextThread() const |
| { |
| return thread().thread() == &Thread::current(); |
| } |
| |
| bool WorkerGlobalScope::isJSExecutionForbidden() const |
| { |
| return m_script->isExecutionForbidden(); |
| } |
| |
| #if ENABLE(WEB_CRYPTO) |
| |
| class CryptoBufferContainer : public ThreadSafeRefCounted<CryptoBufferContainer> { |
| public: |
| static Ref<CryptoBufferContainer> create() { return adoptRef(*new CryptoBufferContainer); } |
| Vector<uint8_t>& buffer() { return m_buffer; } |
| |
| private: |
| Vector<uint8_t> m_buffer; |
| }; |
| |
| class CryptoBooleanContainer : public ThreadSafeRefCounted<CryptoBooleanContainer> { |
| public: |
| static Ref<CryptoBooleanContainer> create() { return adoptRef(*new CryptoBooleanContainer); } |
| bool boolean() const { return m_boolean; } |
| void setBoolean(bool boolean) { m_boolean = boolean; } |
| |
| private: |
| std::atomic<bool> m_boolean { false }; |
| }; |
| |
| bool WorkerGlobalScope::wrapCryptoKey(const Vector<uint8_t>& key, Vector<uint8_t>& wrappedKey) |
| { |
| Ref<WorkerGlobalScope> protectedThis(*this); |
| auto resultContainer = CryptoBooleanContainer::create(); |
| auto doneContainer = CryptoBooleanContainer::create(); |
| auto wrappedKeyContainer = CryptoBufferContainer::create(); |
| m_thread.workerLoaderProxy().postTaskToLoader([resultContainer = resultContainer.copyRef(), key, wrappedKeyContainer = wrappedKeyContainer.copyRef(), doneContainer = doneContainer.copyRef(), workerMessagingProxy = makeRef(downcast<WorkerMessagingProxy>(m_thread.workerLoaderProxy()))](ScriptExecutionContext& context) { |
| resultContainer->setBoolean(context.wrapCryptoKey(key, wrappedKeyContainer->buffer())); |
| doneContainer->setBoolean(true); |
| workerMessagingProxy->postTaskForModeToWorkerGlobalScope([](ScriptExecutionContext& context) { |
| ASSERT_UNUSED(context, context.isWorkerGlobalScope()); |
| }, WorkerRunLoop::defaultMode()); |
| }); |
| |
| auto waitResult = MessageQueueMessageReceived; |
| while (!doneContainer->boolean() && waitResult != MessageQueueTerminated) |
| waitResult = m_thread.runLoop().runInMode(this, WorkerRunLoop::defaultMode()); |
| |
| if (doneContainer->boolean()) |
| wrappedKey.swap(wrappedKeyContainer->buffer()); |
| return resultContainer->boolean(); |
| } |
| |
| bool WorkerGlobalScope::unwrapCryptoKey(const Vector<uint8_t>& wrappedKey, Vector<uint8_t>& key) |
| { |
| Ref<WorkerGlobalScope> protectedThis(*this); |
| auto resultContainer = CryptoBooleanContainer::create(); |
| auto doneContainer = CryptoBooleanContainer::create(); |
| auto keyContainer = CryptoBufferContainer::create(); |
| m_thread.workerLoaderProxy().postTaskToLoader([resultContainer = resultContainer.copyRef(), wrappedKey, keyContainer = keyContainer.copyRef(), doneContainer = doneContainer.copyRef(), workerMessagingProxy = makeRef(downcast<WorkerMessagingProxy>(m_thread.workerLoaderProxy()))](ScriptExecutionContext& context) { |
| resultContainer->setBoolean(context.unwrapCryptoKey(wrappedKey, keyContainer->buffer())); |
| doneContainer->setBoolean(true); |
| workerMessagingProxy->postTaskForModeToWorkerGlobalScope([](ScriptExecutionContext& context) { |
| ASSERT_UNUSED(context, context.isWorkerGlobalScope()); |
| }, WorkerRunLoop::defaultMode()); |
| }); |
| |
| auto waitResult = MessageQueueMessageReceived; |
| while (!doneContainer->boolean() && waitResult != MessageQueueTerminated) |
| waitResult = m_thread.runLoop().runInMode(this, WorkerRunLoop::defaultMode()); |
| |
| if (doneContainer->boolean()) |
| key.swap(keyContainer->buffer()); |
| return resultContainer->boolean(); |
| } |
| |
| #endif // ENABLE(WEB_CRYPTO) |
| |
| Crypto& WorkerGlobalScope::crypto() |
| { |
| if (!m_crypto) |
| m_crypto = Crypto::create(this); |
| return *m_crypto; |
| } |
| |
| Performance& WorkerGlobalScope::performance() const |
| { |
| return *m_performance; |
| } |
| |
| WorkerCacheStorageConnection& WorkerGlobalScope::cacheStorageConnection() |
| { |
| if (!m_cacheStorageConnection) |
| m_cacheStorageConnection = WorkerCacheStorageConnection::create(*this); |
| return *m_cacheStorageConnection; |
| } |
| |
| MessagePortChannelProvider& WorkerGlobalScope::messagePortChannelProvider() |
| { |
| if (!m_messagePortChannelProvider) |
| m_messagePortChannelProvider = makeUnique<WorkerMessagePortChannelProvider>(*this); |
| return *m_messagePortChannelProvider; |
| } |
| |
| #if ENABLE(SERVICE_WORKER) |
| WorkerSWClientConnection& WorkerGlobalScope::swClientConnection() |
| { |
| if (!m_swClientConnection) |
| m_swClientConnection = WorkerSWClientConnection::create(*this); |
| return *m_swClientConnection; |
| } |
| #endif |
| |
| void WorkerGlobalScope::createImageBitmap(ImageBitmap::Source&& source, ImageBitmapOptions&& options, ImageBitmap::Promise&& promise) |
| { |
| ImageBitmap::createPromise(*this, WTFMove(source), WTFMove(options), WTFMove(promise)); |
| } |
| |
| void WorkerGlobalScope::createImageBitmap(ImageBitmap::Source&& source, int sx, int sy, int sw, int sh, ImageBitmapOptions&& options, ImageBitmap::Promise&& promise) |
| { |
| ImageBitmap::createPromise(*this, WTFMove(source), WTFMove(options), sx, sy, sw, sh, WTFMove(promise)); |
| } |
| |
| CSSValuePool& WorkerGlobalScope::cssValuePool() |
| { |
| if (!m_cssValuePool) |
| m_cssValuePool = makeUnique<CSSValuePool>(); |
| return *m_cssValuePool; |
| } |
| |
| } // namespace WebCore |