910e62b5创建于 1月15日历史提交
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/win/default_apps_util.h"

#include <shobjidl.h>

#include <shellapi.h>
#include <wrl/client.h>

#include <optional>
#include <string_view>

#include "base/metrics/histogram_functions.h"
#include "base/strings/cstring_view.h"
#include "base/strings/escape.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/com_init_util.h"
#include "base/win/registry.h"
#include "base/win/windows_version.h"

namespace {

// Undocumented COM interface for opening the "set default app for <file type>"
// dialog.
class __declspec(uuid("6A283FE2-ECFA-4599-91C4-E80957137B26")) IOpenWithLauncher
    : public IUnknown {
 public:
  virtual HRESULT STDMETHODCALLTYPE Launch(HWND hWndParent,
                                           const wchar_t* lpszPath,
                                           int flags) = 0;
};

// Returns the class ID for the "Execute Unknown" class, read from
// `HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\OpenWith`.
// Returns std::nullopt upon failure.
std::optional<CLSID> GetOpenWithLauncherCLSID() {
  std::wstring value;
  base::win::RegKey(HKEY_LOCAL_MACHINE,
                    L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\OpenWith",
                    KEY_QUERY_VALUE)
      .ReadValue(L"OpenWithLauncher", &value);
  if (value.empty()) {
    return std::nullopt;
  }
  CLSID clsid;
  const auto hr = ::CLSIDFromString(value.c_str(), &clsid);
  return SUCCEEDED(hr) ? std::make_optional(clsid) : std::nullopt;
}

// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused. These represent various outcomes of
// attempting to open the Settings app via the IOpenWithLauncher COM interface.
enum class OpenWithLauncherResult {
  // The settings window was launched successfully and the user changed a
  // setting.
  kSuccess = 0,
  // The settings window was launched successfully, but the user closed it
  // without taking action.
  kSuccessNoChange = 1,
  // Failed to get the class ID from the registry.
  kClsidNotFound = 2,
  // Failed to create an instance of the COM class.
  kComError = 3,
  // Launching the Settings app failed.
  kLaunchError = 4,
  kMaxValue = kLaunchError
};

// Records the `result` of opening the Settings app via the IOpenWithLauncher
// COM interface.
void RecordOpenWithLauncherResult(OpenWithLauncherResult result) {
  base::UmaHistogramEnumeration("Windows.OpenWithLauncherResult", result);
}

// Returns the target used as a activate parameter when opening the settings
// pointing to the page that is the most relevant to a user trying to change the
// default handler for `protocol`.
std::wstring GetTargetForDefaultAppsSettings(std::wstring_view protocol) {
  static constexpr std::wstring_view kSystemSettingsDefaultAppsPrefix(
      L"SystemSettings_DefaultApps_");
  if (base::EqualsCaseInsensitiveASCII(protocol, L"http")) {
    return base::StrCat({kSystemSettingsDefaultAppsPrefix, L"Browser"});
  }
  if (base::EqualsCaseInsensitiveASCII(protocol, L"mailto")) {
    return base::StrCat({kSystemSettingsDefaultAppsPrefix, L"Email"});
  }
  return L"SettingsPageAppsDefaultsProtocolView";
}

}  // namespace

namespace base::win {

bool LaunchDefaultAppsSettingsModernDialog(std::wstring_view protocol) {
  // The appModelId looks arbitrary but it is the same in Win8 and Win10. There
  // is no easy way to retrieve the appModelId from the registry.
  static constexpr wchar_t kControlPanelAppModelId[] =
      L"windows.immersivecontrolpanel_cw5n1h2txyewy"
      L"!microsoft.windows.immersivecontrolpanel";

  Microsoft::WRL::ComPtr<IApplicationActivationManager> activator;
  HRESULT hr =
      ::CoCreateInstance(CLSID_ApplicationActivationManager, nullptr,
                         CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&activator));
  if (FAILED(hr)) {
    return false;
  }

  DWORD pid = 0;
  hr = activator->ActivateApplication(
      kControlPanelAppModelId, L"page=SettingsPageAppsDefaults", AO_NONE, &pid);
  if (FAILED(hr)) {
    return false;
  }
  // Scrolling to a specific protocol is only possible on Windows 10.
  if (protocol.empty() || GetVersion() >= Version::WIN11) {
    return true;
  }

