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

#include <windows.h>
#include <shlwapi.h>
#include <fstream>

#include "common.h"
#include "Registry.h"
#include "UtfConvert.h"

#include "json/json.h"
#include "mozilla/WinHeaderOnlyUtils.h"

// There is little logging or error handling in this file, because the file and
// registry values we are reading here are normally absent, so never finding
// anything that we look for at all would not be an error worth generating an
// event log for.

#define AGENT_POLICY_NAME "DisableDefaultBrowserAgent"
#define TELEMETRY_POLICY_NAME "DisableTelemetry"

// The Firefox policy engine hardcodes the string "Mozilla" in its registry
// key accesses rather than using the configured vendor name, so we should do
// the same here to be sure we're compatible with it.
#define POLICY_REGKEY_NAME L"SOFTWARE\\Policies\\Mozilla\\" MOZ_APP_BASENAME

namespace mozilla::default_agent {

// This enum is the return type for the functions that check policy values.
enum class PolicyState {
  Enabled,   // There is a policy explicitly set to enabled
  Disabled,  // There is a policy explicitly set to disabled
  NoPolicy,  // This policy isn't configured
};

static PolicyState FindPolicyInRegistry(HKEY rootKey,
                                        const wchar_t* policyName) {
  HKEY rawRegKey = nullptr;
  RegOpenKeyExW(rootKey, POLICY_REGKEY_NAME, 0, KEY_READ, &rawRegKey);

  nsAutoRegKey regKey(rawRegKey);

  if (!regKey) {
    return PolicyState::NoPolicy;
  }

  // If this key is empty and doesn't have any actual policies in it,
  // treat that the same as the key not existing and return no result.
  DWORD numSubKeys = 0, numValues = 0;
  LSTATUS ls = RegQueryInfoKeyW(regKey.get(), nullptr, nullptr, nullptr,
                                &numSubKeys, nullptr, nullptr, &numValues,
                                nullptr, nullptr, nullptr, nullptr);
  if (ls != ERROR_SUCCESS) {
    return PolicyState::NoPolicy;
  }

  DWORD policyValue = UINT32_MAX;
  DWORD policyValueSize = sizeof(policyValue);
  ls = RegGetValueW(regKey.get(), nullptr, policyName, RRF_RT_REG_DWORD,
                    nullptr, &policyValue, &policyValueSize);

  if (ls != ERROR_SUCCESS) {
    return PolicyState::NoPolicy;
  }
  return policyValue == 0 ? PolicyState::Disabled : PolicyState::Enabled;
}

static PolicyState FindPolicyInFile(const char* policyName) {
  mozilla::UniquePtr<wchar_t[]> thisBinaryPath = mozilla::GetFullBinaryPath();
  if (!PathRemoveFileSpecW(thisBinaryPath.get())) {
    return PolicyState::NoPolicy;
  }

  wchar_t policiesFilePath[MAX_PATH] = L"";
  if (!PathCombineW(policiesFilePath, thisBinaryPath.get(), L"distribution")) {
    return PolicyState::NoPolicy;
  }

  if (!PathAppendW(policiesFilePath, L"policies.json")) {
    return PolicyState::NoPolicy;
  }

  // We need a narrow string-based std::ifstream because that's all jsoncpp can
  // use; that means we need to supply it the file path as a narrow string.
  Utf16ToUtf8Result policiesFilePathToUtf8 = Utf16ToUtf8(policiesFilePath);
  if (policiesFilePathToUtf8.isErr()) {
    return PolicyState::NoPolicy;
  }
  std::string policiesFilePathA = policiesFilePathToUtf8.unwrap();

  Json::Value jsonRoot;
  std::ifstream stream(policiesFilePathA);
  Json::Reader().parse(stream, jsonRoot);

  if (jsonRoot.isObject() && jsonRoot.isMember("Policies") &&
      jsonRoot["Policies"].isObject()) {
    if (jsonRoot["Policies"].isMember(policyName) &&
        jsonRoot["Policies"][policyName].isBool()) {
      return jsonRoot["Policies"][policyName].asBool() ? PolicyState::Enabled
                                                       : PolicyState::Disabled;
    } else {
      return PolicyState::NoPolicy;
    }
  }

  return PolicyState::NoPolicy;
}

static PolicyState IsDisabledByPref(const wchar_t* prefRegValue) {
  auto prefValueResult =
      RegistryGetValueBool(IsPrefixed::Prefixed, prefRegValue);

  if (prefValueResult.isErr()) {
    return PolicyState::NoPolicy;
  }
  auto prefValue = prefValueResult.unwrap();
  if (prefValue.isNothing()) {
    return PolicyState::NoPolicy;
  }
  return prefValue.value() ? PolicyState::Enabled : PolicyState::Disabled;
}

// Everything we call from this function wants wide strings, except for jsoncpp,
// which cannot work with them at all, so at some point we need both formats.
// It's awkward to take both formats as individual arguments, but it would be
// more awkward to take one and runtime convert it to the other, or to turn
// this function into a macro so that the preprocessor can trigger the
// conversion for us, so this is what we've got.
static bool IsThingDisabled(const char* thing, const wchar_t* wideThing) {
  // The logic here is intended to be the same as that used by Firefox's policy
  // engine implementation; they should be kept in sync. We have added the pref
  // check at the end though, since that's our own custom mechanism.
  PolicyState state = FindPolicyInRegistry(HKEY_LOCAL_MACHINE, wideThing);
  if (state == PolicyState::NoPolicy) {
    state = FindPolicyInRegistry(HKEY_CURRENT_USER, wideThing);
  }
  if (state == PolicyState::NoPolicy) {
    state = FindPolicyInFile(thing);
  }
  if (state == PolicyState::NoPolicy) {
    state = IsDisabledByPref(wideThing);
  }
  return state == PolicyState::Enabled ? true : false;
}

bool IsAgentDisabled() {
  return IsThingDisabled(AGENT_POLICY_NAME, L"" AGENT_POLICY_NAME);
}

bool IsTelemetryDisabled() {
  return IsThingDisabled(TELEMETRY_POLICY_NAME, L"" TELEMETRY_POLICY_NAME);
}

}  // namespace mozilla::default_agent
