// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/policy/core/common/policy_loader_mac.h"

#include <Foundation/Foundation.h>

#include <utility>

#include "base/apple/foundation_util.h"
#include "base/enterprise_util.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/sys_string_conversions.h"
#import "base/task/sequenced_task_runner.h"
#include "base/task/sequenced_task_runner.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/policy/core/common/external_data_fetcher.h"
#include "components/policy/core/common/features.h"
#include "components/policy/core/common/mac_util.h"
#include "components/policy/core/common/policy_bundle.h"
#include "components/policy/core/common/policy_loader_common.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/core/common/preferences_mac.h"
#include "components/policy/core/common/schema.h"
#include "components/policy/core/common/schema_map.h"

namespace policy {

namespace {

bool ShouldHonorPolicies() {
  // Only honor sensitive policies if the Mac is managed or connected to an
  // enterprise.
  // TODO (crbug.com/1322121): Use PlatformManagementService instead.
  return base::IsManagedOrEnterpriseDevice();
}

}  // namespace

PolicyLoaderMac::PolicyLoaderMac(
    scoped_refptr<base::SequencedTaskRunner> task_runner,
    ManagementService* management_service,
    const base::FilePath& managed_policy_path,
    std::unique_ptr<MacPreferences> preferences)
    : PolicyLoaderMac(task_runner,
                      management_service,
                      managed_policy_path,
                      std::move(preferences),
                      kCFPreferencesCurrentApplication) {}

PolicyLoaderMac::PolicyLoaderMac(
    scoped_refptr<base::SequencedTaskRunner> task_runner,
    ManagementService* management_service,
    const base::FilePath& managed_policy_path,
    std::unique_ptr<MacPreferences> preferences,
    CFStringRef application_id)
    : AsyncPolicyLoader(task_runner,
                        management_service,
                        /*periodic_updates=*/true),
      preferences_(std::move(preferences)),
      managed_policy_path_(managed_policy_path),
      application_id_(CFStringCreateCopy(kCFAllocatorDefault, application_id)) {
}

PolicyLoaderMac::~PolicyLoaderMac() = default;

void PolicyLoaderMac::InitOnBackgroundThread() {
  if (!managed_policy_path_.empty()) {
    watcher_.Watch(managed_policy_path_,
                   base::FilePathWatcher::Type::kNonRecursive,
                   base::BindRepeating(&PolicyLoaderMac::OnFileUpdated,
                                       base::Unretained(this)));
  }

  base::File::Info file_info;
  bool managed_policy_file_exists = false;
  if (base::GetFileInfo(managed_policy_path_, &file_info) &&
      !file_info.is_directory) {
    managed_policy_file_exists = true;
  }

  base::UmaHistogramBoolean("EnterpriseCheck.IsManagedOrEnterpriseDevice",
                            base::IsManagedOrEnterpriseDevice());

  base::UmaHistogramBoolean("EnterpriseCheck.IsManaged2",
                            managed_policy_file_exists);
  base::UmaHistogramBoolean("EnterpriseCheck.IsEnterpriseUser",
                            base::IsEnterpriseDevice());

  base::UmaHistogramEnumeration("EnterpriseCheck.Mac.IsDeviceMDMEnrolledNew",
                                base::IsDeviceRegisteredWithManagement());
  base::DeviceUserDomainJoinState state =
      base::AreDeviceAndUserJoinedToDomain();
  base::UmaHistogramBoolean("EnterpriseCheck.IsDomainJoined",
                            state.device_joined);
  base::UmaHistogramBoolean("EnterpriseCheck.Mac.IsCurrentUserDomainUser",
                            state.user_joined);
}

PolicyBundle PolicyLoaderMac::Load() {
  preferences_->AppSynchronize(application_id_.get());
  PolicyBundle bundle;

  // Load Chrome's policy.
  PolicyMap& chrome_policy =
      bundle.Get(PolicyNamespace(POLICY_DOMAIN_CHROME, std::string()));

  const Schema* schema =
      schema_map()->GetSchema(PolicyNamespace(POLICY_DOMAIN_CHROME, ""));
  for (Schema::Iterator it = schema->GetPropertiesIterator(); !it.IsAtEnd();
       it.Advance()) {
    base::apple::ScopedCFTypeRef<CFStringRef> name(
        base::SysUTF8ToCFStringRef(it.key()));
    base::apple::ScopedCFTypeRef<CFPropertyListRef> value(
        preferences_->CopyAppValue(name.get(), application_id_.get()));
    if (!value)
      continue;
    bool forced =
        preferences_->AppValueIsForced(name.get(), application_id_.get());
    PolicyLevel level =
        forced ? POLICY_LEVEL_MANDATORY : POLICY_LEVEL_RECOMMENDED;
    PolicyScope scope = POLICY_SCOPE_USER;
    if (forced) {
      scope = preferences_->IsManagedPolicyAvailableForMachineScope(name.get())
                  ? POLICY_SCOPE_MACHINE
                  : POLICY_SCOPE_USER;
    }
    std::unique_ptr<base::Value> policy = PropertyToValue(value.get());
    if (policy) {
      chrome_policy.Set(it.key(), level, scope, POLICY_SOURCE_PLATFORM,
                        std::move(*policy), nullptr);
    }
  }

  // Load policy for the registered components.
  LoadPolicyForDomain(POLICY_DOMAIN_EXTENSIONS, "extensions", &bundle);

  // When the feature flag is enabled, use the new ManagementService-based
  // mechanism. When disabled, fall back to the original ShouldHonorPolicies()
  // behavior for safety.
  // Note: Policy loading can happen before FeatureList is initialized (during
  // early startup in CreatePrefService). In that case, use the old behavior.
  bool should_filter;
  if (base::FeatureList::GetInstance() &&
      base::FeatureList::IsEnabled(
          features::kUseManagementServiceForSensitivePolicies)) {
    should_filter = ShouldFilterSensitivePolicies();
  } else {
    should_filter = !ShouldHonorPolicies();
  }
  if (should_filter) {
    FilterSensitivePolicies(&chrome_policy);
  }

  return bundle;
}

base::Time PolicyLoaderMac::LastModificationTime() {
  base::File::Info file_info;
  if (!base::GetFileInfo(managed_policy_path_, &file_info) ||
      file_info.is_directory) {
    return base::Time();
  }

  return file_info.last_modified;
}

#if BUILDFLAG(IS_MAC)

base::FilePath PolicyLoaderMac::GetManagedPolicyPath(CFStringRef bundle_id) {
  // This constructs the path to the plist file in which macOS stores the
  // managed preference for the application. This is undocumented and therefore
  // fragile, but if it doesn't work out, AsyncPolicyLoader has a task that
  // polls periodically in order to reload managed preferences later even if we
  // missed the change.

  base::FilePath path;
  if (!base::apple::GetLocalDirectory(NSLibraryDirectory, &path)) {
    return base::FilePath();
  }
  path = path.Append(FILE_PATH_LITERAL("Managed Preferences"));
  char* login = getlogin();
  if (!login)
    return base::FilePath();
  path = path.AppendASCII(login);
  return path.Append(base::SysCFStringRefToUTF8(bundle_id) + ".plist");
}

#endif

void PolicyLoaderMac::LoadPolicyForDomain(PolicyDomain domain,
                                          const std::string& domain_name,
                                          PolicyBundle* bundle) {
  std::string id_prefix(base::SysCFStringRefToUTF8(application_id_.get()));
  id_prefix.append(".").append(domain_name).append(".");

  const ComponentMap* components = schema_map()->GetComponents(domain);
  if (!components)
    return;

  for (const auto& component : *components) {
    PolicyMap policy;
    LoadPolicyForComponent(id_prefix + component.first, component.second,
                           &policy);
    if (!policy.empty())
      bundle->Get(PolicyNamespace(domain, component.first)).Swap(&policy);
  }
}

void PolicyLoaderMac::LoadPolicyForComponent(
    const std::string& bundle_id_string,
    const Schema& schema,
    PolicyMap* policy) {
  // TODO(joaodasilva): Extensions may be registered in a ComponentMap
  // without a schema, to allow a graceful update of the Legacy Browser Support
  // extension on Windows. Remove this check once that support is removed.
  if (!schema.valid())
    return;

  base::apple::ScopedCFTypeRef<CFStringRef> bundle_id =
      base::SysUTF8ToCFStringRef(bundle_id_string);
  preferences_->AppSynchronize(bundle_id.get());

  for (Schema::Iterator it = schema.GetPropertiesIterator(); !it.IsAtEnd();
       it.Advance()) {
    base::apple::ScopedCFTypeRef<CFStringRef> pref_name =
        base::SysUTF8ToCFStringRef(it.key());
    base::apple::ScopedCFTypeRef<CFPropertyListRef> value(
        preferences_->CopyAppValue(pref_name.get(), bundle_id.get()));
    if (!value)
      continue;
    bool forced =
        preferences_->AppValueIsForced(pref_name.get(), bundle_id.get());
    PolicyLevel level =
        forced ? POLICY_LEVEL_MANDATORY : POLICY_LEVEL_RECOMMENDED;
    PolicyScope scope = POLICY_SCOPE_USER;
    if (forced) {
      scope =
          preferences_->IsManagedPolicyAvailableForMachineScope(pref_name.get())
              ? POLICY_SCOPE_MACHINE
              : POLICY_SCOPE_USER;
    }
    std::unique_ptr<base::Value> policy_value = PropertyToValue(value.get());
    if (policy_value) {
      policy->Set(it.key(), level, scope, POLICY_SOURCE_PLATFORM,
                  std::move(*policy_value), nullptr);
    }
  }
}

void PolicyLoaderMac::OnFileUpdated(const base::FilePath& path, bool error) {
  if (!error)
    Reload(false);
}

}  // namespace policy
