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

#include "ipc/IPCMessageUtilsSpecializations.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/dom/BrowsingContext.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/FeaturePolicyViolationReportBody.h"
#include "mozilla/dom/PermissionMessageUtils.h"
#include "mozilla/dom/ReportingUtils.h"
#include "nsContentUtils.h"
#include "nsIOService.h"
#include "nsJSUtils.h"

namespace mozilla {
namespace dom {

struct FeatureMap {
  const char* mFeatureName;
  FeaturePolicyUtils::FeaturePolicyValue mDefaultAllowList;
};

/*
 * IMPORTANT: Do not change this list without review from a DOM peer _AND_ a
 * DOM Security peer!
 */
static FeatureMap sSupportedFeatures[] = {
    {"camera", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
    {"geolocation", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
    {"microphone", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
    {"display-capture", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
    {"fullscreen", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
    {"web-share", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
    {"gamepad", FeaturePolicyUtils::FeaturePolicyValue::eAll},
    {"publickey-credentials-create",
     FeaturePolicyUtils::FeaturePolicyValue::eSelf},
    {"publickey-credentials-get",
     FeaturePolicyUtils::FeaturePolicyValue::eSelf},
    {"speaker-selection", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
    {"storage-access", FeaturePolicyUtils::FeaturePolicyValue::eAll},
    {"screen-wake-lock", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
};

/*
 * This is experimental features list, which is disabled by default by pref
 * dom.security.featurePolicy.experimental.enabled.
 */
static FeatureMap sExperimentalFeatures[] = {
    // We don't support 'autoplay' for now, because it would be overwrote by
    // 'user-gesture-activation' policy. However, we can still keep it in the
    // list as we might start supporting it after we use different autoplay
    // policy.
    {"autoplay", FeaturePolicyUtils::FeaturePolicyValue::eAll},
    {"encrypted-media", FeaturePolicyUtils::FeaturePolicyValue::eAll},
    {"midi", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
    {"payment", FeaturePolicyUtils::FeaturePolicyValue::eAll},
    {"document-domain", FeaturePolicyUtils::FeaturePolicyValue::eAll},
    {"vr", FeaturePolicyUtils::FeaturePolicyValue::eAll},
    // https://immersive-web.github.io/webxr/#feature-policy
    {"xr-spatial-tracking", FeaturePolicyUtils::FeaturePolicyValue::eSelf},
};

/* static */
bool FeaturePolicyUtils::IsExperimentalFeature(const nsAString& aFeatureName) {
  uint32_t numFeatures =
      (sizeof(sExperimentalFeatures) / sizeof(sExperimentalFeatures[0]));
  for (uint32_t i = 0; i < numFeatures; ++i) {
    if (aFeatureName.LowerCaseEqualsASCII(
            sExperimentalFeatures[i].mFeatureName)) {
      return true;
    }
  }

  return false;
}

/* static */
bool FeaturePolicyUtils::IsSupportedFeature(const nsAString& aFeatureName) {
  uint32_t numFeatures =
      (sizeof(sSupportedFeatures) / sizeof(sSupportedFeatures[0]));
  for (uint32_t i = 0; i < numFeatures; ++i) {
    if (aFeatureName.LowerCaseEqualsASCII(sSupportedFeatures[i].mFeatureName)) {
      return true;
    }
  }

  return StaticPrefs::dom_security_featurePolicy_experimental_enabled() &&
         IsExperimentalFeature(aFeatureName);
}

/* static */
void FeaturePolicyUtils::ForEachFeature(
    const std::function<void(const char*)>& aCallback) {
  uint32_t numFeatures =
      (sizeof(sSupportedFeatures) / sizeof(sSupportedFeatures[0]));
  for (uint32_t i = 0; i < numFeatures; ++i) {
    aCallback(sSupportedFeatures[i].mFeatureName);
  }

  if (StaticPrefs::dom_security_featurePolicy_experimental_enabled()) {
    numFeatures =
        (sizeof(sExperimentalFeatures) / sizeof(sExperimentalFeatures[0]));
    for (uint32_t i = 0; i < numFeatures; ++i) {
      aCallback(sExperimentalFeatures[i].mFeatureName);
    }
  }
}

/* static */ FeaturePolicyUtils::FeaturePolicyValue
FeaturePolicyUtils::DefaultAllowListFeature(const nsAString& aFeatureName) {
  uint32_t numFeatures =
      (sizeof(sSupportedFeatures) / sizeof(sSupportedFeatures[0]));
  for (uint32_t i = 0; i < numFeatures; ++i) {
    if (aFeatureName.LowerCaseEqualsASCII(sSupportedFeatures[i].mFeatureName)) {
      return sSupportedFeatures[i].mDefaultAllowList;
    }
  }

  if (StaticPrefs::dom_security_featurePolicy_experimental_enabled()) {
    numFeatures =
        (sizeof(sExperimentalFeatures) / sizeof(sExperimentalFeatures[0]));
    for (uint32_t i = 0; i < numFeatures; ++i) {
      if (aFeatureName.LowerCaseEqualsASCII(
              sExperimentalFeatures[i].mFeatureName)) {
        return sExperimentalFeatures[i].mDefaultAllowList;
      }
    }
  }

  return FeaturePolicyValue::eNone;
}

static bool IsSameOriginAsTop(Document* aDocument) {
  MOZ_ASSERT(aDocument);

  BrowsingContext* browsingContext = aDocument->GetBrowsingContext();
  if (!browsingContext) {
    return false;
  }

  nsPIDOMWindowOuter* topWindow = browsingContext->Top()->GetDOMWindow();
  if (!topWindow) {
    // If we don't have a DOMWindow, We are not in same origin.
    return false;
  }

  Document* topLevelDocument = topWindow->GetExtantDoc();
  if (!topLevelDocument) {
    return false;
  }

  return NS_SUCCEEDED(
      nsContentUtils::CheckSameOrigin(topLevelDocument, aDocument));
}

/* static */
bool FeaturePolicyUtils::IsFeatureUnsafeAllowedAll(
    Document* aDocument, const nsAString& aFeatureName) {
  MOZ_ASSERT(aDocument);

  if (!aDocument->IsHTMLDocument()) {
    return false;
  }

  FeaturePolicy* policy = aDocument->FeaturePolicy();
  MOZ_ASSERT(policy);

  return policy->HasFeatureUnsafeAllowsAll(aFeatureName) &&
         !policy->IsSameOriginAsSrc(aDocument->NodePrincipal()) &&
         !policy->AllowsFeatureExplicitlyInAncestorChain(
             aFeatureName, policy->DefaultOrigin()) &&
         !IsSameOriginAsTop(aDocument);
}

/* static */
bool FeaturePolicyUtils::IsFeatureAllowed(Document* aDocument,
                                          const nsAString& aFeatureName) {
  MOZ_ASSERT(aDocument);

  // Skip apply features in experimental phase
  if (!StaticPrefs::dom_security_featurePolicy_experimental_enabled() &&
      IsExperimentalFeature(aFeatureName)) {
    return true;
  }

  FeaturePolicy* policy = aDocument->FeaturePolicy();
  MOZ_ASSERT(policy);

  if (policy->AllowsFeatureInternal(aFeatureName, policy->DefaultOrigin())) {
    return true;
  }

  ReportViolation(aDocument, aFeatureName);
  return false;
}

/* static */
void FeaturePolicyUtils::ReportViolation(Document* aDocument,
                                         const nsAString& aFeatureName) {
  MOZ_ASSERT(aDocument);

  nsCOMPtr<nsIURI> uri = aDocument->GetDocumentURI();
  if (NS_WARN_IF(!uri)) {
    return;
  }

  // Strip the URL of any possible username/password and make it ready to be
  // presented in the UI.
  nsCOMPtr<nsIURI> exposableURI = net::nsIOService::CreateExposableURI(uri);
  nsAutoCString spec;
  nsresult rv = exposableURI->GetSpec(spec);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return;
  }
  JSContext* cx = nsContentUtils::GetCurrentJSContext();
  if (NS_WARN_IF(!cx)) {
    return;
  }

  Nullable<int32_t> lineNumber;
  Nullable<int32_t> columnNumber;
  auto loc = JSCallingLocation::Get();
  if (loc) {
    lineNumber.SetValue(static_cast<int32_t>(loc.mLine));
    columnNumber.SetValue(static_cast<int32_t>(loc.mColumn));
  }

  nsPIDOMWindowInner* window = aDocument->GetInnerWindow();
  if (NS_WARN_IF(!window)) {
    return;
  }

  RefPtr<FeaturePolicyViolationReportBody> body =
      new FeaturePolicyViolationReportBody(window->AsGlobal(), aFeatureName,
                                           loc.FileName(), lineNumber,
                                           columnNumber, u"enforce"_ns);

  ReportingUtils::Report(window->AsGlobal(), nsGkAtoms::featurePolicyViolation,
                         u"default"_ns, NS_ConvertUTF8toUTF16(spec), body);
}

}  // namespace dom
}  // namespace mozilla

namespace IPC {

void ParamTraits<mozilla::dom::FeaturePolicyInfo>::Write(
    MessageWriter* aWriter, const mozilla::dom::FeaturePolicyInfo& aParam) {
  WriteParam(aWriter, aParam.mInheritedDeniedFeatureNames);
  WriteParam(aWriter, aParam.mAttributeEnabledFeatureNames);
  WriteParam(aWriter, aParam.mDeclaredString);
  WriteParam(aWriter, aParam.mDefaultOrigin);
  WriteParam(aWriter, aParam.mSelfOrigin);
  WriteParam(aWriter, aParam.mSrcOrigin);
}

bool ParamTraits<mozilla::dom::FeaturePolicyInfo>::Read(
    MessageReader* aReader, mozilla::dom::FeaturePolicyInfo* aResult) {
  return ReadParam(aReader, &aResult->mInheritedDeniedFeatureNames) &&
         ReadParam(aReader, &aResult->mAttributeEnabledFeatureNames) &&
         ReadParam(aReader, &aResult->mDeclaredString) &&
         ReadParam(aReader, &aResult->mDefaultOrigin) &&
         ReadParam(aReader, &aResult->mSelfOrigin) &&
         ReadParam(aReader, &aResult->mSrcOrigin);
}

}  // namespace IPC
