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

#include "chrome/updater/mac/setup/setup.h"

#import <ServiceManagement/ServiceManagement.h>

#include "base/at_exit.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/mac/bundle_locations.h"
#include "base/mac/foundation_util.h"
#include "base/mac/mac_util.h"
#include "base/mac/scoped_nsobject.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/strings/strcat.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/thread_pool.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/threading/platform_thread.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/time/time.h"
#include "chrome/common/mac/launchd.h"
#include "chrome/updater/constants.h"
#include "chrome/updater/crash_client.h"
#include "chrome/updater/crash_reporter.h"
#include "chrome/updater/launchd_util.h"
#import "chrome/updater/mac/mac_util.h"
#include "chrome/updater/mac/setup/keystone.h"
#import "chrome/updater/mac/xpc_service_names.h"
#include "chrome/updater/setup.h"
#include "chrome/updater/updater_branding.h"
#include "chrome/updater/updater_scope.h"
#include "chrome/updater/util.h"
#include "components/crash/core/common/crash_key.h"
#include "third_party/abseil-cpp/absl/types/optional.h"

namespace updater {
namespace {

#pragma mark Helpers
Launchd::Domain LaunchdDomain(UpdaterScope scope) {
  switch (scope) {
    case UpdaterScope::kSystem:
      return Launchd::Domain::Local;
    case UpdaterScope::kUser:
      return Launchd::Domain::User;
  }
}

Launchd::Type ServiceLaunchdType(UpdaterScope scope) {
  switch (scope) {
    case UpdaterScope::kSystem:
      return Launchd::Type::Daemon;
    case UpdaterScope::kUser:
      return Launchd::Type::Agent;
  }
}

CFStringRef CFSessionType(UpdaterScope scope) {
  switch (scope) {
    case UpdaterScope::kSystem:
      return CFSTR("System");
    case UpdaterScope::kUser:
      return CFSTR("Aqua");
  }
}

NSString* NSStringSessionType(UpdaterScope scope) {
  switch (scope) {
    case UpdaterScope::kSystem:
      return @"System";
    case UpdaterScope::kUser:
      return @"Aqua";
  }
}

#pragma mark Setup
bool CopyBundle(const base::FilePath& dest_path, UpdaterScope scope) {
  if (!base::PathExists(dest_path)) {
    base::File::Error error;
    if (!base::CreateDirectoryAndGetError(dest_path, &error)) {
      LOG(ERROR) << "Failed to create '" << dest_path.value().c_str()
                 << "' directory: " << base::File::ErrorToString(error);
      return false;
    }
  }

  // For system installs, set file permissions to be drwxr-xr-x
  if (scope == UpdaterScope::kSystem) {
    constexpr int kPermissionsMask = base::FILE_PERMISSION_USER_MASK |
                                     base::FILE_PERMISSION_READ_BY_GROUP |
                                     base::FILE_PERMISSION_EXECUTE_BY_GROUP |
                                     base::FILE_PERMISSION_READ_BY_OTHERS |
                                     base::FILE_PERMISSION_EXECUTE_BY_OTHERS;
    if (!base::SetPosixFilePermissions(
            GetLibraryFolderPath(scope)->Append(COMPANY_SHORTNAME_STRING),
            kPermissionsMask) ||
        !base::SetPosixFilePermissions(*GetUpdaterFolderPath(scope),
                                       kPermissionsMask) ||
        !base::SetPosixFilePermissions(*GetVersionedUpdaterFolderPath(scope),
                                       kPermissionsMask)) {
      LOG(ERROR) << "Failed to set permissions to drwxr-xr-x at "
                 << dest_path.value().c_str();
      return false;
    }
  }

  if (!base::CopyDirectory(base::mac::OuterBundlePath(), dest_path, true)) {
    LOG(ERROR) << "Copying app to '" << dest_path.value().c_str() << "' failed";
    return false;
  }
  return true;
}

NSString* MakeProgramArgument(const char* argument) {
  return base::SysUTF8ToNSString(base::StrCat({"--", argument}));
}

NSString* MakeProgramArgumentWithValue(const char* argument,
                                       const char* value) {
  return base::SysUTF8ToNSString(base::StrCat({"--", argument, "=", value}));
}

base::ScopedCFTypeRef<CFDictionaryRef> CreateServiceLaunchdPlist(
    UpdaterScope scope,
    const base::FilePath& updater_path) {
  // See the man page for launchd.plist.
  NSMutableArray<NSString*>* program_arguments =
      [NSMutableArray<NSString*> array];
  [program_arguments addObjectsFromArray:@[
    base::SysUTF8ToNSString(updater_path.value()),
    MakeProgramArgument(kServerSwitch),
    MakeProgramArgumentWithValue(kServerServiceSwitch,
                                 kServerUpdateServiceSwitchValue),
    MakeProgramArgument(kEnableLoggingSwitch),
    MakeProgramArgumentWithValue(kLoggingModuleSwitch,
                                 kLoggingModuleSwitchValue)

  ]];
  if (scope == UpdaterScope::kSystem)
    [program_arguments addObject:MakeProgramArgument(kSystemSwitch)];

  NSDictionary<NSString*, id>* launchd_plist = @{
    @LAUNCH_JOBKEY_LABEL : GetUpdateServiceLaunchdLabel(scope),
    @LAUNCH_JOBKEY_PROGRAMARGUMENTS : program_arguments,
    @LAUNCH_JOBKEY_MACHSERVICES : @{GetUpdateServiceMachName(scope) : @YES},
    @LAUNCH_JOBKEY_ABANDONPROCESSGROUP : @YES,
    @LAUNCH_JOBKEY_LIMITLOADTOSESSIONTYPE : NSStringSessionType(scope)
  };

  return base::ScopedCFTypeRef<CFDictionaryRef>(
      base::mac::CFCast<CFDictionaryRef>(launchd_plist),
      base::scoped_policy::RETAIN);
}

base::ScopedCFTypeRef<CFDictionaryRef> CreateWakeLaunchdPlist(
    UpdaterScope scope,
    const base::FilePath& updater_path) {
  // See the man page for launchd.plist.
  NSMutableArray<NSString*>* program_arguments =
      [NSMutableArray<NSString*> array];
  [program_arguments addObjectsFromArray:@[
    base::SysUTF8ToNSString(updater_path.value()),
    MakeProgramArgument(kWakeSwitch), MakeProgramArgument(kEnableLoggingSwitch)
  ]];
  if (scope == UpdaterScope::kSystem)
    [program_arguments addObject:MakeProgramArgument(kSystemSwitch)];

  NSDictionary<NSString*, id>* launchd_plist = @{
    @LAUNCH_JOBKEY_LABEL : GetWakeLaunchdLabel(scope),
    @LAUNCH_JOBKEY_PROGRAMARGUMENTS : program_arguments,
    @LAUNCH_JOBKEY_STARTINTERVAL : @3600,
    @LAUNCH_JOBKEY_ABANDONPROCESSGROUP : @YES,
    @LAUNCH_JOBKEY_LIMITLOADTOSESSIONTYPE : NSStringSessionType(scope)
  };

  return base::ScopedCFTypeRef<CFDictionaryRef>(
      base::mac::CFCast<CFDictionaryRef>(launchd_plist),
      base::scoped_policy::RETAIN);
}

base::ScopedCFTypeRef<CFDictionaryRef> CreateUpdateServiceInternalLaunchdPlist(
    UpdaterScope scope,
    const base::FilePath& updater_path) {
  // See the man page for launchd.plist.
  NSMutableArray<NSString*>* program_arguments =
      [NSMutableArray<NSString*> array];
  [program_arguments addObjectsFromArray:@[
    base::SysUTF8ToNSString(updater_path.value()),
    MakeProgramArgument(kServerSwitch),
    MakeProgramArgumentWithValue(kServerServiceSwitch,
                                 kServerUpdateServiceInternalSwitchValue),
    MakeProgramArgument(kEnableLoggingSwitch),
    MakeProgramArgumentWithValue(kLoggingModuleSwitch,
                                 kLoggingModuleSwitchValue)
  ]];
  if (scope == UpdaterScope::kSystem)
    [program_arguments addObject:MakeProgramArgument(kSystemSwitch)];

  NSDictionary<NSString*, id>* launchd_plist = @{
    @LAUNCH_JOBKEY_LABEL : GetUpdateServiceInternalLaunchdLabel(scope),
    @LAUNCH_JOBKEY_PROGRAMARGUMENTS : program_arguments,
    @LAUNCH_JOBKEY_MACHSERVICES :
        @{GetUpdateServiceInternalMachName(scope) : @YES},
    @LAUNCH_JOBKEY_ABANDONPROCESSGROUP : @YES,
    @LAUNCH_JOBKEY_LIMITLOADTOSESSIONTYPE : NSStringSessionType(scope)
  };

  return base::ScopedCFTypeRef<CFDictionaryRef>(
      base::mac::CFCast<CFDictionaryRef>(launchd_plist),
      base::scoped_policy::RETAIN);
}

bool CreateUpdateServiceLaunchdJobPlist(UpdaterScope scope,
                                        const base::FilePath& updater_path) {
  // We're creating directories and writing a file.
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  base::ScopedCFTypeRef<CFDictionaryRef> plist(
      CreateServiceLaunchdPlist(scope, updater_path));
  return Launchd::GetInstance()->WritePlistToFile(
      LaunchdDomain(scope), ServiceLaunchdType(scope),
      CopyUpdateServiceLaunchdName(scope), plist);
}

bool CreateWakeLaunchdJobPlist(UpdaterScope scope,
                               const base::FilePath& updater_path) {
  // We're creating directories and writing a file.
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);
  base::ScopedCFTypeRef<CFDictionaryRef> plist(
      CreateWakeLaunchdPlist(scope, updater_path));
  return Launchd::GetInstance()->WritePlistToFile(
      LaunchdDomain(scope), ServiceLaunchdType(scope),
      CopyWakeLaunchdName(scope), plist);
}

bool CreateUpdateServiceInternalLaunchdJobPlist(
    UpdaterScope scope,
    const base::FilePath& updater_path) {
  // We're creating directories and writing a file.
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);
  base::ScopedCFTypeRef<CFDictionaryRef> plist(
      CreateUpdateServiceInternalLaunchdPlist(scope, updater_path));
  return Launchd::GetInstance()->WritePlistToFile(
      LaunchdDomain(scope), ServiceLaunchdType(scope),
      CopyUpdateServiceInternalLaunchdName(scope), plist);
}

