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

#include "GMPServiceParent.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/glean/GleanPings.h"
#include "mozilla/glean/TelemetryMetrics.h"
#include "mozilla/MozPromise.h"
#include "mozilla/net/SocketProcessParent.h"
#include "mozilla/ipc/UtilityProcessParent.h"
#include "mozilla/ipc/UtilityProcessManager.h"
#include "mozilla/RDDChild.h"
#include "mozilla/RDDProcessManager.h"
#include "mozilla/WinDllServices.h"
#include "nsISupportsImpl.h"
#include "nsProxyRelease.h"
#include "nsXULAppAPI.h"
#include "UntrustedModulesDataSerializer.h"

namespace mozilla {
namespace Telemetry {

static const uint32_t kMaxModulesArrayLen = 100;

using UntrustedModulesIpcPromise =
    MozPromise<Maybe<UntrustedModulesData>, ipc::ResponseRejectReason, true>;

using MultiGetUntrustedModulesPromise =
    MozPromise<bool /*aIgnored*/, nsresult, true>;

class MOZ_HEAP_CLASS MultiGetUntrustedModulesData final {
 public:
  /**
   * @param aFlags [in] Combinations of the flags defined under nsITelemetry.
   *               (See "Flags for getUntrustedModuleLoadEvents"
   *                in nsITelemetry.idl)
   */
  explicit MultiGetUntrustedModulesData(uint32_t aFlags)
      : mFlags(aFlags),
        mBackupSvc(UntrustedModulesBackupService::Get()),
        mPromise(new MultiGetUntrustedModulesPromise::Private(__func__)),
        mNumPending(0) {}

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MultiGetUntrustedModulesData)

  RefPtr<MultiGetUntrustedModulesPromise> GetUntrustedModuleLoadEvents();
  void Serialize(RefPtr<dom::Promise>&& aPromise);

  /**
   * Submit a "third-party-modules" ping with any already-gotten data.
   */
  nsresult SubmitToGlean();

  MultiGetUntrustedModulesData(const MultiGetUntrustedModulesData&) = delete;
  MultiGetUntrustedModulesData(MultiGetUntrustedModulesData&&) = delete;
  MultiGetUntrustedModulesData& operator=(const MultiGetUntrustedModulesData&) =
      delete;
  MultiGetUntrustedModulesData& operator=(MultiGetUntrustedModulesData&&) =
      delete;

 private:
  ~MultiGetUntrustedModulesData() = default;

  void AddPending(RefPtr<UntrustedModulesPromise>&& aNewPending) {
    MOZ_ASSERT(NS_IsMainThread());

    ++mNumPending;

    RefPtr<MultiGetUntrustedModulesData> self(this);
    aNewPending->Then(
        GetMainThreadSerialEventTarget(), __func__,
        [self](Maybe<UntrustedModulesData>&& aResult) {
          self->OnCompletion(std::move(aResult));
        },
        [self](nsresult aReason) { self->OnCompletion(); });
  }

  void AddPending(RefPtr<UntrustedModulesIpcPromise>&& aNewPending) {
    MOZ_ASSERT(NS_IsMainThread());

    ++mNumPending;

    RefPtr<MultiGetUntrustedModulesData> self(this);
    aNewPending->Then(
        GetMainThreadSerialEventTarget(), __func__,
        [self](Maybe<UntrustedModulesData>&& aResult) {
          self->OnCompletion(std::move(aResult));
        },
        [self](ipc::ResponseRejectReason&& aReason) { self->OnCompletion(); });
  }

  void OnCompletion() {
    MOZ_ASSERT(NS_IsMainThread() && mNumPending > 0);

    --mNumPending;
    if (mNumPending) {
      return;
    }

    mPromise->Resolve(true, __func__);
  }

  void OnCompletion(Maybe<UntrustedModulesData>&& aResult) {
    MOZ_ASSERT(NS_IsMainThread());

    if (aResult.isSome()) {
      mBackupSvc->Backup(std::move(aResult.ref()));
    }

    OnCompletion();
  }

 private:
  // Combinations of the flags defined under nsITelemetry.
  // (See "Flags for getUntrustedModuleLoadEvents" in nsITelemetry.idl)
  uint32_t mFlags;

