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

#include "chrome/updater/device_management/dm_storage.h"

#import <Foundation/Foundation.h>

#include <string>

#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/important_file_writer.h"
#include "base/logging.h"
#include "base/mac/mac_util.h"
#include "base/mac/scoped_ioobject.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/updater/updater_branding.h"
#include "chrome/updater/util/mac_util.h"

namespace updater {
namespace {

const CFStringRef kEnrollmentTokenKey = CFSTR("EnrollmentToken");
const CFStringRef kBrowserBundleId =
    CFSTR(MAC_BROWSER_BUNDLE_IDENTIFIER_STRING);

bool LoadEnrollmentTokenFromPolicy(std::string* enrollment_token) {
  base::apple::ScopedCFTypeRef<CFPropertyListRef> token_value(
      CFPreferencesCopyAppValue(kEnrollmentTokenKey, kBrowserBundleId));
  if (!token_value || CFGetTypeID(token_value) != CFStringGetTypeID() ||
      !CFPreferencesAppValueIsForced(kEnrollmentTokenKey, kBrowserBundleId)) {
    return false;
  }

  CFStringRef value_string = base::apple::CFCast<CFStringRef>(token_value);
  if (!value_string)
    return false;

  *enrollment_token = base::SysCFStringRefToUTF8(value_string);
  return true;
}

void DeletePolicyEnrollmentToken() {
  CFPreferencesSetValue(kEnrollmentTokenKey, nil, kBrowserBundleId,
                        kCFPreferencesAnyUser, kCFPreferencesCurrentHost);
  CFPreferencesSynchronize(kBrowserBundleId, kCFPreferencesAnyUser,
                           kCFPreferencesCurrentHost);
}

// Enrollment token path:
//   /Library/Google/Chrome/CloudManagementEnrollmentToken.
base::FilePath GetEnrollmentTokenFilePath() {
  base::FilePath lib_path;
  if (!base::apple::GetLocalDirectory(NSLibraryDirectory, &lib_path)) {
    VLOG(1) << "Failed to get local library path.";
    return base::FilePath();
  }

  return lib_path.AppendASCII(COMPANY_SHORTNAME_STRING)
      .AppendASCII(BROWSER_NAME_STRING)
      .AppendASCII("CloudManagementEnrollmentToken");
}

// DM token path:
//   /Library/Application Support/Google/CloudManagement.
base::FilePath GetDmTokenFilePath() {
  base::FilePath app_path;
  if (!base::apple::GetLocalDirectory(NSApplicationSupportDirectory,
                                      &app_path)) {
    VLOG(1) << "Failed to get Application support path.";
    return base::FilePath();
  }

  return app_path.AppendASCII(COMPANY_SHORTNAME_STRING)
      .AppendASCII("CloudManagement");
}

bool LoadTokenFromFile(const base::FilePath& token_file_path,
                       std::string* token) {
  std::string token_value;
  if (token_file_path.empty() ||
      !base::ReadFileToString(token_file_path, &token_value)) {
    return false;
  }

  *token = std::string(base::TrimWhitespaceASCII(token_value, base::TRIM_ALL));
  return true;
}

class TokenService : public TokenServiceInterface {
 public:
  TokenService(const base::FilePath& enrollment_token_path,
               const base::FilePath& dm_token_path);
  ~TokenService() override = default;

  // Overrides for TokenServiceInterface.
  std::string GetDeviceID() const override { return device_id_; }
  bool IsEnrollmentMandatory() const override { return false; }
  bool StoreEnrollmentToken(const std::string& enrollment_token) override;
  bool DeleteEnrollmentToken() override;
  std::string GetEnrollmentToken() const override { return enrollment_token_; }
  bool StoreDmToken(const std::string& dm_token) override;
  bool DeleteDmToken() override;
  std::string GetDmToken() const override { return dm_token_; }

 private:
  // Cached values in memory.
  const std::string device_id_ = base::mac::GetPlatformSerialNumber();
  const base::FilePath enrollment_token_path_;
  const base::FilePath dm_token_path_;
  std::string enrollment_token_;
  std::string dm_token_;
};

TokenService::TokenService(const base::FilePath& enrollment_token_path,
                           const base::FilePath& dm_token_path)
    : enrollment_token_path_(enrollment_token_path.empty()
                                 ? GetEnrollmentTokenFilePath()
                                 : enrollment_token_path),
      dm_token_path_(dm_token_path.empty() ? GetDmTokenFilePath()
                                           : dm_token_path) {
  std::string enrollment_token;
  if (LoadEnrollmentTokenFromPolicy(&enrollment_token) ||
      LoadTokenFromFile(enrollment_token_path_, &enrollment_token)) {
    enrollment_token_ = enrollment_token;
  }

  std::string dm_token;
  if (LoadTokenFromFile(dm_token_path_, &dm_token)) {
    dm_token_ = dm_token;
  }
}

bool TokenService::StoreEnrollmentToken(const std::string& enrollment_token) {
  if (enrollment_token_path_.empty() ||
      !CreateGlobalAccessibleDirectory(enrollment_token_path_.DirName()) ||
      !WriteContentToGlobalReadableFile(enrollment_token_path_,
                                        enrollment_token)) {
    VLOG(1) << "Failed to update enrollment token.";
    return false;
  }

  enrollment_token_ = enrollment_token;
  VLOG(1) << "Updated enrollment token to: " << enrollment_token;
  return true;
}

bool TokenService::DeleteEnrollmentToken() {
  enrollment_token_ = "";
  DeletePolicyEnrollmentToken();
  return base::DeleteFile(base::FilePath(enrollment_token_path_));
}

bool TokenService::StoreDmToken(const std::string& token) {
  if (dm_token_path_.empty() ||
      !CreateGlobalAccessibleDirectory(dm_token_path_.DirName()) ||
      !WriteContentToGlobalReadableFile(dm_token_path_, token)) {
    VLOG(1) << "Failed to update DM token.";
    return false;
  }
  dm_token_ = token;
  VLOG(1) << "Updated DM token to: " << token;
  return true;
}

bool TokenService::DeleteDmToken() {
  if (dm_token_path_.empty() || !base::DeleteFile(dm_token_path_)) {
    VLOG(1) << "Failed to delete DM token.";
    return false;
  }
  dm_token_.clear();
  VLOG(1) << "DM token deleted.";
  return true;
}

}  // namespace

DMStorage::DMStorage(const base::FilePath& policy_cache_root,
                     const base::FilePath& enrollment_token_path,
                     const base::FilePath& dm_token_path)
    : DMStorage(policy_cache_root,
                std::make_unique<TokenService>(enrollment_token_path,
                                               dm_token_path)) {}

scoped_refptr<DMStorage> GetDefaultDMStorage() {
  absl::optional<base::FilePath> keystone_path =
      GetKeystoneFolderPath(UpdaterScope::kSystem);
  return keystone_path ? base::MakeRefCounted<DMStorage>(
                             keystone_path->AppendASCII("DeviceManagement"))
                       : nullptr;
}

}  // namespace updater