bool StartUpdateServiceVersionedLaunchdJob(
    UpdaterScope scope,
    const base::ScopedCFTypeRef<CFStringRef> name) {
  return Launchd::GetInstance()->RestartJob(LaunchdDomain(scope),
                                            ServiceLaunchdType(scope), name,
                                            CFSessionType(scope));
}

bool StartUpdateWakeVersionedLaunchdJob(UpdaterScope scope) {
  return Launchd::GetInstance()->RestartJob(
      LaunchdDomain(scope), ServiceLaunchdType(scope),
      CopyWakeLaunchdName(scope), CFSessionType(scope));
}

bool StartUpdateServiceInternalVersionedLaunchdJob(UpdaterScope scope) {
  return Launchd::GetInstance()->RestartJob(
      LaunchdDomain(scope), ServiceLaunchdType(scope),
      CopyUpdateServiceInternalLaunchdName(scope), CFSessionType(scope));
}

bool StartLaunchdServiceJob(UpdaterScope scope) {
  return StartUpdateServiceVersionedLaunchdJob(
      scope, CopyUpdateServiceLaunchdName(scope));
}

bool RemoveServiceJobFromLaunchd(UpdaterScope scope,
                                 base::ScopedCFTypeRef<CFStringRef> name) {
  return RemoveJobFromLaunchd(scope, LaunchdDomain(scope),
                              ServiceLaunchdType(scope), name);
}

