/* -*- 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 "FetchEventOpChild.h"

#include <utility>

#include "MainThreadUtils.h"
#include "nsContentPolicyUtils.h"
#include "nsContentUtils.h"
#include "nsDebug.h"
#include "nsError.h"
#include "nsIChannel.h"
#include "nsIConsoleReportCollector.h"
#include "nsIContentPolicy.h"
#include "nsIInputStream.h"
#include "nsILoadInfo.h"
#include "nsINetworkInterceptController.h"
#include "nsIObserverService.h"
#include "nsIScriptError.h"
#include "nsISupportsImpl.h"
#include "nsIURI.h"
#include "nsNetUtil.h"
#include "nsProxyRelease.h"
#include "nsTArray.h"
#include "nsThreadUtils.h"

#include "ServiceWorkerPrivate.h"
#include "mozilla/Assertions.h"
#include "mozilla/LoadInfo.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/Telemetry.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/Unused.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/dom/FetchService.h"
#include "mozilla/dom/InternalHeaders.h"
#include "mozilla/dom/InternalResponse.h"
#include "mozilla/dom/PRemoteWorkerControllerChild.h"
#include "mozilla/dom/ServiceWorkerRegistrationInfo.h"
#include "mozilla/net/NeckoChannelParams.h"

namespace mozilla::dom {

namespace {

bool CSPPermitsResponse(nsILoadInfo* aLoadInfo,
                        SafeRefPtr<InternalResponse> aResponse,
                        const nsACString& aWorkerScriptSpec) {
  AssertIsOnMainThread();
  MOZ_ASSERT(aLoadInfo);

  nsCString url = aResponse->GetUnfilteredURL();
  if (url.IsEmpty()) {
    // Synthetic response.
    url = aWorkerScriptSpec;
  }

  nsCOMPtr<nsIURI> uri;
  nsresult rv = NS_NewURI(getter_AddRefs(uri), url, nullptr, nullptr);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return false;
  }

  int16_t decision = nsIContentPolicy::ACCEPT;
  rv = NS_CheckContentLoadPolicy(uri, aLoadInfo, ""_ns, &decision);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return false;
  }

  return decision == nsIContentPolicy::ACCEPT;
}

void AsyncLog(nsIInterceptedChannel* aChannel, const nsACString& aScriptSpec,
              uint32_t aLineNumber, uint32_t aColumnNumber,
              const nsACString& aMessageName, nsTArray<nsString>&& aParams) {
  AssertIsOnMainThread();
  MOZ_ASSERT(aChannel);

  nsCOMPtr<nsIConsoleReportCollector> reporter =
      aChannel->GetConsoleReportCollector();

  if (reporter) {
    // NOTE: is appears that `const nsTArray<nsString>&` is required for
    // nsIConsoleReportCollector::AddConsoleReport to resolve to the correct
    // overload.
    const nsTArray<nsString> params = std::move(aParams);

    reporter->AddConsoleReport(
        nsIScriptError::errorFlag, "Service Worker Interception"_ns,
        nsContentUtils::eDOM_PROPERTIES, aScriptSpec, aLineNumber,
        aColumnNumber, aMessageName, params);
  }
}

class SynthesizeResponseWatcher final : public nsIInterceptedBodyCallback {
 public:
  NS_DECL_THREADSAFE_ISUPPORTS

  SynthesizeResponseWatcher(
      const nsMainThreadPtrHandle<nsIInterceptedChannel>& aInterceptedChannel,
      const nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo>& aRegistration,
      const bool aIsNonSubresourceRequest,
      FetchEventRespondWithClosure&& aClosure, nsAString&& aRequestURL)
      : mInterceptedChannel(aInterceptedChannel),
        mRegistration(aRegistration),
        mIsNonSubresourceRequest(aIsNonSubresourceRequest),
        mClosure(std::move(aClosure)),
        mRequestURL(std::move(aRequestURL)) {
    AssertIsOnMainThread();
    MOZ_ASSERT(mInterceptedChannel);
    MOZ_ASSERT(mRegistration);
  }

  NS_IMETHOD
  BodyComplete(nsresult aRv) override {
    AssertIsOnMainThread();
    MOZ_ASSERT(mInterceptedChannel);

    if (NS_WARN_IF(NS_FAILED(aRv))) {
      AsyncLog(mInterceptedChannel, mClosure.respondWithScriptSpec(),
               mClosure.respondWithLineNumber(),
               mClosure.respondWithColumnNumber(),
               "InterceptionFailedWithURL"_ns, {mRequestURL});

      CancelInterception(NS_ERROR_INTERCEPTION_FAILED);

      return NS_OK;
    }

    nsresult rv = mInterceptedChannel->FinishSynthesizedResponse();

    if (NS_WARN_IF(NS_FAILED(rv))) {
      CancelInterception(rv);
    }

    mInterceptedChannel = nullptr;

    return NS_OK;
  }

  // See FetchEventOpChild::MaybeScheduleRegistrationUpdate() for comments.
  void CancelInterception(nsresult aStatus) {
    AssertIsOnMainThread();
    MOZ_ASSERT(mInterceptedChannel);
    MOZ_ASSERT(mRegistration);

    mInterceptedChannel->CancelInterception(aStatus);

    if (mIsNonSubresourceRequest) {
      mRegistration->MaybeScheduleUpdate();
    } else {
      mRegistration->MaybeScheduleTimeCheckAndUpdate();
    }

    mInterceptedChannel = nullptr;
    mRegistration = nullptr;
  }

 private:
  ~SynthesizeResponseWatcher() {
    if (NS_WARN_IF(mInterceptedChannel)) {
      CancelInterception(NS_ERROR_DOM_ABORT_ERR);
    }
  }

  nsMainThreadPtrHandle<nsIInterceptedChannel> mInterceptedChannel;
  nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> mRegistration;
  const bool mIsNonSubresourceRequest;
  const FetchEventRespondWithClosure mClosure;
  const nsString mRequestURL;
};

NS_IMPL_ISUPPORTS(SynthesizeResponseWatcher, nsIInterceptedBodyCallback)

}  // anonymous namespace

/* static */ RefPtr<GenericPromise> FetchEventOpChild::SendFetchEvent(
    PRemoteWorkerControllerChild* aManager,
    ParentToParentServiceWorkerFetchEventOpArgs&& aArgs,
    nsCOMPtr<nsIInterceptedChannel> aInterceptedChannel,
    RefPtr<ServiceWorkerRegistrationInfo> aRegistration,
    RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises,
    RefPtr<KeepAliveToken>&& aKeepAliveToken) {
  AssertIsOnMainThread();
  MOZ_ASSERT(aManager);
  MOZ_ASSERT(aInterceptedChannel);
  MOZ_ASSERT(aKeepAliveToken);

  FetchEventOpChild* actor = new FetchEventOpChild(
      std::move(aArgs), std::move(aInterceptedChannel),
      std::move(aRegistration), std::move(aPreloadResponseReadyPromises),
      std::move(aKeepAliveToken));

  actor->mWasSent = true;
  Unused << aManager->SendPFetchEventOpConstructor(actor, actor->mArgs);

  return actor->mPromiseHolder.Ensure(__func__);
}