  hr = activator->ActivateApplication(
      kControlPanelAppModelId,
      base::StrCat({L"page=SettingsPageAppsDefaults&target=",
                    GetTargetForDefaultAppsSettings(protocol)})
          .c_str(),
      AO_NONE, &pid);
  return SUCCEEDED(hr);
}

bool LaunchDefaultAppForFileExtensionSettings(
    base::wcstring_view file_extension,
    HWND parent_hwnd) {
  AssertComInitialized();

  // Create an "Execute Unknown" COM object with `IOpenWithLauncher` interface.
  const auto open_with_launcher_clsid = GetOpenWithLauncherCLSID();
  if (!open_with_launcher_clsid) {
    RecordOpenWithLauncherResult(OpenWithLauncherResult::kClsidNotFound);
    return false;
  }
  Microsoft::WRL::ComPtr<IOpenWithLauncher> open_with_launcher;
  if (FAILED(::CoCreateInstance(*open_with_launcher_clsid, nullptr,
                                CLSCTX_LOCAL_SERVER,
                                IID_PPV_ARGS(&open_with_launcher)))) {
    RecordOpenWithLauncherResult(OpenWithLauncherResult::kComError);
    return false;
  }

  // Open "select a default app for `file_extension` files" dialog.
  // `kOpenWithFlags` is a working `flags` argument discovered by observation.
  static constexpr int kOpenWithFlags = 0x2004;
  const HRESULT hr = open_with_launcher->Launch(
      parent_hwnd, file_extension.data(), kOpenWithFlags);
  if (SUCCEEDED(hr)) {
    RecordOpenWithLauncherResult(OpenWithLauncherResult::kSuccess);
    return true;
  }
  // On Windows 10, `ERROR_CANCELLED` just means the user closed the dialog
  // without changing anything.
  if (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED)) {
    RecordOpenWithLauncherResult(OpenWithLauncherResult::kSuccessNoChange);
    return true;
  }
  RecordOpenWithLauncherResult(OpenWithLauncherResult::kLaunchError);
  return false;
}

bool LaunchSettingsDefaultApps(std::wstring_view app_name,
                               bool is_per_user_install) {
  AssertComInitialized();

  // The `app_name` parameter is escaped using URL escaping, not because it is
  // part of a URL, but because the Settings app parses the parameter as if it
  // was part of a URL.
  // The '+' character is not escaped by `EscapeQueryParamValue`, because it is
  // valid in a query parameter, but it is not valid in this context, so it
  // needs to be escaped.
  const std::wstring settings_url = StrCat(
      {L"ms-settings:defaultapps?",
       is_per_user_install ? L"registeredAppUser=" : L"registeredAppMachine=",
       ASCIIToWide(EscapeQueryParamValue(WideToUTF8(app_name),
                                         /*use_plus=*/false))});
  return reinterpret_cast<intptr_t>(::ShellExecute(
             /*hwnd=*/nullptr, L"open", settings_url.c_str(),
             /*lpParameters=*/nullptr,
             /*lpDirectory=*/nullptr, SW_SHOWNORMAL)) > 32;
}

bool LaunchSettingsUri(base::wcstring_view uri) {
  AssertComInitialized();

  Microsoft::WRL::ComPtr<IApplicationActivationManager> activator;
  HRESULT hr = ::CoCreateInstance(CLSID_ApplicationActivationManager, nullptr,
                                  CLSCTX_ALL, IID_PPV_ARGS(&activator));
  if (FAILED(hr)) {
    return false;
  }

  ::CoAllowSetForegroundWindow(activator.Get(), nullptr);

  static constexpr wchar_t kControlPanelAppModelId[] =
      L"windows.immersivecontrolpanel_cw5n1h2txyewy"
      L"!microsoft.windows.immersivecontrolpanel";
  DWORD pid = 0;
  hr = activator->ActivateApplication(kControlPanelAppModelId, uri.c_str(),
                                      AO_NONE, &pid);
  return SUCCEEDED(hr);
}

}  // namespace base::win