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

#include "FetchLog.h"
#include "FetchObserver.h"
#include "FetchUtil.h"
#include "InternalResponse.h"
#include "Request.h"
#include "Response.h"
#include "mozilla/ConsoleReportCollector.h"
#include "mozilla/SchedulerGroup.h"
#include "mozilla/dom/PerformanceStorage.h"
#include "mozilla/dom/PerformanceTiming.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/RemoteWorkerChild.h"
#include "mozilla/dom/SecurityPolicyViolationEventBinding.h"
#include "mozilla/dom/WorkerChannelInfo.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/dom/WorkerRef.h"
#include "mozilla/dom/WorkerScope.h"
#include "nsIAsyncInputStream.h"
#include "nsIGlobalObject.h"
#include "nsIObserverService.h"
#include "nsIRunnable.h"
#include "nsIURI.h"
#include "nsNetUtil.h"
#include "nsPIDOMWindowInlines.h"
#include "nsThreadUtils.h"

namespace mozilla::dom {

NS_IMPL_ISUPPORTS0(FetchChild)

mozilla::ipc::IPCResult FetchChild::Recv__delete__(const nsresult&& aResult) {
  FETCH_LOG(("FetchChild::Recv__delete__ [%p]", this));
  if (mIsShutdown) {
    return IPC_OK();
  }
  // Shutdown has not been called, so mWorkerRef->Private() should be still
  // alive.
  if (mWorkerRef) {
    MOZ_ASSERT(mWorkerRef->Private());
    mWorkerRef->Private()->AssertIsOnWorkerThread();
  } else {
    MOZ_ASSERT(mIsKeepAliveRequest);
  }

  if (mPromise->State() == Promise::PromiseState::Pending) {
    if (NS_FAILED(aResult)) {
      mPromise->MaybeReject(aResult);
      if (mFetchObserver) {
        mFetchObserver->SetState(FetchState::Errored);
      }
    } else {
      mPromise->MaybeResolve(aResult);
      if (mFetchObserver) {
        mFetchObserver->SetState(FetchState::Complete);
      }
    }
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult FetchChild::RecvOnResponseAvailableInternal(
    ParentToChildInternalResponse&& aResponse) {
  FETCH_LOG(("FetchChild::RecvOnResponseAvailableInternal [%p]", this));
  if (mIsShutdown) {
    return IPC_OK();
  }
  // Shutdown has not been called, so mWorkerRef->Private() should be still
  // alive.
  if (mWorkerRef) {
    MOZ_ASSERT(mWorkerRef->Private());
    mWorkerRef->Private()->AssertIsOnWorkerThread();
  }

  SafeRefPtr<InternalResponse> internalResponse =
      InternalResponse::FromIPC(aResponse);
  IgnoredErrorResult result;
  internalResponse->Headers()->SetGuard(HeadersGuardEnum::Immutable, result);
  MOZ_ASSERT(internalResponse);

  if (internalResponse->Type() != ResponseType::Error) {
    if (internalResponse->Type() == ResponseType::Opaque) {
      internalResponse->GeneratePaddingInfo();
    }

    if (mFetchObserver) {
      mFetchObserver->SetState(FetchState::Complete);
    }

    // mFetchObserver->SetState runs JS and a blocking JS function can run
    // queued runnables, including ActorDestroy that nullifies mPromise.
    if (!mPromise) {
      return IPC_OK();
    }
    nsCOMPtr<nsIGlobalObject> global;
    global = mPromise->GetGlobalObject();
    RefPtr<Response> response =
        new Response(global, internalResponse.clonePtr(), mSignalImpl);
    mPromise->MaybeResolve(response);

    return IPC_OK();
  }

  FETCH_LOG(
      ("FetchChild::RecvOnResponseAvailableInternal [%p] response type is "
       "Error(0x%x)",
       this, static_cast<int32_t>(internalResponse->GetErrorCode())));
  if (mFetchObserver) {
    mFetchObserver->SetState(FetchState::Errored);
  }

  // mFetchObserver->SetState runs JS and a blocking JS function can run queued
  // runnables, including ActorDestroy that nullifies mPromise.
  if (!mPromise) {
    return IPC_OK();
  }
  mPromise->MaybeRejectWithTypeError<MSG_FETCH_FAILED>();
  return IPC_OK();
}

mozilla::ipc::IPCResult FetchChild::RecvOnResponseEnd(ResponseEndArgs&& aArgs) {
  FETCH_LOG(("FetchChild::RecvOnResponseEnd [%p]", this));
  if (mIsShutdown) {
    return IPC_OK();
  }
  // Shutdown has not been called, so mWorkerRef->Private() should be still
  // alive.
  if (mWorkerRef) {
    MOZ_ASSERT(mWorkerRef->Private());
    mWorkerRef->Private()->AssertIsOnWorkerThread();
  }

  if (aArgs.endReason() == FetchDriverObserver::eAborted) {
    FETCH_LOG(
        ("FetchChild::RecvOnResponseEnd [%p] endReason is eAborted", this));
    if (mFetchObserver) {
      mFetchObserver->SetState(FetchState::Errored);
    }

    // mFetchObserver->SetState runs JS and a blocking JS function can run
    // queued runnables, including ActorDestroy that nullifies mPromise.
    if (!mPromise) {
      return IPC_OK();
    }
    mPromise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
  }

  Unfollow();
  return IPC_OK();
}

mozilla::ipc::IPCResult FetchChild::RecvOnDataAvailable() {
  FETCH_LOG(("FetchChild::RecvOnDataAvailable [%p]", this));
  if (mIsShutdown) {
    return IPC_OK();
  }
  // Shutdown has not been called, so mWorkerRef->Private() should be still
  // alive.
  if (mWorkerRef) {
    MOZ_ASSERT(mWorkerRef->Private());
    mWorkerRef->Private()->AssertIsOnWorkerThread();
  }

  if (mFetchObserver && mFetchObserver->State() == FetchState::Requesting) {
    mFetchObserver->SetState(FetchState::Responding);
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult FetchChild::RecvOnFlushConsoleReport(
    nsTArray<net::ConsoleReportCollected>&& aReports) {
  FETCH_LOG(("FetchChild::RecvOnFlushConsoleReport [%p]", this));
  if (mIsShutdown) {
    return IPC_OK();
  }
  MOZ_ASSERT(mReporter);

  if (NS_IsMainThread()) {
    MOZ_ASSERT(mIsKeepAliveRequest);
    // extract doc object to flush the console report
    for (const auto& report : aReports) {
      mReporter->AddConsoleReport(
          report.errorFlags(), report.category(), report.propertiesFile(),
          report.sourceFileURI(), report.lineNumber(), report.columnNumber(),
          report.messageName(), report.stringParams());
    }

    MOZ_ASSERT(mPromise);
    nsCOMPtr<nsPIDOMWindowInner> window =
        do_QueryInterface(mPromise->GetGlobalObject());
    if (window) {
      Document* doc = window->GetExtantDoc();
      mReporter->FlushConsoleReports(doc);
    } else {
      mReporter->FlushReportsToConsole(0);
    }
    return IPC_OK();
  }
  // Shutdown has not been called, so mWorkerRef->Private() should be still
  // alive.
  if (mWorkerRef) {
    MOZ_ASSERT(mWorkerRef->Private());
    mWorkerRef->Private()->AssertIsOnWorkerThread();
  }

  RefPtr<ThreadSafeWorkerRef> workerRef = mWorkerRef;
  nsCOMPtr<nsIConsoleReportCollector> reporter = mReporter;

  nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
      __func__, [reports = std::move(aReports), reporter = std::move(reporter),
                 workerRef = std::move(workerRef)]() mutable {
        for (const auto& report : reports) {
          reporter->AddConsoleReport(
              report.errorFlags(), report.category(), report.propertiesFile(),
              report.sourceFileURI(), report.lineNumber(),
              report.columnNumber(), report.messageName(),
              report.stringParams());
        }

        if (workerRef->Private()->IsServiceWorker()) {
          reporter->FlushReportsToConsoleForServiceWorkerScope(
              workerRef->Private()->ServiceWorkerScope());
        }

        if (workerRef->Private()->IsSharedWorker()) {
          workerRef->Private()
              ->GetRemoteWorkerController()
              ->FlushReportsOnMainThread(reporter);
        }

        reporter->FlushConsoleReports(workerRef->Private()->GetLoadGroup());
      });
  MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(r.forget()));

  return IPC_OK();
}

RefPtr<FetchChild> FetchChild::CreateForWorker(
    WorkerPrivate* aWorkerPrivate, RefPtr<Promise> aPromise,
    RefPtr<AbortSignalImpl> aSignalImpl, RefPtr<FetchObserver> aObserver) {
  MOZ_DIAGNOSTIC_ASSERT(aWorkerPrivate);
  aWorkerPrivate->AssertIsOnWorkerThread();
  FETCH_LOG(("FetchChild::CreateForWorker [%p]", aWorkerPrivate));

  RefPtr<FetchChild> actor = MakeRefPtr<FetchChild>(
      std::move(aPromise), std::move(aSignalImpl), std::move(aObserver));

  RefPtr<StrongWorkerRef> workerRef =
      StrongWorkerRef::Create(aWorkerPrivate, "FetchChild", [actor]() {
        FETCH_LOG(("StrongWorkerRef callback"));
        actor->Shutdown();
      });
  if (NS_WARN_IF(!workerRef)) {
    return nullptr;
  }

  actor->mWorkerRef = new ThreadSafeWorkerRef(workerRef);
  if (NS_WARN_IF(!actor->mWorkerRef)) {
    return nullptr;
  }
  return actor;
}

RefPtr<FetchChild> FetchChild::CreateForMainThread(
    RefPtr<Promise> aPromise, RefPtr<AbortSignalImpl> aSignalImpl,
    RefPtr<FetchObserver> aObserver) {
  RefPtr<FetchChild> actor = MakeRefPtr<FetchChild>(
      std::move(aPromise), std::move(aSignalImpl), std::move(aObserver));
  FETCH_LOG(("FetchChild::CreateForMainThread actor[%p]", actor.get()));

  return actor;
}

mozilla::ipc::IPCResult FetchChild::RecvOnCSPViolationEvent(
    const nsAString& aJSON, const nsAString& aReportGroupName) {
  FETCH_LOG(("FetchChild::RecvOnCSPViolationEvent [%p] aJSON: %s\n", this,
             NS_ConvertUTF16toUTF8(aJSON).get()));

  nsString JSON(aJSON);

  nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(__func__, [JSON]() mutable {
    SecurityPolicyViolationEventInit violationEventInit;
    if (NS_WARN_IF(!violationEventInit.Init(JSON))) {
      return;
    }

    nsCOMPtr<nsIURI> uri;
    nsresult rv =
        NS_NewURI(getter_AddRefs(uri), violationEventInit.mBlockedURI);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return;
    }

    nsCOMPtr<nsIObserverService> observerService =
        mozilla::services::GetObserverService();
    if (!observerService) {
      return;
    }

    rv = observerService->NotifyObservers(
        uri, CSP_VIOLATION_TOPIC, violationEventInit.mViolatedDirective.get());
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return;
    }
  });
  MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(r.forget()));

