chromium/chrome/browser/web_applications/test/os_integration_test_override_impl.cc

// Copyright 2023 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/browser/web_applications/test/os_integration_test_override_impl.h"

#include <map>
#include <memory>
#include <optional>
#include <ostream>
#include <string>
#include <tuple>
#include <vector>

#include "base/base_paths.h"
#include "base/check_is_test.h"
#include "base/containers/contains.h"
#include "base/containers/span.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/ref_counted.h"
#include "base/memory/scoped_refptr.h"
#include "base/no_destructor.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "base/synchronization/lock.h"
#include "base/test/bind.h"
#include "base/threading/thread_restrictions.h"
#include "base/types/expected.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/os_integration/os_integration_test_override.h"
#include "chrome/browser/web_applications/os_integration/web_app_file_handler_registration.h"
#include "chrome/browser/web_applications/test/fake_environment.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_icon_generator.h"
#include "chrome/browser/web_applications/web_app_icon_manager.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "components/webapps/common/web_app_id.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"

#if BUILDFLAG(IS_LINUX)
#include "base/nix/xdg_util.h"
#endif

#if BUILDFLAG(IS_MAC)
#include <ImageIO/ImageIO.h>

#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/files/scoped_temp_dir.h"
#include "chrome/browser/shell_integration.h"
#include "chrome/browser/web_applications/os_integration/mac/app_shim_registry.h"
#include "net/base/filename_util.h"
#import "skia/ext/skia_utils_mac.h"
#endif

#if BUILDFLAG(IS_WIN)
#include <windows.h>

#include <shellapi.h>

#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/containers/flat_set.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/test_reg_util_win.h"
#include "base/win/registry.h"
#include "base/win/scoped_gdi_object.h"
#include "base/win/shortcut.h"
#include "base/win/windows_types.h"
#include "chrome/browser/web_applications/os_integration/web_app_handler_registration_utils_win.h"
#include "chrome/browser/web_applications/os_integration/web_app_uninstallation_via_os_settings_registration.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/win/jumplist_updater.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/install_static/install_util.h"
#include "chrome/installer/util/install_util.h"
#include "chrome/installer/util/shell_util.h"
#include "third_party/re2/src/re2/re2.h"
#include "ui/gfx/icon_util.h"
#endif

