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

#include "mozilla/dom/DOMTypes.h"
#include "mozilla/ipc/IPDLParamTraits.h"
#include "mozilla/dom/FeaturePolicyViolationReportBody.h"
#include "mozilla/dom/ReportingUtils.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/dom/Document.h"
#include "nsContentUtils.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},
    {"speaker-selection", 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;
  }

  nsAutoString fileName;
  Nullable<int32_t> lineNumber;
  Nullable<int32_t> columnNumber;
  uint32_t line = 0;
  uint32_t column = 0;
  if (nsJSUtils::GetCallingLocation(cx, fileName, &line, &column)) {
    lineNumber.SetValue(static_cast<int32_t>(line));
    columnNumber.SetValue(static_cast<int32_t>(column));
  }

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

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

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

}  // namespace dom

namespace ipc {
void IPDLParamTraits<dom::FeaturePolicy*>::Write(IPC::MessageWriter* aWriter,
                                                 IProtocol* aActor,
                                                 dom::FeaturePolicy* aParam) {
  if (!aParam) {
    WriteIPDLParam(aWriter, aActor, false);
    return;
  }

  WriteIPDLParam(aWriter, aActor, true);

  dom::FeaturePolicyInfo info;
  info.defaultOrigin() = aParam->DefaultOrigin();
  info.selfOrigin() = aParam->GetSelfOrigin();
  info.srcOrigin() = aParam->GetSrcOrigin();

  info.declaredString() = aParam->DeclaredString();
  info.inheritedDeniedFeatureNames() =
      aParam->InheritedDeniedFeatureNames().Clone();
  info.attributeEnabledFeatureNames() =
      aParam->AttributeEnabledFeatureNames().Clone();

  WriteIPDLParam(aWriter, aActor, info);
}

bool IPDLParamTraits<dom::FeaturePolicy*>::Read(
    IPC::MessageReader* aReader, IProtocol* aActor,
    RefPtr<dom::FeaturePolicy>* aResult) {
  *aResult = nullptr;
  bool notnull = false;
  if (!ReadIPDLParam(aReader, aActor, &notnull)) {
    return false;
  }

  if (!notnull) {
    return true;
  }

  dom::FeaturePolicyInfo info;
  if (!ReadIPDLParam(aReader, aActor, &info)) {
    return false;
  }

  // Note that we only do IPC for feature policy to inherit policy from parent
  // to child document. That does not need to bind feature policy with a node.
  RefPtr<dom::FeaturePolicy> featurePolicy = new dom::FeaturePolicy(nullptr);
  featurePolicy->SetDefaultOrigin(info.defaultOrigin());
  featurePolicy->SetInheritedDeniedFeatureNames(
      info.inheritedDeniedFeatureNames());

  const auto& declaredString = info.declaredString();
  if (info.selfOrigin() && !declaredString.IsEmpty()) {
    featurePolicy->SetDeclaredPolicy(nullptr, declaredString, info.selfOrigin(),
                                     info.srcOrigin());
  }

  for (auto& featureName : info.attributeEnabledFeatureNames()) {
    featurePolicy->MaybeSetAllowedPolicy(featureName);
  }

  *aResult = std::move(featurePolicy);
  return true;
}
}  // namespace ipc

}  // namespace mozilla