  RefPtr<UntrustedModulesBackupService> mBackupSvc;
  RefPtr<MultiGetUntrustedModulesPromise::Private> mPromise;
  size_t mNumPending;
};

RefPtr<MultiGetUntrustedModulesPromise>
MultiGetUntrustedModulesData::GetUntrustedModuleLoadEvents() {
  MOZ_ASSERT(XRE_IsParentProcess() && NS_IsMainThread());

  // Parent process
  RefPtr<DllServices> dllSvc(DllServices::Get());
  AddPending(dllSvc->GetUntrustedModulesData());

  // Child processes
  nsTArray<dom::ContentParent*> contentParents;
  dom::ContentParent::GetAll(contentParents);
  for (auto&& contentParent : contentParents) {
    AddPending(contentParent->SendGetUntrustedModulesData());
  }

  if (RefPtr<net::SocketProcessParent> socketActor =
          net::SocketProcessParent::GetSingleton()) {
    AddPending(socketActor->SendGetUntrustedModulesData());
  }

  if (RDDProcessManager* rddMgr = RDDProcessManager::Get()) {
    if (RDDChild* rddChild = rddMgr->GetRDDChild()) {
      AddPending(rddChild->SendGetUntrustedModulesData());
    }
  }

  if (RefPtr<ipc::UtilityProcessManager> utilityManager =
          ipc::UtilityProcessManager::GetIfExists()) {
    for (RefPtr<ipc::UtilityProcessParent>& parent :
         utilityManager->GetAllProcessesProcessParent()) {
      AddPending(parent->SendGetUntrustedModulesData());
    }
  }

  if (RefPtr<gmp::GeckoMediaPluginServiceParent> gmps =
          gmp::GeckoMediaPluginServiceParent::GetSingleton()) {
    nsTArray<RefPtr<
        gmp::GeckoMediaPluginServiceParent::GetUntrustedModulesDataPromise>>
        promises;
    gmps->SendGetUntrustedModulesData(promises);
    for (auto& promise : promises) {
      AddPending(std::move(promise));
    }
  }

  return mPromise;
}

#if defined(XP_WIN)
nsTArray<nsDependentSubstring> GetBlockedModules() {
  nsTArray<nsDependentSubstring> blockedModules;
  RefPtr<DllServices> dllSvc(DllServices::Get());
  nt::SharedSection* sharedSection = dllSvc->GetSharedSection();
  if (!sharedSection) {
    return blockedModules;
  }

  auto dynamicBlocklist = sharedSection->GetDynamicBlocklist();
  for (const auto& blockedEntry : dynamicBlocklist) {
    if (!blockedEntry.IsValidDynamicBlocklistEntry()) {
      break;
    }
    blockedModules.AppendElement(
        nsDependentSubstring(blockedEntry.mName.Buffer,
                             blockedEntry.mName.Length / sizeof(wchar_t)));
  }
  return blockedModules;
}
#endif

nsresult MultiGetUntrustedModulesData::SubmitToGlean() {
  MOZ_ASSERT(mFlags == 0,
             "The Glean 'third-party-modules' ping doesn't know how to handle "
             "nsITelemetry's flags for getUntrustedModuleLoadEvents.");

  const UntrustedModulesBackupData& stagingRef = mBackupSvc->Staging();
  if (stagingRef.IsEmpty()) {
    return NS_OK;
  }
  glean::third_party_modules::ModulesObject modules;
  glean::third_party_modules::ProcessesObject processes;
  uint32_t modulesArrayIdx = 0;
  for (const auto& container : stagingRef.Values()) {
    if (!container) {
      continue;
    }
    const auto& data = container->mData;
    // We are duplicating the module mapping that
    // UntrustedModulesDataSerializer::AddSingleData does because
    // 1) Accessing its mIndexMap at the correct time would be fragile.
    // 2) Decoupling data submission from JS serialization is arguably good.
    nsTHashMap<nsStringHashKey, uint32_t> indexMap;
    for (const auto& entry : data.mModules) {
      nsresult rv = NS_OK;
      (void)indexMap.LookupOrInsertWith(entry.GetKey(), [&]() -> uint32_t {
        const auto record = entry.GetData();
        if (!record) {
          rv = NS_ERROR_FAILURE;
          return 0;
        }
        glean::third_party_modules::ModulesObjectItem item;
        item.resolvedDllName =
            Some(NS_ConvertUTF16toUTF8(record->mSanitizedDllName));
        if (record->mVersion.isSome()) {
          auto [major, minor, patch, build] = record->mVersion->AsTuple();
          nsCString version;
          version.AppendPrintf("%" PRIu16 ".%" PRIu16 ".%" PRIu16 ".%" PRIu16,
                               major, minor, patch, build);
          item.fileVersion = Some(version);
        }
        if (record->mVendorInfo.isSome()) {
          MOZ_ASSERT(!record->mVendorInfo->mVendor.IsEmpty());
          if (record->mVendorInfo->mVendor.IsEmpty()) {
            // Per `SerializeModule`, this is an error condition severe enough
            // to cease further processing.
            rv = NS_ERROR_FAILURE;
            return 0;
          }
          switch (record->mVendorInfo->mSource) {
            case VendorInfo::Source::Signature:
              item.signedBy =
                  Some(NS_ConvertUTF16toUTF8(record->mVendorInfo->mVendor));
              break;
            case VendorInfo::Source::VersionInfo:
              item.companyName =
                  Some(NS_ConvertUTF16toUTF8(record->mVendorInfo->mVendor));
              break;
            default:
              MOZ_ASSERT_UNREACHABLE("Unknown VendorInfo Source!");
              rv = NS_ERROR_FAILURE;
              return 0;
          }
        }
        item.trustFlags = Some(static_cast<int64_t>(record->mTrustFlags));
        modules.EmplaceBack(item);
        return modulesArrayIdx++;
      });

      if (modulesArrayIdx > kMaxModulesArrayLen) {
        rv = NS_ERROR_CANNOT_CONVERT_DATA;
      }

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

    nsDependentCString processType;
    if (data.mProcessType == GeckoProcessType_Default) {
      processType.Rebind("browser"_ns, 0);
    } else {
      processType.Rebind(XRE_GeckoProcessTypeToString(data.mProcessType));
    }

    // UntrusteModulesDataSerializer::Add allows for multiple containers' data
    // to be for the same process (type and pid).
    // When that happens, the latest containers' data would take precedence.
    // By intuition, I don't think that happens so we don't need to worry.
    // And if it does, we'll get two items in the `processes` array:
    // analyses may merely take the latest one.
    glean::third_party_modules::ProcessesObjectItem process{
        .processType = Some(processType),
        .sanitizationFailures = Some(data.mSanitizationFailures),
        .trustTestFailures = Some(data.mTrustTestFailures),
    };

    nsCString strPid(processType);
    strPid.AppendLiteral(".0x");
    strPid.AppendInt(static_cast<uint32_t>(data.mPid), 16);
    process.processName = Some(strPid);

    nsCString elapsed;
    elapsed.AppendFloat(data.mElapsed.ToSeconds());
    process.elapsed = Some(elapsed);

    if (data.mXULLoadDurationMS.isSome()) {
      nsCString xulLoadDuration;
      xulLoadDuration.AppendFloat(*(data.mXULLoadDurationMS));
      process.xulLoadDurationMS = Some(xulLoadDuration);
    }

    glean::third_party_modules::ProcessesObjectItemEvents eventsArray;
    for (const auto& eventContainer : data.mEvents) {
      const ProcessedModuleLoadEvent& event = eventContainer->mEvent;
      if (!event) {
        // Event has no module.
        continue;
      }
      glean::third_party_modules::ProcessesObjectItemEventsItem eventItem{
          .processUptimeMS = Some(static_cast<int64_t>(event.mProcessUptimeMS)),
          // TODO: Document the narrowing conversion (2^64ms is ~584MY)
          .threadID = Some(event.mThreadId),
          .requestedDllName =
              Some(NS_ConvertUTF16toUTF8(event.mRequestedDllName)),
          .isDependent = Some(event.mIsDependent),
          .loadStatus = Some(event.mLoadStatus),
      };

      if (event.mLoadDurationMS.isSome()) {
        nsCString loadDuration;
        loadDuration.AppendFloat(*(event.mLoadDurationMS));
        eventItem.loadDurationMS = Some(loadDuration);
      }

      nsDependentCString effectiveThreadName;
      if (event.mThreadId == ::GetCurrentThreadId()) {
        effectiveThreadName.Rebind("Main Thread"_ns, 0);
      } else {
        effectiveThreadName.Rebind(event.mThreadName, 0);
      }
      if (!effectiveThreadName.IsEmpty()) {
        eventItem.threadName = Some(event.mThreadName);
      }

      if (!event.mRequestedDllName.IsEmpty() &&
          !event.mRequestedDllName.Equals(event.mModule->mSanitizedDllName,
                                          nsCaseInsensitiveStringComparator)) {
        // TODO: Truncate to MAX_PATH - 3 + "..."
        eventItem.requestedDllName =
            Some(NS_ConvertUTF16toUTF8(event.mRequestedDllName));
      }

      nsAutoCString baseAddress("0x");
      baseAddress.AppendInt(event.mBaseAddress, 16);
      eventItem.baseAddress = Some(baseAddress);

      uint32_t moduleIndex;
      if (!indexMap.Get(event.mModule->mResolvedNtName, &moduleIndex)) {
        // TODO: Should we instrument this somehow?
        continue;
      }
      eventItem.moduleIndex = Some(moduleIndex);

      eventsArray.AppendElement(eventItem);
    }
    if (eventsArray.Length()) {
      process.events = Some(std::move(eventsArray));
    }

    glean::third_party_modules::ProcessesObjectItemCombinedstacks
        combinedStacks;
    glean::third_party_modules::ProcessesObjectItemCombinedstacksMemorymap
        memoryMap;
    const size_t moduleCount = data.mStacks.GetModuleCount();
    for (size_t moduleIndex = 0; moduleIndex < moduleCount; ++moduleIndex) {
      const auto& module = data.mStacks.GetModule(moduleIndex);
      memoryMap.EmplaceBack(nsTArray<nsCString>{
          NS_ConvertUTF16toUTF8(module.mName),
          module.mBreakpadId,
      });
    }
    combinedStacks.memoryMap = Some(std::move(memoryMap));

    const size_t stackCount = data.mStacks.GetStackCount();
    glean::third_party_modules::ProcessesObjectItemCombinedstacksStacks
        stackArray;
    for (size_t stackIndex = 0; stackIndex < stackCount; ++stackIndex) {
      nsTArray<nsTArray<int64_t>> pcArray;
      const CombinedStacks::Stack& stack = data.mStacks.GetStack(stackIndex);
      const uint32_t pcCount = stack.size();
      for (size_t pcIndex = 0; pcIndex < pcCount; ++pcIndex) {
        const ProcessedStack::Frame& frame = stack[pcIndex];
        const int64_t modIndex =
            (std::numeric_limits<uint16_t>::max() == frame.mModIndex)
                ? -1
                : frame.mModIndex;
        pcArray.EmplaceBack(nsTArray<int64_t>{
            modIndex,
            static_cast<int64_t>(frame.mOffset),
        });
      }
      stackArray.AppendElement(std::move(pcArray));
    }
    combinedStacks.stacks = Some(std::move(stackArray));

    process.combinedStacks = Some(std::move(combinedStacks));
    processes.AppendElement(std::move(process));
  }
  glean::third_party_modules::modules.Set(std::move(modules));
  glean::third_party_modules::processes.Set(std::move(processes));

#if defined(XP_WIN)
  nsTArray<nsDependentSubstring> uBlockedModules = GetBlockedModules();
  nsTArray<nsCString> blockedModules;
  for (const auto& blockedModule : uBlockedModules) {
    blockedModules.EmplaceBack(NS_ConvertUTF16toUTF8(blockedModule));
  }
  glean::third_party_modules::blocked_modules.Set(blockedModules);
#endif
  glean_pings::ThirdPartyModules.Submit();
  return NS_OK;
}

void MultiGetUntrustedModulesData::Serialize(RefPtr<dom::Promise>&& aPromise) {
  MOZ_ASSERT(NS_IsMainThread());

  dom::AutoJSAPI jsapi;
  if (NS_WARN_IF(!jsapi.Init(aPromise->GetGlobalObject()))) {
    aPromise->MaybeReject(NS_ERROR_FAILURE);
    return;
  }

  JSContext* cx = jsapi.cx();
  UntrustedModulesDataSerializer serializer(cx, kMaxModulesArrayLen, mFlags);
  if (!serializer) {
    aPromise->MaybeReject(NS_ERROR_FAILURE);
    return;
  }

  nsresult rv;
  if (mFlags & nsITelemetry::INCLUDE_OLD_LOADEVENTS) {
    // When INCLUDE_OLD_LOADEVENTS is set, we need to return instances
    // from both "Staging" and "Settled" backup.
    if (mFlags & nsITelemetry::KEEP_LOADEVENTS_NEW) {
      // When INCLUDE_OLD_LOADEVENTS and KEEP_LOADEVENTS_NEW are set, we need to
      // return a JS object consisting of all instances from both "Staging" and
      // "Settled" backups, keeping instances in those backups as is.
      if (mFlags & nsITelemetry::EXCLUDE_STACKINFO_FROM_LOADEVENTS) {
        // Without the stack info, we can add multiple UntrustedModulesData to
        // the serializer directly.
        rv = serializer.Add(mBackupSvc->Staging());
        if (NS_WARN_IF(NS_FAILED(rv))) {
          aPromise->MaybeReject(rv);
          return;
        }
        rv = serializer.Add(mBackupSvc->Settled());
        if (NS_WARN_IF(NS_FAILED(rv))) {
          aPromise->MaybeReject(rv);
          return;
        }
      } else {
        // Currently we don't have a method to merge UntrustedModulesData into
        // a serialized JS object because merging CombinedStack will be tricky.
        // Thus we return an error on this flag combination.
        aPromise->MaybeReject(NS_ERROR_INVALID_ARG);
        return;
      }
    } else {
      // When KEEP_LOADEVENTS_NEW is not set, we can move data from "Staging"
      // to "Settled" first, then add "Settled" to the serializer.
      mBackupSvc->SettleAllStagingData();

      const UntrustedModulesBackupData& settledRef = mBackupSvc->Settled();
      if (settledRef.IsEmpty()) {
        aPromise->MaybeReject(NS_ERROR_NOT_AVAILABLE);
        return;
      }

      rv = serializer.Add(settledRef);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        aPromise->MaybeReject(rv);
        return;
      }
    }
  } else {
    // When INCLUDE_OLD_LOADEVENTS is not set, we serialize only the "Staging"
    // into a JS object.
    const UntrustedModulesBackupData& stagingRef = mBackupSvc->Staging();

    if (stagingRef.IsEmpty()) {
      aPromise->MaybeReject(NS_ERROR_NOT_AVAILABLE);
      return;
    }

    rv = serializer.Add(stagingRef);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      aPromise->MaybeReject(rv);
      return;
    }

    // When KEEP_LOADEVENTS_NEW is not set, we move all "Staging" instances
    // to the "Settled".
    if (!(mFlags & nsITelemetry::KEEP_LOADEVENTS_NEW)) {
      mBackupSvc->SettleAllStagingData();
    }
  }

#if defined(XP_WIN)
  nsTArray<nsDependentSubstring> blockedModules = GetBlockedModules();
  if (!blockedModules.IsEmpty()) {
    rv = serializer.AddBlockedModules(blockedModules);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      aPromise->MaybeReject(rv);
      return;
    }
  }