namespace web_app {

namespace {

#if BUILDFLAG(IS_WIN)
constexpr wchar_t kUninstallRegistryKey[] =
    L"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\";

base::FilePath GetShortcutProfile(base::FilePath shortcut_path) {
  base::FilePath shortcut_profile;
  std::wstring cmd_line_string;
  if (base::win::ResolveShortcut(shortcut_path, nullptr, &cmd_line_string)) {
    base::CommandLine shortcut_cmd_line =
        base::CommandLine::FromString(L"program " + cmd_line_string);
    shortcut_profile =
        shortcut_cmd_line.GetSwitchValuePath(switches::kProfileDirectory);
  }
  return shortcut_profile;
}

std::vector<std::wstring> GetFileExtensionsForProgId(
    const std::wstring& file_handler_prog_id) {
  const std::wstring prog_id_path =
      base::StrCat({ShellUtil::kRegClasses, L"\\", file_handler_prog_id});

  // Get list of handled file extensions from value FileExtensions at
  // HKEY_CURRENT_USER\Software\Classes\<file_handler_prog_id>.
  base::win::RegKey file_extensions_key(HKEY_CURRENT_USER, prog_id_path.c_str(),
                                        KEY_QUERY_VALUE);
  std::wstring handled_file_extensions;
  LONG result = file_extensions_key.ReadValue(L"FileExtensions",
                                              &handled_file_extensions);
  CHECK_EQ(result, ERROR_SUCCESS);

  return base::SplitString(handled_file_extensions, std::wstring(L";"),
                           base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
}
#endif

#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
// Performs a blocking read of app icons from the disk.
SkColor IconManagerReadIconTopLeftColorForSize(WebAppIconManager& icon_manager,
                                               const webapps::AppId& app_id,
                                               SquareSizePx size_px) {}
#endif

}  // namespace

OsIntegrationTestOverrideBlockingRegistration::
    OsIntegrationTestOverrideBlockingRegistration() {}

OsIntegrationTestOverrideBlockingRegistration::
    ~OsIntegrationTestOverrideBlockingRegistration() {}

OsIntegrationTestOverrideImpl&
OsIntegrationTestOverrideBlockingRegistration::test_override() const {}

// static
scoped_refptr<OsIntegrationTestOverrideImpl>
OsIntegrationTestOverrideImpl::Get() {}

// static
std::unique_ptr<OsIntegrationTestOverrideImpl::BlockingRegistration>
OsIntegrationTestOverrideImpl::OverrideForTesting() {}

bool OsIntegrationTestOverrideImpl::SimulateDeleteShortcutsByUser(
    Profile* profile,
    const webapps::AppId& app_id,
    const std::string& app_name) {}

#if BUILDFLAG(IS_MAC)
bool OsIntegrationTestOverrideImpl::DeleteChromeAppsDir() {
  if (chrome_apps_folder_.IsValid()) {
    bool success = chrome_apps_folder_.Delete();
    if (!success) {
      // Creating shortcuts kicks of an asynchronous task to eventually update
      // the icon of `chrome_apps_folder_`. If that task happens to run during
      // the above Delete() call deletion might fail. If that is the case, a
      // single retry should be enough to be able to delete the folder anyway.
      success = chrome_apps_folder_.Delete();
    }
    return success;
  } else {
    return false;
  }
}
#endif  // BUILDFLAG(IS_MAC)

#if BUILDFLAG(IS_WIN)
bool OsIntegrationTestOverrideImpl::DeleteDesktopDirOnWin() {
  if (desktop_.IsValid()) {
    return desktop_.Delete();
  } else {
    return false;
  }
}

bool OsIntegrationTestOverrideImpl::DeleteApplicationMenuDirOnWin() {
  if (application_menu_.IsValid()) {
    return application_menu_.Delete();
  } else {
    return false;
  }
}
#endif  // BUILDFLAG(IS_WIN)

#if BUILDFLAG(IS_LINUX)
bool OsIntegrationTestOverrideImpl::DeleteDesktopDirOnLinux() {}
#endif  // BUILDFLAG(IS_LINUX)

bool OsIntegrationTestOverrideImpl::IsRunOnOsLoginEnabled(
    Profile* profile,
    const webapps::AppId& app_id,
    const std::string& app_name) {}

bool OsIntegrationTestOverrideImpl::IsFileExtensionHandled(
    Profile* profile,
    const webapps::AppId& app_id,
    std::string app_name,
    std::string file_extension) {}

std::optional<SkColor>
OsIntegrationTestOverrideImpl::GetShortcutIconTopLeftColor(
    Profile* profile,
    base::FilePath shortcut_dir,
    const webapps::AppId& app_id,
    const std::string& app_name,
    SquareSizePx size_px) {}

base::FilePath OsIntegrationTestOverrideImpl::GetShortcutPath(
    Profile* profile,
    base::FilePath shortcut_dir,
    const webapps::AppId& app_id,
    const std::string& app_name) {}

bool OsIntegrationTestOverrideImpl::IsShortcutCreated(
    Profile* profile,
    const webapps::AppId& app_id,
    const std::string& app_name) {}

bool OsIntegrationTestOverrideImpl::AreShortcutsMenuRegistered() {}

#if BUILDFLAG(IS_WIN)

std::vector<SkColor>
OsIntegrationTestOverrideImpl::GetIconColorsForShortcutsMenu(
    const std::wstring& app_user_model_id) {
  CHECK(IsShortcutsMenuRegisteredForApp(app_user_model_id));
  std::vector<SkColor> icon_colors;
  for (auto& shell_link_item : jump_list_entry_map_[app_user_model_id]) {
    icon_colors.emplace_back(
        ReadColorFromShortcutMenuIcoFile(shell_link_item->icon_path()));
  }
  return icon_colors;
}

int OsIntegrationTestOverrideImpl::GetCountOfShortcutIconsCreated(
    const std::wstring& app_user_model_id) {
  CHECK(IsShortcutsMenuRegisteredForApp(app_user_model_id));
  return jump_list_entry_map_[app_user_model_id].size();
}

bool OsIntegrationTestOverrideImpl::IsShortcutsMenuRegisteredForApp(
    const std::wstring& app_user_model_id) {
  return base::Contains(jump_list_entry_map_, app_user_model_id);
}

base::expected<bool, std::string>
OsIntegrationTestOverrideImpl::IsUninstallRegisteredWithOs(
    const webapps::AppId& app_id,
    const std::string& app_name,
    Profile* profile) {
  base::win::RegKey uninstall_reg_key;
  LONG result = uninstall_reg_key.Open(HKEY_CURRENT_USER, kUninstallRegistryKey,
                                       KEY_READ);

  if (result == ERROR_FILE_NOT_FOUND) {
    return base::unexpected(
        "Cannot find the uninstall registry key. If a testing hive is being "
        "used, then this key needs to be created there on initialization.");
  }

  if (result != ERROR_SUCCESS) {
    return base::unexpected(
        base::StringPrintf("Cannot open the registry key: %ld", result));
  }

  const std::wstring key =
      GetUninstallStringKeyForTesting(profile->GetPath(), app_id);

  base::win::RegKey uninstall_reg_entry_key;
  result = uninstall_reg_entry_key.Open(uninstall_reg_key.Handle(), key.c_str(),
                                        KEY_READ);
  if (result == ERROR_FILE_NOT_FOUND) {
    return base::ok(false);
  }

  if (result != ERROR_SUCCESS) {
    return base::unexpected(
        base::StringPrintf("Error opening uninstall key for app: %ld", result));
  }

  std::wstring display_icon_path;
  std::wstring display_name;
  std::wstring display_version;
  std::wstring application_version;
  std::wstring publisher;
  std::wstring uninstall_string;
  DWORD no_repair;
  DWORD no_modify;
  bool read_success = true;
  read_success &= uninstall_reg_entry_key.ReadValue(
                      L"DisplayIcon", &display_icon_path) == ERROR_SUCCESS;
  read_success &= uninstall_reg_entry_key.ReadValue(
                      L"DisplayName", &display_name) == ERROR_SUCCESS;
  read_success &= uninstall_reg_entry_key.ReadValue(
                      L"DisplayVersion", &display_version) == ERROR_SUCCESS;
  read_success &=
      uninstall_reg_entry_key.ReadValue(L"ApplicationVersion",
                                        &application_version) == ERROR_SUCCESS;
  read_success &= uninstall_reg_entry_key.ReadValue(L"Publisher", &publisher) ==
                  ERROR_SUCCESS;
  read_success &= uninstall_reg_entry_key.ReadValue(
                      L"UninstallString", &uninstall_string) == ERROR_SUCCESS;
  read_success &= uninstall_reg_entry_key.ReadValueDW(
                      L"NoRepair", &no_repair) == ERROR_SUCCESS;
  read_success &= uninstall_reg_entry_key.ReadValueDW(
                      L"NoModify", &no_modify) == ERROR_SUCCESS;
  if (!read_success) {
    return base::unexpected("Error reading registry values");
  }

  if (display_version != L"1.0" || application_version != L"1.0" ||
      no_repair != 1 || no_modify != 1 ||
      publisher != install_static::GetChromeInstallSubDirectory()) {
    return base::unexpected("Incorrect static registry data.");
  }

  base::FilePath web_app_icon_dir = GetOsIntegrationResourcesDirectoryForApp(
      profile->GetPath(), app_id, GURL());
  base::FilePath expected_icon_path =
      internals::GetIconFilePath(web_app_icon_dir, base::UTF8ToUTF16(app_name));
  if (expected_icon_path.value() != display_icon_path) {
    return base::unexpected(base::StrCat(
        {"Invalid icon path ", base::WideToUTF8(display_icon_path),
         ", expected ", base::WideToUTF8(expected_icon_path.value())}));
  }
  if (base::UTF8ToWide(app_name) != display_name) {
    return base::unexpected(
        base::StrCat({"Invalid display name ", base::WideToUTF8(display_name),
                      ", expected ", app_name}));
  }
  std::wstring expected_uninstall_substr =
      base::StrCat({L"--uninstall-app-id=", base::UTF8ToWide(app_id)});
  if (!base::Contains(uninstall_string, expected_uninstall_substr)) {
    return base::unexpected(base::StrCat({"Could not find uninstall flag: ",
                                          base::WideToUTF8(uninstall_string)}));
  }

  return true;
}
#endif  // BUILDFLAG(IS_WIN)

const OsIntegrationTestOverrideImpl::AppProtocolList&
OsIntegrationTestOverrideImpl::protocol_scheme_registrations() {}

OsIntegrationTestOverrideImpl*
OsIntegrationTestOverrideImpl::AsOsIntegrationTestOverrideImpl() {}

#if BUILDFLAG(IS_WIN)
void OsIntegrationTestOverrideImpl::AddShortcutsMenuJumpListEntryForApp(
    const std::wstring& app_user_model_id,
    const std::vector<scoped_refptr<ShellLinkItem>>& shell_link_items) {
  jump_list_entry_map_[app_user_model_id] = shell_link_items;
  shortcut_menu_apps_registered_.emplace(app_user_model_id);
}

void OsIntegrationTestOverrideImpl::DeleteShortcutsMenuJumpListEntryForApp(
    const std::wstring& app_user_model_id) {
  jump_list_entry_map_.erase(app_user_model_id);
  shortcut_menu_apps_registered_.erase(app_user_model_id);
}

base::FilePath OsIntegrationTestOverrideImpl::desktop() {
  return desktop_.GetPath();
}
base::FilePath OsIntegrationTestOverrideImpl::application_menu() {
  return application_menu_.GetPath().Append(
      InstallUtil::GetChromeAppsShortcutDirName());
}
base::FilePath OsIntegrationTestOverrideImpl::quick_launch() {
  return quick_launch_.GetPath();
}
base::FilePath OsIntegrationTestOverrideImpl::startup() {
  return startup_.GetPath();
}
#endif  // BUILDFLAG(IS_WIN)

#if BUILDFLAG(IS_MAC)
bool OsIntegrationTestOverrideImpl::IsChromeAppsValid() {
  return chrome_apps_folder_.IsValid();
}
base::FilePath OsIntegrationTestOverrideImpl::chrome_apps_folder() {
  return chrome_apps_folder_.GetPath();
}
void OsIntegrationTestOverrideImpl::EnableOrDisablePathOnLogin(
    const base::FilePath& file_path,
    bool enable_on_login) {
  startup_enabled_[file_path] = enable_on_login;
}
#endif  // BUILDFLAG(IS_MAC)

#if BUILDFLAG(IS_LINUX)
base::FilePath OsIntegrationTestOverrideImpl::desktop() {}
base::FilePath OsIntegrationTestOverrideImpl::startup() {}
base::FilePath OsIntegrationTestOverrideImpl::applications() {}
base::FilePath OsIntegrationTestOverrideImpl::xdg_data_home_dir() {}
base::Environment* OsIntegrationTestOverrideImpl::environment() {}
#endif  // BUILDFLAG(IS_LINUX)

void OsIntegrationTestOverrideImpl::RegisterProtocolSchemes(
    const webapps::AppId& app_id,
    std::vector<std::string> protocols) {}

OsIntegrationTestOverrideImpl::OsIntegrationTestOverrideImpl(
    const base::FilePath& base_path) {}

OsIntegrationTestOverrideImpl::~OsIntegrationTestOverrideImpl() {}

#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
SkColor OsIntegrationTestOverrideImpl::GetIconTopLeftColorFromShortcutFile(
    const base::FilePath& shortcut_path) {
  CHECK(base::PathExists(shortcut_path));
#if BUILDFLAG(IS_MAC)
  base::FilePath icon_path =
      shortcut_path.AppendASCII("Contents/Resources/app.icns");
  base::apple::ScopedCFTypeRef<CFDictionaryRef> empty_dict(
      CFDictionaryCreate(nullptr, nullptr, nullptr, 0, nullptr, nullptr));
  base::apple::ScopedCFTypeRef<CFURLRef> url =
      base::apple::FilePathToCFURL(icon_path);
  base::apple::ScopedCFTypeRef<CGImageSourceRef> source(
      CGImageSourceCreateWithURL(url.get(), nullptr));
  if (!source) {
    return 0;
  }
  // Get the first icon in the .icns file (index 0)
  base::apple::ScopedCFTypeRef<CGImageRef> cg_image(
      CGImageSourceCreateImageAtIndex(source.get(), 0, empty_dict.get()));
  if (!cg_image) {
    return 0;
  }
  SkBitmap bitmap = skia::CGImageToSkBitmap(cg_image.get());
  if (bitmap.empty()) {
    return 0;
  }
  return bitmap.getColor(0, 0);
#elif BUILDFLAG(IS_WIN)
  SHFILEINFO file_info = {0};
  if (SHGetFileInfo(shortcut_path.value().c_str(), FILE_ATTRIBUTE_NORMAL,
                    &file_info, sizeof(file_info),
                    SHGFI_ICON | 0 | SHGFI_USEFILEATTRIBUTES)) {
    const SkBitmap bitmap = IconUtil::CreateSkBitmapFromHICON(file_info.hIcon);
    if (bitmap.empty()) {
      return 0;
    }
    return bitmap.getColor(0, 0);
  } else {
    return 0;
  }
#endif
}
#endif

#if BUILDFLAG(IS_WIN)
SkColor OsIntegrationTestOverrideImpl::ReadColorFromShortcutMenuIcoFile(
    const base::FilePath& file_path) {
  HICON icon = static_cast<HICON>(
      LoadImage(NULL, file_path.value().c_str(), IMAGE_ICON, 32, 32,
                LR_LOADTRANSPARENT | LR_LOADFROMFILE));
  base::win::ScopedHICON scoped_icon(icon);
  SkBitmap output_image =
      IconUtil::CreateSkBitmapFromHICON(scoped_icon.get(), gfx::Size(32, 32));
  SkColor color = output_image.getColor(output_image.dimensions().width() / 2,
                                        output_image.dimensions().height() / 2);
  return color;
}
#endif

}  // namespace web_app