  if (mCSPEventListener) {
    (void)NS_WARN_IF(NS_FAILED(
        mCSPEventListener->OnCSPViolationEvent(aJSON, aReportGroupName)));
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult FetchChild::RecvOnReportPerformanceTiming(
    ResponseTiming&& aTiming) {
  FETCH_LOG(("FetchChild::RecvOnReportPerformanceTiming [%p]", this));
  if (mIsShutdown) {
    return IPC_OK();
  }
  // Shutdown has not been called, so mWorkerRef->Private() should be still
  // alive.
  if (mWorkerRef) {
    MOZ_ASSERT(mWorkerRef->Private());
    mWorkerRef->Private()->AssertIsOnWorkerThread();

    RefPtr<PerformanceStorage> performanceStorage =
        mWorkerRef->Private()->GetPerformanceStorage();
    if (performanceStorage) {
      performanceStorage->AddEntry(
          aTiming.entryName(), aTiming.initiatorType(),
          MakeUnique<PerformanceTimingData>(aTiming.timingData()));
    }
  } else if (mIsKeepAliveRequest) {
    MOZ_ASSERT(mPromise->GetGlobalObject());
    auto* innerWindow = mPromise->GetGlobalObject()->GetAsInnerWindow();
    if (innerWindow) {
      mozilla::dom::Performance* performance = innerWindow->GetPerformance();
      if (performance) {
        performance->AsPerformanceStorage()->AddEntry(
            aTiming.entryName(), aTiming.initiatorType(),
            MakeUnique<PerformanceTimingData>(aTiming.timingData()));
      }
    }
  }
  return IPC_OK();
}

mozilla::ipc::IPCResult FetchChild::RecvOnNotifyNetworkMonitorAlternateStack(
    uint64_t aChannelID) {
  FETCH_LOG(
      ("FetchChild::RecvOnNotifyNetworkMonitorAlternateStack [%p]", this));
  if (mIsShutdown) {
    return IPC_OK();
  }
  // Shutdown has not been called, so mWorkerRef->Private() should be still
  // alive.
  if (mWorkerRef) {
    MOZ_ASSERT(mWorkerRef->Private());
    mWorkerRef->Private()->AssertIsOnWorkerThread();

    if (!mOriginStack) {
      return IPC_OK();
    }

    if (!mWorkerChannelInfo) {
      mWorkerChannelInfo = MakeRefPtr<WorkerChannelInfo>(
          aChannelID, mWorkerRef->Private()->AssociatedBrowsingContextID());
    }

    // Unfortunately, SerializedStackHolder can only be read on the main thread.
    // However, it doesn't block the fetch execution.
    nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
        __func__, [channel = mWorkerChannelInfo,
                   stack = std::move(mOriginStack)]() mutable {
          NotifyNetworkMonitorAlternateStack(channel, std::move(stack));
        });

    MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(r.forget()));
  } else {
    // Handle main-thread fetch requests
    if (!mOriginStack) {
      return IPC_OK();
    }

    if (!mWorkerChannelInfo) {
      // Get browsing context from the promise's global object
      uint64_t browsingContextID = 0;
      if (mPromise && mPromise->GetGlobalObject()) {
        if (auto* innerWindow =
                mPromise->GetGlobalObject()->GetAsInnerWindow()) {
          if (auto* browsingContext = innerWindow->GetBrowsingContext()) {
            browsingContextID = browsingContext->Id();
          }
        }
      }
      if (browsingContextID == 0) {
        FETCH_LOG(
            ("FetchChild::RecvOnNotifyNetworkMonitorAlternateStack: unable to "
             "get browsingContextID for main-thread fetch, channelID=%" PRIu64,
             aChannelID));
      }
      mWorkerChannelInfo =
          MakeRefPtr<WorkerChannelInfo>(aChannelID, browsingContextID);
    }

    nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
        __func__, [channel = mWorkerChannelInfo,
                   stack = std::move(mOriginStack)]() mutable {
          NotifyNetworkMonitorAlternateStack(channel, std::move(stack));
        });

    MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(r.forget()));
  }

  return IPC_OK();
}

void FetchChild::SetCSPEventListener(nsICSPEventListener* aListener) {
  MOZ_ASSERT(aListener && !mCSPEventListener);
  mCSPEventListener = aListener;
}

FetchChild::FetchChild(RefPtr<Promise>&& aPromise,
                       RefPtr<AbortSignalImpl>&& aSignalImpl,
                       RefPtr<FetchObserver>&& aObserver)
    : mPromise(std::move(aPromise)),
      mSignalImpl(std::move(aSignalImpl)),
      mFetchObserver(std::move(aObserver)),
      mReporter(new ConsoleReportCollector()) {
  FETCH_LOG(("FetchChild::FetchChild [%p]", this));
}

void FetchChild::RunAbortAlgorithm() {
  FETCH_LOG(("FetchChild::RunAbortAlgorithm [%p]", this));
  if (mIsShutdown) {
    return;
  }
  if (mWorkerRef || mIsKeepAliveRequest) {
    (void)SendAbortFetchOp(true);
  }
}

void FetchChild::DoFetchOp(const FetchOpArgs& aArgs) {
  FETCH_LOG(("FetchChild::DoFetchOp [%p]", this));
  // we need to store this for keepalive request
  // as we need to update the load group during actor termination
  mIsKeepAliveRequest = aArgs.request().keepalive();
  if (mIsKeepAliveRequest) {
    mKeepaliveRequestSize =
        aArgs.request().bodySize() > 0 ? aArgs.request().bodySize() : 0;
  }
  if (mSignalImpl) {
    if (mSignalImpl->Aborted()) {
      (void)SendAbortFetchOp(true);
      return;
    }
    Follow(mSignalImpl);
  }
  (void)SendFetchOp(aArgs);
}