FetchEventOpChild::~FetchEventOpChild() {
  AssertIsOnMainThread();
  MOZ_ASSERT(mInterceptedChannelHandled);
  MOZ_DIAGNOSTIC_ASSERT(mPromiseHolder.IsEmpty());
}

FetchEventOpChild::FetchEventOpChild(
    ParentToParentServiceWorkerFetchEventOpArgs&& aArgs,
    nsCOMPtr<nsIInterceptedChannel>&& aInterceptedChannel,
    RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration,
    RefPtr<FetchServicePromises>&& aPreloadResponseReadyPromises,
    RefPtr<KeepAliveToken>&& aKeepAliveToken)
    : mArgs(std::move(aArgs)),
      mInterceptedChannel(std::move(aInterceptedChannel)),
      mRegistration(std::move(aRegistration)),
      mKeepAliveToken(std::move(aKeepAliveToken)),
      mPreloadResponseReadyPromises(std::move(aPreloadResponseReadyPromises)) {
  if (mPreloadResponseReadyPromises) {
    // This promise should be configured to use synchronous dispatch, so if it's
    // already resolved when we run this code then the callback will be called
    // synchronously and pass the preload response with the constructor message.
    //
    // Note that it's fine to capture the this pointer in the callbacks because
    // we disconnect the request in Recv__delete__().
    mPreloadResponseReadyPromises->GetResponseAvailablePromise()
        ->Then(
            GetCurrentSerialEventTarget(), __func__,
            [this](FetchServiceResponse&& aResponse) {
              if (!mWasSent) {
                // The actor wasn't sent yet, we can still send the preload
                // response with it.
                mArgs.preloadResponse() =
                    Some(aResponse->ToParentToParentInternalResponse());
              } else {
                // It's too late to send the preload response with the actor, we
                // have to send it in a separate message.
                SendPreloadResponse(
                    aResponse->ToParentToParentInternalResponse());
              }
              mPreloadResponseAvailablePromiseRequestHolder.Complete();
            },
            [this](const CopyableErrorResult&) {
              mPreloadResponseAvailablePromiseRequestHolder.Complete();
            })
        ->Track(mPreloadResponseAvailablePromiseRequestHolder);

    mPreloadResponseReadyPromises->GetResponseTimingPromise()
        ->Then(
            GetCurrentSerialEventTarget(), __func__,
            [this](ResponseTiming&& aTiming) {
              if (!mWasSent) {
                // The actor wasn't sent yet, we can still send the preload
                // response timing with it.
                mArgs.preloadResponseTiming() = Some(std::move(aTiming));
              } else {
                SendPreloadResponseTiming(aTiming);
              }
              mPreloadResponseTimingPromiseRequestHolder.Complete();
            },
            [this](const CopyableErrorResult&) {
              mPreloadResponseTimingPromiseRequestHolder.Complete();
            })
        ->Track(mPreloadResponseTimingPromiseRequestHolder);

    mPreloadResponseReadyPromises->GetResponseEndPromise()
        ->Then(
            GetCurrentSerialEventTarget(), __func__,
            [this](ResponseEndArgs&& aResponse) {
              if (!mWasSent) {
                // The actor wasn't sent yet, we can still send the preload
                // response end args with it.
                mArgs.preloadResponseEndArgs() = Some(std::move(aResponse));
              } else {
                // It's too late to send the preload response end with the
                // actor, we have to send it in a separate message.
                SendPreloadResponseEnd(aResponse);
              }
              mPreloadResponseReadyPromises = nullptr;
              mPreloadResponseEndPromiseRequestHolder.Complete();
            },
            [this](const CopyableErrorResult&) {
              mPreloadResponseReadyPromises = nullptr;
              mPreloadResponseEndPromiseRequestHolder.Complete();
            })
        ->Track(mPreloadResponseEndPromiseRequestHolder);
  }
}