bool RemoveUpdateServiceJobFromLaunchd(
    UpdaterScope scope,
    base::ScopedCFTypeRef<CFStringRef> name) {
  return RemoveServiceJobFromLaunchd(scope, name);
}

bool RemoveUpdateServiceJobFromLaunchd(UpdaterScope scope) {
  return RemoveUpdateServiceJobFromLaunchd(scope,
                                           CopyUpdateServiceLaunchdName(scope));
}

bool RemoveUpdateWakeJobFromLaunchd(UpdaterScope scope) {
  return RemoveServiceJobFromLaunchd(scope, CopyWakeLaunchdName(scope));
}

bool RemoveUpdateServiceInternalJobFromLaunchd(UpdaterScope scope) {
  return RemoveServiceJobFromLaunchd(
      scope, CopyUpdateServiceInternalLaunchdName(scope));
}

bool DeleteFolder(const absl::optional<base::FilePath>& installed_path) {
  if (!installed_path)
    return false;
  if (!base::DeletePathRecursively(*installed_path)) {
    PLOG(ERROR) << "Deleting " << installed_path << " failed";
    return false;
  }
  return true;
}

bool DeleteInstallFolder(UpdaterScope scope) {
  return DeleteFolder(GetUpdaterFolderPath(scope));
}

bool DeleteCandidateInstallFolder(UpdaterScope scope) {
  return DeleteFolder(GetVersionedUpdaterFolderPath(scope));
}

bool DeleteDataFolder(UpdaterScope scope) {
  return DeleteFolder(GetBaseDirectory(scope));
}

void CleanAfterInstallFailure(UpdaterScope scope) {
  // If install fails at any point, attempt to clean the install.
  DeleteCandidateInstallFolder(scope);
  RemoveUpdateWakeJobFromLaunchd(scope);
  RemoveUpdateServiceInternalJobFromLaunchd(scope);
}

bool RemoveQuarantineAttributes(const base::FilePath& updater_bundle_path,
                                const base::FilePath& updater_executable_path) {
  if (!base::mac::RemoveQuarantineAttribute(updater_bundle_path)) {
    VPLOG(1) << "Could not remove com.apple.quarantine for the bundle.";
    return false;
  }

  if (!base::mac::RemoveQuarantineAttribute(updater_executable_path)) {
    VPLOG(1) << "Could not remove com.apple.quarantine for the "
                "executable.";
    return false;
  }

  return true;
}

