// 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/browser/web_applications/os_integration/web_app_handler_registration_utils_win.h"
#include <string_view>
#include <utility>
#include <vector>
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/hash/hash.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_attributes_entry.h"
#include "chrome/browser/profiles/profile_attributes_storage.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/shell_integration.h"
#include "chrome/browser/web_applications/chrome_pwa_launcher/chrome_pwa_launcher_util.h"
#include "chrome/browser/web_applications/os_integration/web_app_shortcut_win.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/install_static/install_util.h"
#include "chrome/installer/util/shell_util.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/browser_thread.h"
#include "net/base/filename_util.h"
namespace web_app {
namespace {
// UMA metric name for file handler registration result.
constexpr const char* kRegistrationResultMetric =
"Apps.FileHandler.Registration.Win.Result";
// Returns true if the app with id |app_id| is currently installed in one or
// more profiles, excluding |cur_profile_path|, and has its web_app launcher
// registered with Windows as a handler for the associations it supports.
// Sets |only_profile_with_app_installed| to the path of profile that is the
// only profile with the app installed, an empty path otherwise. If the app is
// only installed in exactly one other profile, it will need its app name
// updated.
bool IsWebAppLauncherRegisteredWithWindows(
const webapps::AppId& app_id,
const base::FilePath& cur_profile_path,
base::FilePath* only_profile_with_app_installed) {
ProfileManager* profile_manager = g_browser_process->profile_manager();
auto* storage = &profile_manager->GetProfileAttributesStorage();
bool found_app = false;
std::vector<ProfileAttributesEntry*> entries =
storage->GetAllProfilesAttributes();
for (ProfileAttributesEntry* entry : entries) {
base::FilePath profile_path = entry->GetPath();
if (profile_path == cur_profile_path)
continue;
std::wstring profile_prog_id = GetProgIdForApp(profile_path, app_id);
base::FilePath shim_app_path =
ShellUtil::GetApplicationPathForProgId(profile_prog_id);
if (shim_app_path.empty())
continue;
*only_profile_with_app_installed =
found_app ? base::FilePath() : profile_path;
found_app = true;
if (only_profile_with_app_installed->empty())
break;
}
return found_app;
}
// Construct a string that is used to specify which profile a web
// app is installed for. The string is of the form "( <profile name>)".
std::wstring GetAppNameExtensionForProfile(const base::FilePath& profile_path) {
std::wstring app_name_extension;
ProfileManager* profile_manager = g_browser_process->profile_manager();
ProfileAttributesStorage& storage =
profile_manager->GetProfileAttributesStorage();
ProfileAttributesEntry* entry =
storage.GetProfileAttributesWithPath(profile_path);
if (entry) {
app_name_extension.append(L" (");
app_name_extension.append(base::AsWString(entry->GetLocalProfileName()));
app_name_extension.append(L")");
}
return app_name_extension;
}
Result UpdateAppRegistration(const webapps::AppId& app_id,
const std::wstring& app_name,
const base::FilePath& profile_path,
const std::wstring& prog_id,
const std::wstring& app_name_extension) {
if (!base::DeleteFile(ShellUtil::GetApplicationPathForProgId(prog_id))) {
RecordRegistration(RegistrationResult::kFailToDeleteExistingRegistration);
return Result::kError;
}
std::wstring user_visible_app_name(app_name);
user_visible_app_name.append(app_name_extension);
base::FilePath web_app_path(
GetOsIntegrationResourcesDirectoryForApp(profile_path, app_id, GURL()));
std::optional<base::FilePath> app_launcher_path =
CreateAppLauncherFile(app_name, app_name_extension, web_app_path);
if (!app_launcher_path)
return Result::kError;
base::CommandLine app_launch_cmd =
GetAppLauncherCommand(app_id, app_launcher_path.value(), profile_path);
base::FilePath icon_path =
internals::GetIconFilePath(web_app_path, base::AsString16(app_name));
ShellUtil::AddApplicationClass(prog_id, app_launch_cmd, user_visible_app_name,
app_name, icon_path);
// Retrieve the file handler ProgIds and register the new app name for each of
// them.
const std::vector<std::wstring> file_handler_prog_ids =
ShellUtil::GetFileHandlerProgIdsForAppId(prog_id);
for (const auto& file_handler_prog_id : file_handler_prog_ids) {
ShellUtil::AddApplicationClass(file_handler_prog_id, app_launch_cmd,
user_visible_app_name, app_name, icon_path);
}
return Result::kOk;
}
bool AppNameHasProfileExtension(const std::wstring& app_name,
const base::FilePath& profile_path) {
std::wstring app_name_extension = GetAppNameExtensionForProfile(profile_path);
return base::EndsWith(app_name, app_name_extension,
base::CompareCase::SENSITIVE) &&
app_name.size() > app_name_extension.size();
}
// NOTE: prog_ids generated by this function are written to users' Windows
// registries. Changes to how they are generated will be disruptive to
// previously written values, and should therefore be avoided if possible.
std::wstring GetProgId(const base::FilePath& profile_path,
const webapps::AppId& app_id,
const std::optional<std::set<std::string>>&
file_extensions = std::nullopt) {
// On system-level Win7 installs of the browser we need a user-specific part
// to differentiate HKLM entries from different Windows profiles.
std::wstring user_specific_part;
ShellUtil::GetUserSpecificRegistrySuffix(&user_specific_part);
std::string string_to_hash =
base::StrCat({base::WideToUTF8(profile_path.BaseName().value()), app_id,
base::WideToUTF8(user_specific_part)});
if (file_extensions && !file_extensions->empty()) {
auto iter = file_extensions->begin();
// Provided file extensions must have a leading period. This is enforced
// to ensure that calls to this function have consistent syntax (and
// therefore get the same prog_id for a given set of inputs).
DCHECK_EQ(iter->at(0), '.');
string_to_hash += *iter;
while (++iter != file_extensions->end()) {
DCHECK_EQ(iter->at(0), '.');
string_to_hash += ";";
string_to_hash += *iter;
}
}
const uint32_t hash = base::PersistentHash(string_to_hash);
return base::UTF16ToWide(
base::StrCat({base::AsStringPiece16(install_static::GetBaseAppId()), u".",
base::NumberToString16(hash)}));
}
} // namespace
base::CommandLine GetAppLauncherCommand(const webapps::AppId& app_id,
const base::FilePath& app_launcher_path,
const base::FilePath& profile_path) {
base::CommandLine app_launcher_command(app_launcher_path);
shell_integration::AppendProfileArgs(profile_path, &app_launcher_command);
app_launcher_command.AppendSwitchASCII(switches::kAppId, app_id);
return app_launcher_command;
}
std::wstring GetAppNameExtensionForNextInstall(
const webapps::AppId& app_id,
const base::FilePath& profile_path) {
// Return a profile-specific app name extension only if duplicate |app_id|
// installations exist in other profiles.
base::FilePath only_profile_with_app_installed;
if (IsWebAppLauncherRegisteredWithWindows(app_id, profile_path,
&only_profile_with_app_installed)) {
return GetAppNameExtensionForProfile(profile_path);
}
return std::wstring();
}
base::FilePath GetAppSpecificLauncherFilename(const std::wstring& app_name) {
// Remove any characters that are illegal in Windows filenames.
base::FilePath::StringType sanitized_app_name =
internals::GetSanitizedFileName(base::AsString16(app_name)).value();
// If |sanitized_app_name| is a reserved filename, prepend '_' to allow its
// use as the launcher filename (e.g. "nul" => "_nul"). Prepending is
// preferred over appending in order to handle filenames containing '.', as
// Windows' logic for checking reserved filenames views characters after '.'
// as file extensions, and only the pre-file-extension portion is checked for
// legitimacy (e.g. "nul_" is allowed, but "nul.a_" is not).
if (net::IsReservedNameOnWindows(sanitized_app_name))
sanitized_app_name.insert(0, 1, FILE_PATH_LITERAL('_'));
// Add .exe extension.
return base::FilePath(sanitized_app_name)
.AddExtension(FILE_PATH_LITERAL("exe"));
}
// See https://docs.microsoft.com/en-us/windows/win32/com/-progid--key for
// the allowed characters in a prog_id. Since the prog_id is stored in the
// Windows registry, the mapping between a given profile+app_id and a prog_id
// can not be changed.
std::wstring GetProgIdForApp(const base::FilePath& profile_path,
const webapps::AppId& app_id) {
return GetProgId(profile_path, app_id);
}
std::wstring GetProgIdForAppFileHandler(
const base::FilePath& profile_path,
const webapps::AppId& app_id,
const std::set<std::string>& file_extensions) {
return GetProgId(profile_path, app_id, file_extensions);
}
std::optional<base::FilePath> CreateAppLauncherFile(
const std::wstring& app_name,
const std::wstring& app_name_extension,
const base::FilePath& web_app_path) {
if (!base::CreateDirectory(web_app_path)) {
DPLOG(ERROR) << "Unable to create web app dir";
RecordRegistration(RegistrationResult::kFailToCopyFromGenericLauncher);
return std::nullopt;
}
base::FilePath icon_path =
internals::GetIconFilePath(web_app_path, base::AsString16(app_name));
base::FilePath pwa_launcher_path = GetChromePwaLauncherPath();
std::wstring user_visible_app_name(app_name);
user_visible_app_name.append(app_name_extension);
base::FilePath app_specific_launcher_path = web_app_path.Append(
GetAppSpecificLauncherFilename(user_visible_app_name));
// Create a hard link to the chrome pwa launcher app. Delete any pre-existing
// version of the file first.
base::DeleteFile(app_specific_launcher_path);
if (!base::CreateWinHardLink(app_specific_launcher_path, pwa_launcher_path) &&
!base::CopyFile(pwa_launcher_path, app_specific_launcher_path)) {
DPLOG(ERROR) << "Unable to copy the generic PWA launcher."
<< " pwa_launcher_path: " << pwa_launcher_path
<< " app_specific_launcher_path: "
<< app_specific_launcher_path;
RecordRegistration(RegistrationResult::kFailToCopyFromGenericLauncher);
return std::nullopt;
}
return app_specific_launcher_path;
}
void CheckAndUpdateExternalInstallations(const base::FilePath& cur_profile_path,
const webapps::AppId& app_id,
ResultCallback callback) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
std::wstring prog_id = GetProgIdForApp(cur_profile_path, app_id);
bool cur_profile_has_installation =
!ShellUtil::GetApplicationPathForProgId(prog_id).empty();
base::FilePath external_installation_profile_path;
IsWebAppLauncherRegisteredWithWindows(app_id, cur_profile_path,
&external_installation_profile_path);
// Naming updates are only required if a single external installation exists.
if (external_installation_profile_path.empty()) {
// This enumeration signals if there was not an error. Exiting early here is
// WAI, so this is a success.
std::move(callback).Run(Result::kOk);
return;
}
std::wstring external_installation_prog_id =
GetProgIdForApp(external_installation_profile_path, app_id);
std::wstring external_installation_name =
ShellUtil::GetAppName(external_installation_prog_id);
// Determine the updated name and extension for the external installation
// based on the state of the installation in |cur_profile_path|.
std::wstring updated_name;
std::wstring updated_extension;
if (cur_profile_has_installation) {
// The single installation in a different profile should have a
// profile-specific name.
if (AppNameHasProfileExtension(external_installation_name,
external_installation_profile_path)) {
// This enumeration signals if there was not an error. Exiting early here
// is WAI, so this is a success.
std::move(callback).Run(Result::kOk);
return;
}
updated_name = external_installation_name;
updated_extension =
GetAppNameExtensionForProfile(external_installation_profile_path);
} else {
// The single installation in a different profile should not have a
// profile-specific name.
if (!AppNameHasProfileExtension(external_installation_name,
external_installation_profile_path)) {
// This enumeration signals if there was not an error. Exiting early here
// is WAI, so this is a success.
std::move(callback).Run(Result::kOk);
return;
}
// Remove the profile-specific extension from the external installation.
std::wstring external_installation_extension =
GetAppNameExtensionForProfile(external_installation_profile_path);
updated_name = std::wstring(
std::wstring_view(external_installation_name.c_str(),
external_installation_name.size() -
external_installation_extension.size()));
updated_extension = std::wstring();
}
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&UpdateAppRegistration, app_id, updated_name,
external_installation_profile_path,
external_installation_prog_id, updated_extension),
std::move(callback));
}
// Record UMA metric for the result of file handler registration.
void RecordRegistration(RegistrationResult result) {
UMA_HISTOGRAM_ENUMERATION(kRegistrationResultMetric, result);
}
} // namespace web_app