mozilla::ipc::IPCResult FetchEventOpChild::RecvAsyncLog(
    const nsCString& aScriptSpec, const uint32_t& aLineNumber,
    const uint32_t& aColumnNumber, const nsCString& aMessageName,
    nsTArray<nsString>&& aParams) {
  AssertIsOnMainThread();
  MOZ_ASSERT(mInterceptedChannel);

  AsyncLog(mInterceptedChannel, aScriptSpec, aLineNumber, aColumnNumber,
           aMessageName, std::move(aParams));

  return IPC_OK();
}

mozilla::ipc::IPCResult FetchEventOpChild::RecvRespondWith(
    ParentToParentFetchEventRespondWithResult&& aResult) {
  AssertIsOnMainThread();

  switch (aResult.type()) {
    case ParentToParentFetchEventRespondWithResult::
        TParentToParentSynthesizeResponseArgs:
      mInterceptedChannel->SetFetchHandlerStart(
          aResult.get_ParentToParentSynthesizeResponseArgs()
              .timeStamps()
              .fetchHandlerStart());
      mInterceptedChannel->SetFetchHandlerFinish(
          aResult.get_ParentToParentSynthesizeResponseArgs()
              .timeStamps()
              .fetchHandlerFinish());
      SynthesizeResponse(
          std::move(aResult.get_ParentToParentSynthesizeResponseArgs()));
      break;
    case ParentToParentFetchEventRespondWithResult::TResetInterceptionArgs:
      mInterceptedChannel->SetFetchHandlerStart(
          aResult.get_ResetInterceptionArgs().timeStamps().fetchHandlerStart());
      mInterceptedChannel->SetFetchHandlerFinish(
          aResult.get_ResetInterceptionArgs()
              .timeStamps()
              .fetchHandlerFinish());
      ResetInterception(false);
      break;
    case ParentToParentFetchEventRespondWithResult::TCancelInterceptionArgs:
      mInterceptedChannel->SetFetchHandlerStart(
          aResult.get_CancelInterceptionArgs()
              .timeStamps()
              .fetchHandlerStart());
      mInterceptedChannel->SetFetchHandlerFinish(
          aResult.get_CancelInterceptionArgs()
              .timeStamps()
              .fetchHandlerFinish());
      CancelInterception(aResult.get_CancelInterceptionArgs().status());
      break;
    default:
      MOZ_CRASH("Unknown IPCFetchEventRespondWithResult type!");
      break;
  }

  return IPC_OK();
}