int DoSetup(UpdaterScope scope) {
  const absl::optional<base::FilePath> dest_path =
      GetVersionedUpdaterFolderPath(scope);

  if (!dest_path)
    return setup_exit_codes::kFailedToGetVersionedUpdaterFolderPath;
  if (!CopyBundle(*dest_path, scope))
    return setup_exit_codes::kFailedToCopyBundle;

  const base::FilePath updater_executable_path =
      dest_path->Append(GetExecutableRelativePath());

  // Quarantine attribute needs to be removed here as the copied bundle might be
  // given com.apple.quarantine attribute, and the server is attempted to be
  // launched below, Gatekeeper could prompt the user.
  const absl::optional<base::FilePath> bundle_path =
      GetUpdaterAppBundlePath(scope);
  if (!bundle_path)
    return setup_exit_codes::kFailedToGetAppBundlePath;
  if (!RemoveQuarantineAttributes(*bundle_path, updater_executable_path)) {
    VLOG(1) << "Couldn't remove quarantine bits for updater. This will likely "
               "cause Gatekeeper to show a prompt to the user.";
  }

  if (!CreateWakeLaunchdJobPlist(scope, updater_executable_path))
    return setup_exit_codes::kFailedToCreateWakeLaunchdJobPlist;

  if (!CreateUpdateServiceInternalLaunchdJobPlist(scope,
                                                  updater_executable_path))
    return setup_exit_codes::
        kFailedToCreateUpdateServiceInternalLaunchdJobPlist;

  if (!StartUpdateServiceInternalVersionedLaunchdJob(scope))
    return setup_exit_codes::kFailedToStartLaunchdUpdateServiceInternalJob;

  if (!StartUpdateWakeVersionedLaunchdJob(scope))
    return setup_exit_codes::kFailedToStartLaunchdWakeJob;

  return setup_exit_codes::kSuccess;
}

}  // namespace

int Setup(UpdaterScope scope) {
  int error = DoSetup(scope);
  if (error)
    CleanAfterInstallFailure(scope);
  return error;
}

int PromoteCandidate(UpdaterScope scope) {
  const absl::optional<base::FilePath> dest_path =
      GetVersionedUpdaterFolderPath(scope);
  if (!dest_path)
    return setup_exit_codes::kFailedToGetVersionedUpdaterFolderPath;
  const base::FilePath updater_executable_path =
      dest_path->Append(GetExecutableRelativePath());

  if (!CreateUpdateServiceLaunchdJobPlist(scope, updater_executable_path))
    return setup_exit_codes::kFailedToCreateUpdateServiceLaunchdJobPlist;

  if (!StartLaunchdServiceJob(scope))
    return setup_exit_codes::kFailedToStartLaunchdActiveServiceJob;

  // Wait for launchd to finish the load operation for the update service.
  base::PlatformThread::Sleep(base::Seconds(2));

  return setup_exit_codes::kSuccess;
}

#pragma mark Uninstall
int UninstallCandidate(UpdaterScope scope) {
  if (!DeleteCandidateInstallFolder(scope))
    return setup_exit_codes::kFailedToDeleteFolder;

  if (!RemoveUpdateWakeJobFromLaunchd(scope))
    return setup_exit_codes::kFailedToRemoveWakeJobFromLaunchd;

  // Removing the Update Internal job has to be the last step because launchd is
  // likely to terminate the current process. Clients should expect the
  // connection to invalidate (possibly with an interruption beforehand) as a
  // result of service uninstallation.
  if (!RemoveUpdateServiceInternalJobFromLaunchd(scope))
    return setup_exit_codes::kFailedToRemoveUpdateServiceInternalJobFromLaunchd;

  return setup_exit_codes::kSuccess;
}

int Uninstall(UpdaterScope scope) {
  VLOG(1) << base::CommandLine::ForCurrentProcess()->GetCommandLineString()
          << " : " << __func__;
  const int exit = UninstallCandidate(scope);
  if (exit != setup_exit_codes::kSuccess)
    return exit;

  if (!RemoveUpdateServiceJobFromLaunchd(scope))
    return setup_exit_codes::kFailedToRemoveActiveUpdateServiceJobFromLaunchd;

  if (!DeleteInstallFolder(scope))
    return setup_exit_codes::kFailedToDeleteFolder;

  base::ThreadPool::PostTask(FROM_HERE,
                             {base::MayBlock(), base::WithBaseSyncPrimitives()},
                             base::BindOnce(&UninstallKeystone, scope));

  // Deleting the data folder is best-effort. Current running processes such as
  // the crash handler process may still write to the updater log file, thus
  // it is not always possible to delete the data folder.
  DeleteDataFolder(scope);

  return setup_exit_codes::kSuccess;
}

}  // namespace updater