#endif

  JS::Rooted<JS::Value> jsval(cx);
  serializer.GetObject(&jsval);
  aPromise->MaybeResolve(jsval);
}

nsresult MaybeSubmitAndGetUntrustedModulePayload(JSContext* aCx,
                                                 dom::Promise** aPromise,
                                                 uint32_t aFlags,
                                                 bool aSubmitGleanPing) {
  // Create a promise using global context.
  nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
  if (NS_WARN_IF(!global)) {
    return NS_ERROR_FAILURE;
  }

  ErrorResult result;
  RefPtr<dom::Promise> promise(dom::Promise::Create(global, result));
  if (NS_WARN_IF(result.Failed())) {
    return result.StealNSResult();
  }

  auto multi = MakeRefPtr<MultiGetUntrustedModulesData>(aFlags);
  multi->GetUntrustedModuleLoadEvents()->Then(
      GetMainThreadSerialEventTarget(), __func__,
      [promise, multi, aSubmitGleanPing](bool) mutable {
        if (aSubmitGleanPing) {
          nsresult rv = multi->SubmitToGlean();
          if (NS_FAILED(rv)) {
            promise->MaybeReject(rv);
            return;
          }
        }
        multi->Serialize(std::move(promise));
      },
      [promise](nsresult aRv) { promise->MaybeReject(aRv); });

  promise.forget(aPromise);
  return NS_OK;
}

nsresult SubmitAndGetUntrustedModulePayload(JSContext* aCx,
                                            dom::Promise** aPromise) {
  return MaybeSubmitAndGetUntrustedModulePayload(aCx, aPromise, 0, true);
}

nsresult GetUntrustedModuleLoadEvents(uint32_t aFlags, JSContext* aCx,
                                      dom::Promise** aPromise) {
  return MaybeSubmitAndGetUntrustedModulePayload(aCx, aPromise, aFlags, false);
}

}  // namespace Telemetry
}  // namespace mozilla