mozilla::ipc::IPCResult FetchEventOpChild::Recv__delete__(
    const ServiceWorkerFetchEventOpResult& aResult) {
  AssertIsOnMainThread();
  MOZ_ASSERT(mRegistration);

  if (NS_WARN_IF(!mInterceptedChannelHandled)) {
    MOZ_ASSERT(NS_FAILED(aResult.rv()));
    NS_WARNING(
        "Failed to handle intercepted network request; canceling "
        "interception!");

    CancelInterception(aResult.rv());
  }

  mPromiseHolder.ResolveIfExists(true, __func__);

  // FetchEvent is completed.
  // Disconnect preload response related promises and cancel the preload.
  mPreloadResponseAvailablePromiseRequestHolder.DisconnectIfExists();
  mPreloadResponseTimingPromiseRequestHolder.DisconnectIfExists();
  mPreloadResponseEndPromiseRequestHolder.DisconnectIfExists();
  if (mPreloadResponseReadyPromises) {
    RefPtr<FetchService> fetchService = FetchService::GetInstance();
    fetchService->CancelFetch(std::move(mPreloadResponseReadyPromises));
  }

  /**
   * This corresponds to the "Fire Functional Event" algorithm's step 9:
   *
   * "If the time difference in seconds calculated by the current time minus
   * registration's last update check time is greater than 84600, invoke Soft
   * Update algorithm with registration."
   *
   * TODO: this is probably being called later than it should be; it should be
   * called ASAP after dispatching the FetchEvent.
   */
  mRegistration->MaybeScheduleTimeCheckAndUpdate();

  return IPC_OK();
}

void FetchEventOpChild::ActorDestroy(ActorDestroyReason) {
  AssertIsOnMainThread();

  // If `Recv__delete__` was called, it would have resolved the promise already.
  mPromiseHolder.RejectIfExists(NS_ERROR_DOM_ABORT_ERR, __func__);

  if (NS_WARN_IF(!mInterceptedChannelHandled)) {
    Unused << Recv__delete__(NS_ERROR_DOM_ABORT_ERR);
  }
}