void FetchChild::Shutdown() {
  // This is invoked for worker fetch requests only.
  // We need to modify this to be invoked for main-thread fetch requests as
  // well. Typically during global teardown. See Bug 1901082

  FETCH_LOG(("FetchChild::Shutdown [%p]", this));
  if (mIsShutdown) {
    return;
  }
  mIsShutdown.Flip();

  // If mWorkerRef is nullptr here, that means Recv__delete__() must be called
  if (!mWorkerRef) {
    return;
  }
  mPromise = nullptr;
  mFetchObserver = nullptr;
  Unfollow();
  mSignalImpl = nullptr;
  mCSPEventListener = nullptr;
  SendAbortFetchOp(false);

  mWorkerRef = nullptr;
}

void FetchChild::ActorDestroy(ActorDestroyReason aReason) {
  FETCH_LOG(("FetchChild::ActorDestroy [%p]", this));
  // for keepalive request decrement the pending keepalive count
  if (mIsKeepAliveRequest) {
    // For workers we do not have limit per load group rather we have limit
    // per request
    if (NS_IsMainThread()) {
      MOZ_ASSERT(mPromise->GetGlobalObject());
      nsCOMPtr<nsILoadGroup> loadGroup =
          FetchUtil::GetLoadGroupFromGlobal(mPromise->GetGlobalObject());
      if (loadGroup) {
        FetchUtil::DecrementPendingKeepaliveRequestSize(loadGroup,
                                                        mKeepaliveRequestSize);
      }
    }
  }
  mPromise = nullptr;
  mFetchObserver = nullptr;
  mSignalImpl = nullptr;
  mCSPEventListener = nullptr;
  mWorkerRef = nullptr;
}

}  // namespace mozilla::dom