nsresult FetchEventOpChild::StartSynthesizedResponse(
    ParentToParentSynthesizeResponseArgs&& aArgs) {
  AssertIsOnMainThread();
  MOZ_ASSERT(mInterceptedChannel);
  MOZ_ASSERT(!mInterceptedChannelHandled);
  MOZ_ASSERT(mRegistration);

  /**
   * TODO: moving the IPCInternalResponse won't do anything right now because
   * there isn't a prefect-forwarding or rvalue-ref-parameter overload of
   * `InternalResponse::FromIPC().`
   */
  SafeRefPtr<InternalResponse> response =
      InternalResponse::FromIPC(aArgs.internalResponse());
  if (NS_WARN_IF(!response)) {
    return NS_ERROR_FAILURE;
  }

  nsCOMPtr<nsIChannel> underlyingChannel;
  nsresult rv =
      mInterceptedChannel->GetChannel(getter_AddRefs(underlyingChannel));
  if (NS_WARN_IF(NS_FAILED(rv)) || NS_WARN_IF(!underlyingChannel)) {
    return NS_FAILED(rv) ? rv : NS_ERROR_FAILURE;
  }

  nsCOMPtr<nsILoadInfo> loadInfo = underlyingChannel->LoadInfo();
  if (!CSPPermitsResponse(loadInfo, response.clonePtr(),
                          mArgs.common().workerScriptSpec())) {
    return NS_ERROR_CONTENT_BLOCKED;
  }

  MOZ_ASSERT(response->GetChannelInfo().IsInitialized());
  ChannelInfo channelInfo = response->GetChannelInfo();
  rv = mInterceptedChannel->SetChannelInfo(&channelInfo);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return NS_ERROR_INTERCEPTION_FAILED;
  }

  rv = mInterceptedChannel->SynthesizeStatus(
      response->GetUnfilteredStatus(), response->GetUnfilteredStatusText());
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  AutoTArray<InternalHeaders::Entry, 5> entries;
  response->UnfilteredHeaders()->GetEntries(entries);
  for (auto& entry : entries) {
    mInterceptedChannel->SynthesizeHeader(entry.mName, entry.mValue);
  }

  auto castLoadInfo = static_cast<mozilla::net::LoadInfo*>(loadInfo.get());
  castLoadInfo->SynthesizeServiceWorkerTainting(response->GetTainting());

  // Get the preferred alternative data type of the outer channel
  nsAutoCString preferredAltDataType(""_ns);
  nsCOMPtr<nsICacheInfoChannel> outerChannel =
      do_QueryInterface(underlyingChannel);
  if (outerChannel &&
      !outerChannel->PreferredAlternativeDataTypes().IsEmpty()) {
    preferredAltDataType.Assign(
        outerChannel->PreferredAlternativeDataTypes()[0].type());
  }

  nsCOMPtr<nsIInputStream> body;
  if (preferredAltDataType.Equals(response->GetAlternativeDataType())) {
    body = response->TakeAlternativeBody();
  }
  if (!body) {
    response->GetUnfilteredBody(getter_AddRefs(body));
  } else {
    Telemetry::ScalarAdd(Telemetry::ScalarID::SW_ALTERNATIVE_BODY_USED_COUNT,
                         1);
  }

  // Propagate the URL to the content if the request mode is not "navigate".
  // Note that, we only reflect the final URL if the response.redirected is
  // false. We propagate all the URLs if the response.redirected is true.
  const IPCInternalRequest& request = mArgs.common().internalRequest();
  nsAutoCString responseURL;
  if (request.requestMode() != RequestMode::Navigate) {
    responseURL = response->GetUnfilteredURL();

    // Similar to how we apply the request fragment to redirects automatically
    // we also want to apply it automatically when propagating the response
    // URL from a service worker interception.  Currently response.url strips
    // the fragment, so this will never conflict with an existing fragment
    // on the response.  In the future we will have to check for a response
    // fragment and avoid overriding in that case.
    if (!request.fragment().IsEmpty() && !responseURL.IsEmpty()) {
      MOZ_ASSERT(!responseURL.Contains('#'));
      responseURL.AppendLiteral("#");
      responseURL.Append(request.fragment());
    }
  }

  nsMainThreadPtrHandle<nsIInterceptedChannel> interceptedChannel(
      new nsMainThreadPtrHolder<nsIInterceptedChannel>(
          "nsIInterceptedChannel", mInterceptedChannel, false));

  nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> registration(
      new nsMainThreadPtrHolder<ServiceWorkerRegistrationInfo>(
          "ServiceWorkerRegistrationInfo", mRegistration, false));

  nsCString requestURL = request.urlList().LastElement();
  if (!request.fragment().IsEmpty()) {
    requestURL.AppendLiteral("#");
    requestURL.Append(request.fragment());
  }

  RefPtr<SynthesizeResponseWatcher> watcher = new SynthesizeResponseWatcher(
      interceptedChannel, registration,
      mArgs.common().isNonSubresourceRequest(), std::move(aArgs.closure()),
      NS_ConvertUTF8toUTF16(responseURL));

  rv = mInterceptedChannel->StartSynthesizedResponse(
      body, watcher, nullptr /* TODO */, responseURL, response->IsRedirected());
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  nsCOMPtr<nsIObserverService> obsService = services::GetObserverService();
  if (obsService) {
    obsService->NotifyObservers(underlyingChannel,
                                "service-worker-synthesized-response", nullptr);
  }

  return rv;
}

void FetchEventOpChild::SynthesizeResponse(
    ParentToParentSynthesizeResponseArgs&& aArgs) {
  AssertIsOnMainThread();
  MOZ_ASSERT(mInterceptedChannel);
  MOZ_ASSERT(!mInterceptedChannelHandled);

  nsresult rv = StartSynthesizedResponse(std::move(aArgs));

  if (NS_WARN_IF(NS_FAILED(rv))) {
    NS_WARNING("Failed to synthesize response!");

    mInterceptedChannel->CancelInterception(rv);
  }

  mInterceptedChannelHandled = true;

  MaybeScheduleRegistrationUpdate();
}

void FetchEventOpChild::ResetInterception(bool aBypass) {
  AssertIsOnMainThread();
  MOZ_ASSERT(mInterceptedChannel);
  MOZ_ASSERT(!mInterceptedChannelHandled);

  nsresult rv = mInterceptedChannel->ResetInterception(aBypass);

  if (NS_WARN_IF(NS_FAILED(rv))) {
    NS_WARNING("Failed to resume intercepted network request!");

    mInterceptedChannel->CancelInterception(rv);
  }

  mInterceptedChannelHandled = true;

  MaybeScheduleRegistrationUpdate();
}

void FetchEventOpChild::CancelInterception(nsresult aStatus) {
  AssertIsOnMainThread();
  MOZ_ASSERT(mInterceptedChannel);
  MOZ_ASSERT(!mInterceptedChannelHandled);
  MOZ_ASSERT(NS_FAILED(aStatus));

  // Report a navigation fault if this is a navigation (and we have an active
  // worker, which should be the case in non-shutdown/content-process-crash
  // situations).
  RefPtr<ServiceWorkerInfo> mActive = mRegistration->GetActive();
  if (mActive && mArgs.common().isNonSubresourceRequest()) {
    mActive->ReportNavigationFault();
    // Additional mitigations such as unregistering the registration are handled
    // in ServiceWorkerRegistrationInfo::MaybeScheduleUpdate which will be
    // called by MaybeScheduleRegistrationUpdate which gets called by our call
    // to ResetInterception.
    if (StaticPrefs::dom_serviceWorkers_mitigations_bypass_on_fault()) {
      ResetInterception(true);
      return;
    }
  }

  mInterceptedChannel->CancelInterception(aStatus);
  mInterceptedChannelHandled = true;

  MaybeScheduleRegistrationUpdate();
}

/**
 * This corresponds to the "Handle Fetch" algorithm's steps 20.3, 21.2, and
 * 22.2:
 *
 * "If request is a non-subresource request, or request is a subresource
 * request and the time difference in seconds calculated by the current time
 * minus registration's last update check time is greater than 86400, invoke
 * Soft Update algorithm with registration."
 */
void FetchEventOpChild::MaybeScheduleRegistrationUpdate() const {
  AssertIsOnMainThread();
  MOZ_ASSERT(mRegistration);
  MOZ_ASSERT(mInterceptedChannelHandled);

  if (mArgs.common().isNonSubresourceRequest()) {
    mRegistration->MaybeScheduleUpdate();
  } else {
    mRegistration->MaybeScheduleTimeCheckAndUpdate();
  }
}

}  // namespace mozilla::dom
