From ea59276208860199f098f53a5279311ae98f718a Mon Sep 17 00:00:00 2001 From: ema Date: Tue, 27 Oct 2026 15:27:39 +0800 Subject: [PATCH] Add a Windows 11 Explorer context menu so QuickLook can be opened from the shell. Co-authored-by: Cursor --- Build/AppxManifest.xml | 26 +- QuickLook.Common/Helpers/ProcessHelper.cs | 12 +- .../QuickLook.Native32/ContextMenuExports.cpp | 49 + .../QuickLook.Native32/ContextMenuGuid.h | 24 + .../QuickLook.Native32/ExplorerCommand.cpp | 461 +++++++++ .../QuickLook.Native32/ExplorerCommand.h | 70 ++ .../QuickLook.Native32/HelperMethods.cpp | 10 +- .../QuickLook.ContextMenu.def | 4 + .../QuickLook.ContextMenu.rc | Bin 0 -> 202 bytes .../QuickLook.Native32.vcxproj | 16 + .../QuickLook.Native32/SignPackage.cpp | 186 ++++ .../QuickLook.Native32/dllmain.cpp | 14 +- .../QuickLook.Native32/resource.h | 20 + .../QuickLook.Native64.vcxproj | 16 + .../QuickLook.NativeArm64.vcxproj | 16 + QuickLook/App.xaml.cs | 19 + QuickLook/Helpers/ContextMenuRegistration.cs | 887 ++++++++++++++++++ 17 files changed, 1818 insertions(+), 12 deletions(-) create mode 100644 QuickLook.Native/QuickLook.Native32/ContextMenuExports.cpp create mode 100644 QuickLook.Native/QuickLook.Native32/ContextMenuGuid.h create mode 100644 QuickLook.Native/QuickLook.Native32/ExplorerCommand.cpp create mode 100644 QuickLook.Native/QuickLook.Native32/ExplorerCommand.h create mode 100644 QuickLook.Native/QuickLook.Native32/QuickLook.ContextMenu.def create mode 100644 QuickLook.Native/QuickLook.Native32/QuickLook.ContextMenu.rc create mode 100644 QuickLook.Native/QuickLook.Native32/SignPackage.cpp create mode 100644 QuickLook.Native/QuickLook.Native32/resource.h create mode 100644 QuickLook/Helpers/ContextMenuRegistration.cs diff --git a/Build/AppxManifest.xml b/Build/AppxManifest.xml index 3b5e7ec9d..dfa9a2069 100644 --- a/Build/AppxManifest.xml +++ b/Build/AppxManifest.xml @@ -1,8 +1,12 @@ + xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" + IgnorableNamespaces="com desktop4 desktop5"> @@ -36,6 +40,26 @@ EntryPoint="Windows.FullTrustApplication"> + + + + + + + + + + + + + + + + + + + + . + +#include "ExplorerCommand.h" + +#include "ContextMenuGuid.h" + +#include + +HMODULE g_module = nullptr; +long g_moduleRefs = 0; +long g_moduleLocks = 0; + +STDAPI DllCanUnloadNow() +{ + return (g_moduleRefs == 0 && g_moduleLocks == 0) ? S_OK : S_FALSE; +} + +STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv) +{ + if (ppv == nullptr) + return E_POINTER; + + *ppv = nullptr; + if (rclsid != CLSID_QuickLookCommand) + return CLASS_E_CLASSNOTAVAILABLE; + + auto* factory = new (std::nothrow) ClassFactory(); + if (factory == nullptr) + return E_OUTOFMEMORY; + + const HRESULT hr = factory->QueryInterface(riid, ppv); + factory->Release(); + return hr; +} diff --git a/QuickLook.Native/QuickLook.Native32/ContextMenuGuid.h b/QuickLook.Native/QuickLook.Native32/ContextMenuGuid.h new file mode 100644 index 000000000..e2b4a0a96 --- /dev/null +++ b/QuickLook.Native/QuickLook.Native32/ContextMenuGuid.h @@ -0,0 +1,24 @@ +// Copyright © 2017-2026 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include + +// {A7C3E4B1-5D28-4F6A-9B30-8E1F2A4C6D70} +static const GUID CLSID_QuickLookCommand = { + 0xa7c3e4b1, 0x5d28, 0x4f6a, {0x9b, 0x30, 0x8e, 0x1f, 0x2a, 0x4c, 0x6d, 0x70}}; diff --git a/QuickLook.Native/QuickLook.Native32/ExplorerCommand.cpp b/QuickLook.Native/QuickLook.Native32/ExplorerCommand.cpp new file mode 100644 index 000000000..b68a14c23 --- /dev/null +++ b/QuickLook.Native/QuickLook.Native32/ExplorerCommand.cpp @@ -0,0 +1,461 @@ +// Copyright © 2017-2026 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "ExplorerCommand.h" + +#include "ContextMenuGuid.h" +#include "resource.h" + +#include +#include +#include +#include + +#include +#include + +#pragma comment(lib, "shell32.lib") +#pragma comment(lib, "ole32.lib") +#pragma comment(lib, "shlwapi.lib") +#pragma comment(lib, "uuid.lib") + +namespace +{ + std::wstring ModuleFileName() + { + std::wstring path(32768, L'\0'); + const DWORD length = GetModuleFileNameW(g_module, &path[0], static_cast(path.size())); + path.resize(length); + return path; + } + + std::wstring ModuleDirectory() + { + std::wstring path = ModuleFileName(); + const size_t slash = path.find_last_of(L"\\/"); + if (slash != std::wstring::npos) + path.resize(slash); + return path; + } + + void FreeText(PWSTR text) + { + CoTaskMemFree(text); + } + + bool TryGetFileSystemPath(IShellItem* item, std::wstring& path) + { + if (!item) + return false; + + PWSTR name = nullptr; + if (FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, &name)) || name == nullptr || name[0] == L'\0') + { + FreeText(name); + return false; + } + + path.assign(name); + FreeText(name); + return true; + } + + bool TryGetFirstFileSystemPath(IShellItemArray* items, std::wstring& path) + { + if (!items) + return false; + + DWORD count = 0; + if (FAILED(items->GetCount(&count))) + return false; + + for (DWORD index = 0; index < count; ++index) + { + Microsoft::WRL::ComPtr item; + if (FAILED(items->GetItemAt(index, &item))) + continue; + if (TryGetFileSystemPath(item.Get(), path)) + return true; + } + + return false; + } + + bool TryGetFolderFromSite(IUnknown* site, std::wstring& path) + { + if (!site) + return false; + + Microsoft::WRL::ComPtr browser; + if (FAILED(IUnknown_QueryService(site, SID_STopLevelBrowser, IID_PPV_ARGS(&browser)))) + return false; + + Microsoft::WRL::ComPtr view; + if (FAILED(browser->QueryActiveShellView(&view))) + return false; + + Microsoft::WRL::ComPtr folderView; + if (FAILED(view.As(&folderView))) + return false; + + Microsoft::WRL::ComPtr folder; + if (FAILED(folderView->GetFolder(IID_PPV_ARGS(&folder)))) + return false; + + PIDLIST_ABSOLUTE pidl = nullptr; + if (FAILED(folder->GetCurFolder(&pidl)) || pidl == nullptr) + return false; + + PWSTR name = nullptr; + const HRESULT hr = SHGetNameFromIDList(pidl, SIGDN_FILESYSPATH, &name); + CoTaskMemFree(pidl); + if (FAILED(hr) || name == nullptr || name[0] == L'\0') + { + FreeText(name); + return false; + } + + path.assign(name); + FreeText(name); + return true; + } + + HRESULT LaunchQuickLook(const std::wstring& target) + { + if (target.find(L'"') != std::wstring::npos) + return E_INVALIDARG; + + const std::wstring directory = ModuleDirectory(); + const std::wstring executable = directory + L"\\QuickLook.exe"; + if (GetFileAttributesW(executable.c_str()) == INVALID_FILE_ATTRIBUTES) + return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); + + std::wstring command = L"\""; + command += executable; + command += L"\" \""; + command += target; + command += L"\""; + + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + startup.dwFlags = STARTF_USESHOWWINDOW; + startup.wShowWindow = SW_SHOWNORMAL; + + PROCESS_INFORMATION process{}; + if (!CreateProcessW(executable.c_str(), &command[0], nullptr, nullptr, FALSE, 0, nullptr, directory.c_str(), &startup, &process)) + return HRESULT_FROM_WIN32(GetLastError()); + + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + return S_OK; + } + + const wchar_t* TitleForLanguage(const wchar_t* language, bool allowParent) + { + struct Entry + { + const wchar_t* language; + const wchar_t* title; + }; + + // Same shape as VS Code's OpenWithCodeContextMenu: a verb around the product name. + static const Entry entries[] = { + { L"zh-CN", L"使用 QuickLook 预览" }, + { L"zh-Hans", L"使用 QuickLook 预览" }, + { L"zh-SG", L"使用 QuickLook 预览" }, + { L"zh-TW", L"使用 QuickLook 預覽" }, + { L"zh-HK", L"使用 QuickLook 預覽" }, + { L"zh-MO", L"使用 QuickLook 預覽" }, + { L"zh-Hant", L"使用 QuickLook 預覽" }, + { L"zh", L"使用 QuickLook 预览" }, + { L"ja", L"QuickLook でプレビュー" }, + { L"ko", L"QuickLook로 미리 보기" }, + { L"de", L"Mit QuickLook ansehen" }, + { L"fr", L"Aperçu avec QuickLook" }, + { L"es", L"Vista previa con QuickLook" }, + { L"it", L"Anteprima con QuickLook" }, + { L"pt-BR", L"Visualizar com o QuickLook" }, + { L"pt-PT", L"Pré-visualizar com o QuickLook" }, + { L"pt", L"Visualizar com o QuickLook" }, + { L"ru", L"Просмотр в QuickLook" }, + { L"uk", L"Переглянути в QuickLook" }, + { L"pl", L"Podgląd w QuickLook" }, + { L"nl", L"Bekijken met QuickLook" }, + { L"tr", L"QuickLook ile önizle" }, + { L"vi", L"Xem trước bằng QuickLook" }, + { L"ar", L"معاينة باستخدام QuickLook" }, + { L"hu", L"Előnézet a QuickLookkal" }, + { L"sk", L"Náhľad v QuickLook" }, + { L"id", L"Pratinjau dengan QuickLook" }, + { L"ca", L"Previsualitza amb el QuickLook" }, + { L"nb", L"Forhåndsvis med QuickLook" }, + { L"no", L"Forhåndsvis med QuickLook" }, + { L"mr", L"QuickLook सह पूर्वावलोकन" }, + { L"hi", L"QuickLook से पूर्वावलोकन" }, + { L"he", L"תצוגה מקדימה עם QuickLook" }, + { L"el", L"Προεπισκόπηση με το QuickLook" }, + { L"ro", L"Previzualizare cu QuickLook" }, + { L"en", L"QuickLook" }, + }; + + for (const auto& entry : entries) + { + if (_wcsicmp(language, entry.language) == 0) + return entry.title; + } + + if (!allowParent) + return nullptr; + + const wchar_t* dash = wcschr(language, L'-'); + if (dash == nullptr || dash == language) + return nullptr; + + const std::wstring parent(language, dash); + return TitleForLanguage(parent.c_str(), false); + } + + const wchar_t* MenuTitle() + { + ULONG count = 0; + ULONG length = 0; + if (GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &count, nullptr, &length) && length > 1) + { + std::wstring languages(length, L'\0'); + if (GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &count, &languages[0], &length)) + { + for (const wchar_t* language = languages.c_str(); *language != L'\0'; language += wcslen(language) + 1) + { + if (const wchar_t* title = TitleForLanguage(language, true)) + return title; + } + } + } + + wchar_t locale[LOCALE_NAME_MAX_LENGTH] = {}; + if (LCIDToLocaleName(GetUserDefaultUILanguage(), locale, ARRAYSIZE(locale), 0) > 0) + { + if (const wchar_t* title = TitleForLanguage(locale, true)) + return title; + } + + return L"QuickLook"; + } +} + +ExplorerCommand::ExplorerCommand() + : m_refs(1) +{ + InterlockedIncrement(&g_moduleRefs); +} + +ExplorerCommand::~ExplorerCommand() +{ + InterlockedDecrement(&g_moduleRefs); +} + +IFACEMETHODIMP ExplorerCommand::QueryInterface(REFIID riid, void** ppv) +{ + if (ppv == nullptr) + return E_POINTER; + + *ppv = nullptr; + if (riid == IID_IUnknown || riid == IID_IExplorerCommand) + *ppv = static_cast(this); + else if (riid == IID_IObjectWithSite) + *ppv = static_cast(this); + else + return E_NOINTERFACE; + + AddRef(); + return S_OK; +} + +IFACEMETHODIMP_(ULONG) ExplorerCommand::AddRef() +{ + return static_cast(InterlockedIncrement(&m_refs)); +} + +IFACEMETHODIMP_(ULONG) ExplorerCommand::Release() +{ + const long refs = InterlockedDecrement(&m_refs); + if (refs == 0) + delete this; + return static_cast(refs); +} + +IFACEMETHODIMP ExplorerCommand::GetTitle(IShellItemArray*, PWSTR* title) +{ + if (title == nullptr) + return E_POINTER; + + return SHStrDupW(MenuTitle(), title); +} + +IFACEMETHODIMP ExplorerCommand::GetIcon(IShellItemArray*, PWSTR* icon) +{ + if (icon == nullptr) + return E_POINTER; + + std::wstring resource = ModuleFileName(); + resource += L",-"; + resource += std::to_wstring(IDI_APP_ICON); + return SHStrDupW(resource.c_str(), icon); +} + +IFACEMETHODIMP ExplorerCommand::GetToolTip(IShellItemArray*, PWSTR* tip) +{ + if (tip != nullptr) + *tip = nullptr; + return E_NOTIMPL; +} + +IFACEMETHODIMP ExplorerCommand::GetCanonicalName(GUID* name) +{ + if (name == nullptr) + return E_POINTER; + + *name = CLSID_QuickLookCommand; + return S_OK; +} + +IFACEMETHODIMP ExplorerCommand::GetState(IShellItemArray* items, BOOL, EXPCMDSTATE* state) +{ + if (state == nullptr) + return E_POINTER; + + std::wstring path; + if (TryGetFirstFileSystemPath(items, path) || TryGetFolderFromSite(m_site.Get(), path)) + *state = ECS_ENABLED; + else if (items == nullptr) + *state = ECS_ENABLED; + else + { + DWORD count = 0; + if (FAILED(items->GetCount(&count)) || count == 0) + *state = ECS_ENABLED; + else + *state = ECS_HIDDEN; + } + + return S_OK; +} + +IFACEMETHODIMP ExplorerCommand::Invoke(IShellItemArray* items, IBindCtx*) +{ + std::wstring path; + if (!TryGetFirstFileSystemPath(items, path) && !TryGetFolderFromSite(m_site.Get(), path)) + return E_FAIL; + + return LaunchQuickLook(path); +} + +IFACEMETHODIMP ExplorerCommand::GetFlags(EXPCMDFLAGS* flags) +{ + if (flags == nullptr) + return E_POINTER; + + *flags = ECF_DEFAULT; + return S_OK; +} + +IFACEMETHODIMP ExplorerCommand::EnumSubCommands(IEnumExplorerCommand** commands) +{ + if (commands != nullptr) + *commands = nullptr; + return E_NOTIMPL; +} + +IFACEMETHODIMP ExplorerCommand::SetSite(IUnknown* site) +{ + m_site = site; + return S_OK; +} + +IFACEMETHODIMP ExplorerCommand::GetSite(REFIID riid, void** ppv) +{ + if (ppv == nullptr) + return E_POINTER; + if (m_site == nullptr) + return E_FAIL; + return m_site->QueryInterface(riid, ppv); +} + +ClassFactory::ClassFactory() + : m_refs(1) +{ + InterlockedIncrement(&g_moduleRefs); +} + +ClassFactory::~ClassFactory() +{ + InterlockedDecrement(&g_moduleRefs); +} + +IFACEMETHODIMP ClassFactory::QueryInterface(REFIID riid, void** ppv) +{ + if (ppv == nullptr) + return E_POINTER; + + *ppv = nullptr; + if (riid == IID_IUnknown || riid == IID_IClassFactory) + *ppv = static_cast(this); + else + return E_NOINTERFACE; + + AddRef(); + return S_OK; +} + +IFACEMETHODIMP_(ULONG) ClassFactory::AddRef() +{ + return static_cast(InterlockedIncrement(&m_refs)); +} + +IFACEMETHODIMP_(ULONG) ClassFactory::Release() +{ + const long refs = InterlockedDecrement(&m_refs); + if (refs == 0) + delete this; + return static_cast(refs); +} + +IFACEMETHODIMP ClassFactory::CreateInstance(IUnknown* outer, REFIID riid, void** ppv) +{ + if (ppv == nullptr) + return E_POINTER; + *ppv = nullptr; + if (outer != nullptr) + return CLASS_E_NOAGGREGATION; + + auto* command = new (std::nothrow) ExplorerCommand(); + if (command == nullptr) + return E_OUTOFMEMORY; + + const HRESULT hr = command->QueryInterface(riid, ppv); + command->Release(); + return hr; +} + +IFACEMETHODIMP ClassFactory::LockServer(BOOL lock) +{ + if (lock) + InterlockedIncrement(&g_moduleLocks); + else + InterlockedDecrement(&g_moduleLocks); + return S_OK; +} diff --git a/QuickLook.Native/QuickLook.Native32/ExplorerCommand.h b/QuickLook.Native/QuickLook.Native32/ExplorerCommand.h new file mode 100644 index 000000000..84e774c41 --- /dev/null +++ b/QuickLook.Native/QuickLook.Native32/ExplorerCommand.h @@ -0,0 +1,70 @@ +// Copyright © 2017-2026 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include +#include +#include + +extern HMODULE g_module; +extern long g_moduleRefs; +extern long g_moduleLocks; + +// Windows 11 top-level File Explorer command. One verb, no submenu. +class ExplorerCommand final : public IExplorerCommand, public IObjectWithSite +{ +public: + ExplorerCommand(); + ~ExplorerCommand(); + + IFACEMETHODIMP QueryInterface(REFIID riid, void** ppv) override; + IFACEMETHODIMP_(ULONG) AddRef() override; + IFACEMETHODIMP_(ULONG) Release() override; + + IFACEMETHODIMP GetTitle(IShellItemArray* items, PWSTR* title) override; + IFACEMETHODIMP GetIcon(IShellItemArray* items, PWSTR* icon) override; + IFACEMETHODIMP GetToolTip(IShellItemArray* items, PWSTR* tip) override; + IFACEMETHODIMP GetCanonicalName(GUID* name) override; + IFACEMETHODIMP GetState(IShellItemArray* items, BOOL okToBeSlow, EXPCMDSTATE* state) override; + IFACEMETHODIMP Invoke(IShellItemArray* items, IBindCtx* bindCtx) override; + IFACEMETHODIMP GetFlags(EXPCMDFLAGS* flags) override; + IFACEMETHODIMP EnumSubCommands(IEnumExplorerCommand** commands) override; + + IFACEMETHODIMP SetSite(IUnknown* site) override; + IFACEMETHODIMP GetSite(REFIID riid, void** ppv) override; + +private: + long m_refs; + Microsoft::WRL::ComPtr m_site; +}; + +class ClassFactory final : public IClassFactory +{ +public: + ClassFactory(); + ~ClassFactory(); + + IFACEMETHODIMP QueryInterface(REFIID riid, void** ppv) override; + IFACEMETHODIMP_(ULONG) AddRef() override; + IFACEMETHODIMP_(ULONG) Release() override; + IFACEMETHODIMP CreateInstance(IUnknown* outer, REFIID riid, void** ppv) override; + IFACEMETHODIMP LockServer(BOOL lock) override; + +private: + long m_refs; +}; diff --git a/QuickLook.Native/QuickLook.Native32/HelperMethods.cpp b/QuickLook.Native/QuickLook.Native32/HelperMethods.cpp index 011647472..8a7df1beb 100644 --- a/QuickLook.Native/QuickLook.Native32/HelperMethods.cpp +++ b/QuickLook.Native/QuickLook.Native32/HelperMethods.cpp @@ -166,7 +166,15 @@ bool HelperMethods::IsUWP() return false; UINT32 pn = 0; - return pGCPFN(&pn, nullptr) == ERROR_INSUFFICIENT_BUFFER; + if (pGCPFN(&pn, nullptr) != ERROR_INSUFFICIENT_BUFFER || pn == 0 || pn > 1024) + return false; + + WCHAR name[1024]; + if (pGCPFN(&pn, name) != ERROR_SUCCESS) + return false; + + // Sparse shell identity stays a normal desktop app. Only the Store package is UWP. + return _wcsnicmp(name, L"21090PaddyXu.QuickLook_", 24) == 0; } HWND HelperMethods::GetFocusedControl() diff --git a/QuickLook.Native/QuickLook.Native32/QuickLook.ContextMenu.def b/QuickLook.Native/QuickLook.Native32/QuickLook.ContextMenu.def new file mode 100644 index 000000000..c3764d29f --- /dev/null +++ b/QuickLook.Native/QuickLook.Native32/QuickLook.ContextMenu.def @@ -0,0 +1,4 @@ +EXPORTS + DllCanUnloadNow PRIVATE + DllGetClassObject PRIVATE + SignAppxPackage PRIVATE diff --git a/QuickLook.Native/QuickLook.Native32/QuickLook.ContextMenu.rc b/QuickLook.Native/QuickLook.Native32/QuickLook.ContextMenu.rc new file mode 100644 index 0000000000000000000000000000000000000000..964b92d48e3835d53a7f2d2d70d88f56c795ab0d GIT binary patch literal 202 zcmZXO!3u*g5JcbEuaJ1p7ZjoBrBH0^S&*Va#nOmU{C&qQ#e)nXvzxcGN$B~|@a9WP zN6d-EdXuUd&x@3Z + + NotUsing + + + NotUsing + /utf-8 %(AdditionalOptions) + + + NotUsing + + @@ -269,6 +280,11 @@ + + + QuickLook.ContextMenu.def + + diff --git a/QuickLook.Native/QuickLook.Native32/SignPackage.cpp b/QuickLook.Native/QuickLook.Native32/SignPackage.cpp new file mode 100644 index 000000000..ab627b1e6 --- /dev/null +++ b/QuickLook.Native/QuickLook.Native32/SignPackage.cpp @@ -0,0 +1,186 @@ +// Copyright © 2017-2026 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include +#include + +// SignerSignEx2 has no SDK header. Layout matches the Appx signing sample. +typedef struct _SIGNER_FILE_INFO +{ + DWORD cbSize; + LPCWSTR pwszFileName; + HANDLE hFile; +} SIGNER_FILE_INFO; + +typedef struct _SIGNER_SUBJECT_INFO +{ + DWORD cbSize; + DWORD* pdwIndex; + DWORD dwSubjectChoice; + union + { + SIGNER_FILE_INFO* pSignerFileInfo; + void* pSignerBlobInfo; + }; +} SIGNER_SUBJECT_INFO; + +typedef struct _SIGNER_CERT_STORE_INFO +{ + DWORD cbSize; + PCCERT_CONTEXT pSigningCert; + DWORD dwCertPolicy; + HCERTSTORE hCertStore; +} SIGNER_CERT_STORE_INFO; + +typedef struct _SIGNER_CERT +{ + DWORD cbSize; + DWORD dwCertChoice; + union + { + LPCWSTR pwszSpcFile; + SIGNER_CERT_STORE_INFO* pCertStoreInfo; + void* pSpcChainInfo; + }; + HWND hwnd; +} SIGNER_CERT; + +typedef struct _SIGNER_SIGNATURE_INFO +{ + DWORD cbSize; + ALG_ID algidHash; + DWORD dwAttrChoice; + union + { + void* pAttrAuthcode; + }; + void* psAuthenticated; + void* psUnauthenticated; +} SIGNER_SIGNATURE_INFO; + +typedef struct _SIGNER_CONTEXT +{ + DWORD cbSize; + DWORD cbBlob; + BYTE* pbBlob; +} SIGNER_CONTEXT; + +typedef struct _SIGNER_SIGN_EX2_PARAMS +{ + DWORD dwFlags; + SIGNER_SUBJECT_INFO* pSubjectInfo; + SIGNER_CERT* pSigningCert; + SIGNER_SIGNATURE_INFO* pSignatureInfo; + void* pProviderInfo; + DWORD dwTimestampFlags; + PCSTR pszAlgorithmOid; + PCWSTR pwszTimestampURL; + void* pCryptAttrs; + PVOID pSipData; + SIGNER_CONTEXT** pSignerContext; + PVOID pCryptoPolicy; + PVOID pReserved; +} SIGNER_SIGN_EX2_PARAMS; + +typedef struct _APPX_SIP_CLIENT_DATA +{ + SIGNER_SIGN_EX2_PARAMS* pSignerParams; + IUnknown* pAppxSipState; +} APPX_SIP_CLIENT_DATA; + +typedef HRESULT(WINAPI* SignerSignEx2Function)( + DWORD, + SIGNER_SUBJECT_INFO*, + SIGNER_CERT*, + SIGNER_SIGNATURE_INFO*, + void*, + DWORD, + PCSTR, + PCWSTR, + void*, + PVOID, + SIGNER_CONTEXT**, + PVOID, + PVOID); + +extern "C" __declspec(dllexport) HRESULT STDAPICALLTYPE SignAppxPackage(PCCERT_CONTEXT cert, LPCWSTR path) +{ + if (cert == nullptr || path == nullptr) + return E_INVALIDARG; + + DWORD signerIndex = 0; + + SIGNER_FILE_INFO fileInfo = {}; + fileInfo.cbSize = sizeof(fileInfo); + fileInfo.pwszFileName = path; + + SIGNER_SUBJECT_INFO subjectInfo = {}; + subjectInfo.cbSize = sizeof(subjectInfo); + subjectInfo.pdwIndex = &signerIndex; + subjectInfo.dwSubjectChoice = 0x01; + subjectInfo.pSignerFileInfo = &fileInfo; + + SIGNER_CERT_STORE_INFO certStoreInfo = {}; + certStoreInfo.cbSize = sizeof(certStoreInfo); + certStoreInfo.dwCertPolicy = 0x02; + certStoreInfo.pSigningCert = cert; + + SIGNER_CERT signerCert = {}; + signerCert.cbSize = sizeof(signerCert); + signerCert.dwCertChoice = 0x02; + signerCert.pCertStoreInfo = &certStoreInfo; + + SIGNER_SIGNATURE_INFO signatureInfo = {}; + signatureInfo.cbSize = sizeof(signatureInfo); + signatureInfo.algidHash = CALG_SHA_256; + signatureInfo.dwAttrChoice = 0; + + SIGNER_SIGN_EX2_PARAMS signerParams = {}; + signerParams.pSubjectInfo = &subjectInfo; + signerParams.pSigningCert = &signerCert; + signerParams.pSignatureInfo = &signatureInfo; + + APPX_SIP_CLIENT_DATA sipClientData = {}; + sipClientData.pSignerParams = &signerParams; + signerParams.pSipData = &sipClientData; + + HMODULE module = LoadLibraryExW(L"mssign32.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + if (module == nullptr) + return HRESULT_FROM_WIN32(GetLastError()); + + auto sign = reinterpret_cast(GetProcAddress(module, "SignerSignEx2")); + HRESULT hr = sign == nullptr ? HRESULT_FROM_WIN32(GetLastError()) : sign( + signerParams.dwFlags, + signerParams.pSubjectInfo, + signerParams.pSigningCert, + signerParams.pSignatureInfo, + signerParams.pProviderInfo, + signerParams.dwTimestampFlags, + signerParams.pszAlgorithmOid, + signerParams.pwszTimestampURL, + signerParams.pCryptAttrs, + signerParams.pSipData, + signerParams.pSignerContext, + signerParams.pCryptoPolicy, + signerParams.pReserved); + + if (sipClientData.pAppxSipState != nullptr) + sipClientData.pAppxSipState->Release(); + + FreeLibrary(module); + return hr; +} diff --git a/QuickLook.Native/QuickLook.Native32/dllmain.cpp b/QuickLook.Native/QuickLook.Native32/dllmain.cpp index 370fa77ff..a3354edfb 100644 --- a/QuickLook.Native/QuickLook.Native32/dllmain.cpp +++ b/QuickLook.Native/QuickLook.Native32/dllmain.cpp @@ -18,16 +18,12 @@ #include "stdafx.h" #include "Shell32.h" +extern HMODULE g_module; + BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { - switch (ul_reason_for_call) - { - case DLL_PROCESS_ATTACH: - case DLL_THREAD_ATTACH: - case DLL_THREAD_DETACH: - case DLL_PROCESS_DETACH: - default: - break; - } + if (ul_reason_for_call == DLL_PROCESS_ATTACH) + g_module = hModule; + return TRUE; } diff --git a/QuickLook.Native/QuickLook.Native32/resource.h b/QuickLook.Native/QuickLook.Native32/resource.h new file mode 100644 index 000000000..b4ecefc2d --- /dev/null +++ b/QuickLook.Native/QuickLook.Native32/resource.h @@ -0,0 +1,20 @@ +// Copyright © 2017-2026 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#define IDI_APP_ICON 201 diff --git a/QuickLook.Native/QuickLook.Native64/QuickLook.Native64.vcxproj b/QuickLook.Native/QuickLook.Native64/QuickLook.Native64.vcxproj index 8057d559b..5ad615110 100644 --- a/QuickLook.Native/QuickLook.Native64/QuickLook.Native64.vcxproj +++ b/QuickLook.Native/QuickLook.Native64/QuickLook.Native64.vcxproj @@ -29,6 +29,17 @@ + + NotUsing + + + NotUsing + /utf-8 %(AdditionalOptions) + + + NotUsing + + false false @@ -235,6 +246,11 @@ true + + + $(ProjectDir)..\QuickLook.Native32\QuickLook.ContextMenu.def + + diff --git a/QuickLook.Native/QuickLook.NativeArm64/QuickLook.NativeArm64.vcxproj b/QuickLook.Native/QuickLook.NativeArm64/QuickLook.NativeArm64.vcxproj index cd564747a..86e910639 100644 --- a/QuickLook.Native/QuickLook.NativeArm64/QuickLook.NativeArm64.vcxproj +++ b/QuickLook.Native/QuickLook.NativeArm64/QuickLook.NativeArm64.vcxproj @@ -21,6 +21,17 @@ + + NotUsing + + + NotUsing + /utf-8 %(AdditionalOptions) + + + NotUsing + + false false @@ -169,6 +180,11 @@ true + + + $(ProjectDir)..\QuickLook.Native32\QuickLook.ContextMenu.def + + diff --git a/QuickLook/App.xaml.cs b/QuickLook/App.xaml.cs index 838e774e7..0aad7140f 100644 --- a/QuickLook/App.xaml.cs +++ b/QuickLook/App.xaml.cs @@ -134,6 +134,7 @@ static IEnumerable FetchFiles(string rootPath, string targetFileName) protected override void OnStartup(StartupEventArgs e) { if (!EnsureOSVersion() + || !EnsureContextMenuRegistration(e.Args) || !EnsureFirstInstance(e.Args) || !EnsureFolderWritable(SettingHelper.LocalDataPath)) { @@ -254,6 +255,7 @@ protected override void OnStartup(StartupEventArgs e) CheckUpdate(); CheckAndRegisterPluginIcon(); + ContextMenuRegistration.EnsureRegistered(); } protected override void OnExit(ExitEventArgs e) @@ -281,6 +283,23 @@ private bool EnsureOSVersion() return false; } + private bool EnsureContextMenuRegistration(string[] args) + { + if (!args.Any(arg => string.Equals(arg, "/register-context-menu", StringComparison.OrdinalIgnoreCase))) + return true; + + try + { + ContextMenuRegistration.RegisterSynchronously(); + } + catch (Exception ex) + { + ProcessHelper.WriteLog(ex.ToString()); + } + + return false; + } + private bool EnsureFolderWritable(string folder) { try diff --git a/QuickLook/Helpers/ContextMenuRegistration.cs b/QuickLook/Helpers/ContextMenuRegistration.cs new file mode 100644 index 000000000..814868d49 --- /dev/null +++ b/QuickLook/Helpers/ContextMenuRegistration.cs @@ -0,0 +1,887 @@ +// Copyright © 2017-2026 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using QuickLook.Common.Helpers; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; +using System.Security; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; +using System.Text; +using System.Threading; +using Windows.ApplicationModel; +using Windows.Foundation; +using Windows.Management.Deployment; + +namespace QuickLook.Helpers; + +/// +/// Registers the Windows 11 top-level File Explorer command. +/// QuickLook.config False removes it. +/// +internal static class ContextMenuRegistration +{ + internal const string PackageName = "QL-Win.QuickLook.Shell"; + + const string CertificateSubject = "CN=QuickLook Shell"; + const string CommandClsid = "A7C3E4B1-5D28-4F6A-9B30-8E1F2A4C6D70"; + const string CodeSigningEku = "1.3.6.1.5.5.7.3.3"; + const int CertUntrustedRoot = unchecked((int)0x800B0109); + + internal static void EnsureRegistered() + { + if (!App.IsWin11 || App.IsUWP) + return; + + var thread = new Thread(() => + { + try + { + RegisterCore(false); + } + catch (Exception ex) + { + ProcessHelper.WriteLog(ex.ToString()); + } + }) + { + IsBackground = true, + Name = "QuickLook.ContextMenu", + }; + thread.SetApartmentState(ApartmentState.STA); + thread.Start(); + } + + internal static void RegisterSynchronously() + { + RegisterCore(true); + } + + static void RegisterCore(bool force) + { + if (!App.IsWin11 || App.IsUWP) + return; + + var enabled = SettingHelper.Get("ContextMenu", true); + if (!enabled) + { + RemovePackage(); + DeleteMarker(); + ProcessHelper.WriteLog("QuickLook context menu unregistered."); + return; + } + + var dllName = NativeDllName(); + var dllPath = dllName == null ? null : Path.Combine(App.AppPath, dllName); + if (dllPath == null || !File.Exists(dllPath) || !File.Exists(App.AppFullPath)) + { + if (MarkerMatchesThisInstall()) + { + RemovePackage(); + DeleteMarker(); + } + + ProcessHelper.WriteLog("QuickLook context menu binaries were not found next to QuickLook.exe."); + return; + } + + if (!ArchitectureMatches(dllPath)) + { + ProcessHelper.WriteLog("QuickLook context menu binary does not match this PC architecture."); + return; + } + + var identity = ReadIdentity(dllPath); + var marker = ReadMarker(); + if (!force && marker != null && marker.Matches(identity) && marker.FailedUntil.HasValue && marker.FailedUntil.Value > DateTime.UtcNow) + return; + + if (!force && marker != null && marker.Matches(identity) && !marker.FailedUntil.HasValue && PackageInstalled()) + return; + + X509Certificate2 certificate = null; + try + { + certificate = GetOrCreateCertificate(); + Trust(certificate, StoreName.TrustedPeople); + EnsureMachineTrust(certificate); + RemovePackage(); + + var packagePath = Path.Combine(SettingHelper.LocalDataPath, "QuickLook.Shell.msix"); + Directory.CreateDirectory(SettingHelper.LocalDataPath); + Pack(packagePath, certificate.Subject, identity); + Sign(dllPath, packagePath, certificate); + + try + { + AddPackage(packagePath); + } + catch (Exception ex) when (IsUntrusted(ex)) + { + EnsureMachineTrust(certificate); + AddPackage(packagePath); + } + + WriteMarker(identity, null); + ProcessHelper.WriteLog("QuickLook context menu registered for " + identity.AppDir); + } + catch (Exception ex) + { + WriteMarker(identity, DateTime.UtcNow.AddDays(1)); + ProcessHelper.WriteLog("QuickLook context menu registration failed: " + ex); + if (force) + throw; + } + finally + { + certificate?.Dispose(); + } + } + + static string NativeDllName() + { + if (RuntimeInformation.OSArchitecture == Architecture.Arm64) + return "QuickLook.NativeArm64.dll"; + if (RuntimeInformation.OSArchitecture == Architecture.X64) + return "QuickLook.Native64.dll"; + return null; + } + + static InstallIdentity ReadIdentity(string dllPath) + { + var dll = new FileInfo(dllPath); + var version = Assembly.GetExecutingAssembly().GetName().Version ?? new Version(1, 0, 0, 0); + return new InstallIdentity + { + AppDir = App.AppPath.TrimEnd('\\'), + DllName = dll.Name, + DllTicks = dll.LastWriteTimeUtc.Ticks, + DllLength = dll.Length, + Version = version.Major + "." + version.Minor + "." + Math.Max(version.Build, 0) + "." + Math.Max(version.Revision, 0), + Arch = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm64" : "x64", + }; + } + + static bool ArchitectureMatches(string path) + { + ushort machine = ReadPeMachine(path); + if (RuntimeInformation.OSArchitecture == Architecture.X64) + return machine == 0x8664; + if (RuntimeInformation.OSArchitecture == Architecture.Arm64) + return machine == 0xAA64; + return false; + } + + static ushort ReadPeMachine(string path) + { + using (var stream = File.OpenRead(path)) + using (var reader = new BinaryReader(stream)) + { + stream.Position = 0x3C; + var peOffset = reader.ReadInt32(); + stream.Position = peOffset; + if (reader.ReadUInt32() != 0x4550) + return 0; + return reader.ReadUInt16(); + } + } + + static void Pack(string packagePath, string publisher, InstallIdentity identity) + { + if (File.Exists(packagePath)) + File.Delete(packagePath); + + var manifest = Encoding.UTF8.GetBytes(BuildManifest(publisher, identity)); + var logos = new[] + { + new Logo("Assets\\storelogo.png", CreatePng(50, 0x1F, 0x6F, 0xEB)), + new Logo("Assets\\Square44x44Logo.png", CreatePng(44, 0x1F, 0x6F, 0xEB)), + new Logo("Assets\\Square150x150Logo.png", CreatePng(150, 0x1F, 0x6F, 0xEB)), + }; + + IStream output = null; + IAppxPackageWriter writer = null; + object factory = null; + try + { + IntPtr hashUri; + Check(CreateUri("http://www.w3.org/2001/04/xmlenc#sha256", 0x100, IntPtr.Zero, out hashUri), "CreateUri"); + try + { + var settings = new AppxPackageSettings { hashMethod = hashUri }; + // The package writer reads, writes, and seeks the output stream. + output = CreateFileStream(packagePath, 0x00001012); + factory = Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid("5842a140-ff9f-4166-8f5c-62f5b7b0c781"))); + var appx = (IAppxFactory)factory; + Check(appx.CreatePackageWriter(output, ref settings, out writer), "CreatePackageWriter"); + + foreach (var logo in logos) + { + var payload = CreateMemoryStream(logo.Bytes); + try + { + Check(writer.AddPayloadFile(logo.Name, "image/png", 1, payload), "AddPayloadFile"); + } + finally + { + Marshal.ReleaseComObject(payload); + } + } + + var manifestStream = CreateMemoryStream(manifest); + try + { + Check(writer.Close(manifestStream), "ClosePackage"); + } + finally + { + Marshal.ReleaseComObject(manifestStream); + } + } + finally + { + if (hashUri != IntPtr.Zero) + Marshal.Release(hashUri); + } + } + finally + { + if (writer != null) + Marshal.ReleaseComObject(writer); + if (output != null) + Marshal.ReleaseComObject(output); + if (factory != null) + Marshal.ReleaseComObject(factory); + } + } + + static string BuildManifest(string publisher, InstallIdentity identity) + { + var escapedPublisher = SecurityElement.Escape(publisher); + return + "" + + "" + + "" + + "QuickLookQL-WinAssets\\storelogo.pngtrue" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + } + + static void Sign(string dllPath, string packagePath, X509Certificate2 certificate) + { + var module = LoadLibrary(dllPath); + if (module == IntPtr.Zero) + throw new InvalidOperationException("Loading " + dllPath + " failed: 0x" + Marshal.GetLastWin32Error().ToString("X8")); + + try + { + var proc = GetProcAddress(module, "SignAppxPackage"); + if (proc == IntPtr.Zero) + throw new InvalidOperationException(dllPath + " does not export SignAppxPackage."); + + var sign = Marshal.GetDelegateForFunctionPointer(proc); + var hr = sign(certificate.Handle, packagePath); + if (hr < 0) + throw new InvalidOperationException("Signing the context menu package failed: 0x" + hr.ToString("X8")); + } + finally + { + FreeLibrary(module); + } + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Unicode)] + delegate int SignAppxPackageDelegate(IntPtr certContext, string packagePath); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern IntPtr LoadLibrary(string fileName); + + [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)] + static extern IntPtr GetProcAddress(IntPtr module, string procName); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool FreeLibrary(IntPtr module); + + static void Check(int hr, string step) + { + if (hr < 0) + throw new InvalidOperationException(step + " failed: 0x" + hr.ToString("X8")); + } + + static void AddPackage(string packagePath) + { + var manager = new PackageManager(); + var options = new AddPackageOptions + { + ExternalLocationUri = new Uri(App.AppPath), + ForceUpdateFromAnyVersion = true, + }; + var operation = manager.AddPackageByUriAsync(new Uri(packagePath), options); + Wait(operation); + var result = operation.GetResults(); + if (result.ExtendedErrorCode != null) + throw result.ExtendedErrorCode; + } + + static void RemovePackage() + { + try + { + var manager = new PackageManager(); + foreach (var package in manager.FindPackagesForUser(string.Empty)) + { + if (!string.Equals(package.Id.Name, PackageName, StringComparison.OrdinalIgnoreCase)) + continue; + + var operation = manager.RemovePackageAsync(package.Id.FullName); + Wait(operation); + var result = operation.GetResults(); + if (result != null && result.ExtendedErrorCode != null) + throw result.ExtendedErrorCode; + } + } + catch (Exception ex) + { + ProcessHelper.WriteLog(ex.ToString()); + } + } + + static bool PackageInstalled() + { + var manager = new PackageManager(); + return manager.FindPackagesForUser(string.Empty).Any(package => + string.Equals(package.Id.Name, PackageName, StringComparison.OrdinalIgnoreCase)); + } + + static void Wait(IAsyncInfo operation) + { + while (operation.Status == AsyncStatus.Started) + Thread.Sleep(50); + + if (operation.Status == AsyncStatus.Error) + throw operation.ErrorCode ?? new InvalidOperationException("Package deployment failed."); + if (operation.Status == AsyncStatus.Canceled) + throw new OperationCanceledException("Package deployment was canceled."); + } + + static bool IsUntrusted(Exception ex) + { + for (var current = ex; current != null; current = current.InnerException) + { + if (current.HResult == CertUntrustedRoot) + return true; + if (current.Message != null && current.Message.IndexOf("800B0109", StringComparison.OrdinalIgnoreCase) >= 0) + return true; + } + + return false; + } + + static X509Certificate2 GetOrCreateCertificate() + { + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadOnly); + foreach (var cert in store.Certificates.Cast()) + { + if (cert.HasPrivateKey + && cert.NotAfter > DateTime.UtcNow.AddDays(30) + && cert.Subject.IndexOf("QuickLook Shell", StringComparison.OrdinalIgnoreCase) >= 0 + && HasCodeSigningEku(cert)) + { + return new X509Certificate2(cert); + } + } + } + + var privateKey = CreateCom("X509Enrollment.CX509PrivateKey"); + privateKey.ProviderName = "Microsoft Software Key Storage Provider"; + privateKey.Length = 2048; + privateKey.KeySpec = 0; + privateKey.MachineContext = false; + privateKey.Create(); + + var name = CreateCom("X509Enrollment.CX500DistinguishedName"); + name.Encode(CertificateSubject, 0); + + var request = CreateCom("X509Enrollment.CX509CertificateRequestCertificate"); + request.InitializeFromPrivateKey(1, privateKey, string.Empty); + request.Subject = name; + request.Issuer = name; + request.NotBefore = DateTime.UtcNow.AddDays(-1); + request.NotAfter = DateTime.UtcNow.AddYears(10); + + var hash = CreateCom("X509Enrollment.CObjectId"); + hash.InitializeFromAlgorithmName(1, 0, 0, "SHA256"); + request.HashAlgorithm = hash; + + var ekuOid = CreateCom("X509Enrollment.CObjectId"); + ekuOid.InitializeFromValue(CodeSigningEku); + var ekuOids = CreateCom("X509Enrollment.CObjectIds"); + ekuOids.Add(ekuOid); + var eku = CreateCom("X509Enrollment.CX509ExtensionEnhancedKeyUsage"); + eku.InitializeEncode(ekuOids); + + var keyUsage = CreateCom("X509Enrollment.CX509ExtensionKeyUsage"); + keyUsage.InitializeEncode(0x80); + + request.X509Extensions.Add(keyUsage); + request.X509Extensions.Add(eku); + request.Encode(); + + var enrollment = CreateCom("X509Enrollment.CX509Enrollment"); + enrollment.InitializeFromRequest(request); + string encodedRequest = enrollment.CreateRequest(1); + enrollment.InstallResponse(2, encodedRequest, 1, string.Empty); + + var installed = new X509Certificate2(Convert.FromBase64String((string)enrollment.Certificate)); + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadOnly); + foreach (var cert in store.Certificates.Cast()) + { + if (string.Equals(cert.Thumbprint, installed.Thumbprint, StringComparison.OrdinalIgnoreCase) && cert.HasPrivateKey) + return new X509Certificate2(cert); + } + } + + throw new InvalidOperationException("The context menu signing certificate was created but has no private key."); + } + + static void EnsureMachineTrust(X509Certificate2 certificate) + { + if (IsInStore(StoreName.Root, StoreLocation.LocalMachine, certificate)) + return; + + var cerPath = Path.Combine(SettingHelper.LocalDataPath, "QuickLook.Shell.cer"); + Directory.CreateDirectory(SettingHelper.LocalDataPath); + File.WriteAllBytes(cerPath, certificate.Export(X509ContentType.Cert)); + + if (!IsElevated()) + { + var script = "$ErrorActionPreference='Stop'; Import-Certificate -FilePath '" + cerPath + "' -CertStoreLocation Cert:\\LocalMachine\\Root | Out-Null; Import-Certificate -FilePath '" + cerPath + "' -CertStoreLocation Cert:\\LocalMachine\\TrustedPeople | Out-Null"; + var start = new ProcessStartInfo + { + FileName = "powershell.exe", + Arguments = "-NoProfile -ExecutionPolicy Bypass -Command \"" + script + "\"", + UseShellExecute = true, + Verb = "runas", + }; + using (var process = Process.Start(start)) + { + if (process == null) + throw new InvalidOperationException("Could not start the elevated certificate import."); + process.WaitForExit(); + if (process.ExitCode != 0) + throw new InvalidOperationException("Elevated certificate import failed: " + process.ExitCode); + } + } + else + { + AddPublicCert(certificate, StoreName.Root, StoreLocation.LocalMachine); + AddPublicCert(certificate, StoreName.TrustedPeople, StoreLocation.LocalMachine); + } + + if (!IsInStore(StoreName.Root, StoreLocation.LocalMachine, certificate)) + throw new InvalidOperationException("The context menu certificate is not in the machine root store."); + } + + static bool IsElevated() + { + using (var identity = WindowsIdentity.GetCurrent()) + { + return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator); + } + } + + static bool IsInStore(StoreName storeName, StoreLocation location, X509Certificate2 certificate) + { + using (var store = new X509Store(storeName, location)) + { + store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly); + return store.Certificates.Cast().Any(cert => + string.Equals(cert.Thumbprint, certificate.Thumbprint, StringComparison.OrdinalIgnoreCase)); + } + } + + static void AddPublicCert(X509Certificate2 certificate, StoreName storeName, StoreLocation location) + { + var publicCert = new X509Certificate2(certificate.Export(X509ContentType.Cert)); + using (var store = new X509Store(storeName, location)) + { + store.Open(OpenFlags.ReadWrite); + if (!store.Certificates.Cast().Any(cert => + string.Equals(cert.Thumbprint, publicCert.Thumbprint, StringComparison.OrdinalIgnoreCase))) + { + store.Add(publicCert); + } + } + } + + static void Trust(X509Certificate2 certificate, StoreName storeName) + { + var publicCert = new X509Certificate2(certificate.Export(X509ContentType.Cert)); + using (var store = new X509Store(storeName, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadWrite); + var exists = store.Certificates.Cast() + .Any(cert => string.Equals(cert.Thumbprint, publicCert.Thumbprint, StringComparison.OrdinalIgnoreCase)); + if (!exists) + store.Add(publicCert); + } + } + + static bool HasCodeSigningEku(X509Certificate2 certificate) + { + foreach (var extension in certificate.Extensions) + { + var eku = extension as X509EnhancedKeyUsageExtension; + if (eku == null) + continue; + + foreach (var oid in eku.EnhancedKeyUsages) + { + if (oid.Value == CodeSigningEku) + return true; + } + } + + return false; + } + + static dynamic CreateCom(string progId) + { + var type = Type.GetTypeFromProgID(progId, true); + return Activator.CreateInstance(type); + } + + static string MarkerPath => Path.Combine(SettingHelper.LocalDataPath, "ContextMenu.registration"); + + static bool MarkerMatchesThisInstall() + { + var marker = ReadMarker(); + return marker != null && string.Equals(marker.Identity.AppDir, App.AppPath.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase); + } + + static Marker ReadMarker() + { + try + { + if (!File.Exists(MarkerPath)) + return null; + + var lines = File.ReadAllLines(MarkerPath); + if (lines.Length < 7) + return null; + + var identity = new InstallIdentity + { + AppDir = lines[1], + DllTicks = long.Parse(lines[2]), + DllLength = long.Parse(lines[3]), + Version = lines[4], + Arch = lines[5], + DllName = lines[6], + }; + DateTime? failedUntil = null; + if (lines[0] == "failed" && lines.Length >= 8) + failedUntil = new DateTime(long.Parse(lines[7]), DateTimeKind.Utc); + + return new Marker { Identity = identity, FailedUntil = failedUntil }; + } + catch (Exception ex) + { + ProcessHelper.WriteLog(ex.ToString()); + return null; + } + } + + static void WriteMarker(InstallIdentity identity, DateTime? failedUntil) + { + Directory.CreateDirectory(SettingHelper.LocalDataPath); + var lines = new List + { + failedUntil.HasValue ? "failed" : "ok", + identity.AppDir, + identity.DllTicks.ToString(), + identity.DllLength.ToString(), + identity.Version, + identity.Arch, + identity.DllName, + }; + if (failedUntil.HasValue) + lines.Add(failedUntil.Value.Ticks.ToString()); + + File.WriteAllLines(MarkerPath, lines); + } + + static void DeleteMarker() + { + try + { + if (File.Exists(MarkerPath)) + File.Delete(MarkerPath); + } + catch (Exception ex) + { + ProcessHelper.WriteLog(ex.ToString()); + } + } + + static byte[] CreatePng(int size, byte red, byte green, byte blue) + { + var rowBytes = 1 + (size * 3); + var raw = new byte[rowBytes * size]; + for (var y = 0; y < size; y++) + { + var row = y * rowBytes; + raw[row] = 0; + for (var x = 0; x < size; x++) + { + var pixel = row + 1 + (x * 3); + raw[pixel] = red; + raw[pixel + 1] = green; + raw[pixel + 2] = blue; + } + } + + using (var png = new MemoryStream()) + { + png.Write(new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 }, 0, 8); + var ihdr = new byte[13]; + WriteBigEndian(ihdr, 0, size); + WriteBigEndian(ihdr, 4, size); + ihdr[8] = 8; + ihdr[9] = 2; + WriteChunk(png, "IHDR", ihdr); + WriteChunk(png, "IDAT", Zlib(raw)); + WriteChunk(png, "IEND", Array.Empty()); + return png.ToArray(); + } + } + + static byte[] Zlib(byte[] raw) + { + using (var stream = new MemoryStream()) + { + stream.WriteByte(0x78); + stream.WriteByte(0x9C); + using (var deflate = new DeflateStream(stream, CompressionLevel.Fastest, true)) + deflate.Write(raw, 0, raw.Length); + + var adler = Adler32(raw); + stream.WriteByte((byte)(adler >> 24)); + stream.WriteByte((byte)(adler >> 16)); + stream.WriteByte((byte)(adler >> 8)); + stream.WriteByte((byte)adler); + return stream.ToArray(); + } + } + + static uint Adler32(byte[] data) + { + uint a = 1; + uint b = 0; + foreach (var value in data) + { + a = (a + value) % 65521; + b = (b + a) % 65521; + } + + return (b << 16) | a; + } + + static void WriteChunk(Stream stream, string type, byte[] data) + { + var typeBytes = Encoding.ASCII.GetBytes(type); + var crcSource = new byte[4 + data.Length]; + Buffer.BlockCopy(typeBytes, 0, crcSource, 0, 4); + if (data.Length > 0) + Buffer.BlockCopy(data, 0, crcSource, 4, data.Length); + + WriteBigEndian(stream, data.Length); + stream.Write(typeBytes, 0, 4); + if (data.Length > 0) + stream.Write(data, 0, data.Length); + WriteBigEndian(stream, unchecked((int)Crc32(crcSource))); + } + + static readonly uint[] CrcTable = BuildCrcTable(); + + static uint[] BuildCrcTable() + { + var table = new uint[256]; + for (uint n = 0; n < table.Length; n++) + { + var c = n; + for (var k = 0; k < 8; k++) + c = (c & 1) != 0 ? 0xEDB88320u ^ (c >> 1) : c >> 1; + table[n] = c; + } + + return table; + } + + static uint Crc32(byte[] data) + { + var crc = 0xFFFFFFFFu; + foreach (var value in data) + crc = CrcTable[(crc ^ value) & 0xFF] ^ (crc >> 8); + return crc ^ 0xFFFFFFFFu; + } + + static void WriteBigEndian(Stream stream, int value) + { + stream.WriteByte((byte)(value >> 24)); + stream.WriteByte((byte)(value >> 16)); + stream.WriteByte((byte)(value >> 8)); + stream.WriteByte((byte)value); + } + + static void WriteBigEndian(byte[] buffer, int offset, int value) + { + buffer[offset] = (byte)(value >> 24); + buffer[offset + 1] = (byte)(value >> 16); + buffer[offset + 2] = (byte)(value >> 8); + buffer[offset + 3] = (byte)value; + } + + static IStream CreateFileStream(string path, uint mode) + { + IStream stream; + var hr = SHCreateStreamOnFileEx(path, mode, 0, true, IntPtr.Zero, out stream); + Marshal.ThrowExceptionForHR(hr); + return stream; + } + + static IStream CreateMemoryStream(byte[] data) + { + IStream stream; + Marshal.ThrowExceptionForHR(CreateStreamOnHGlobal(IntPtr.Zero, true, out stream)); + stream.Write(data, data.Length, IntPtr.Zero); + stream.Seek(0, 0, IntPtr.Zero); + return stream; + } + + [DllImport("urlmon.dll", CharSet = CharSet.Unicode, PreserveSig = true)] + static extern int CreateUri(string uri, uint flags, IntPtr reserved, out IntPtr uriObject); + + [DllImport("shlwapi.dll", CharSet = CharSet.Unicode, PreserveSig = true)] + static extern int SHCreateStreamOnFileEx(string fileName, uint mode, uint attributes, bool create, IntPtr template, out IStream stream); + + [DllImport("ole32.dll", PreserveSig = true)] + static extern int CreateStreamOnHGlobal(IntPtr global, bool deleteOnRelease, out IStream stream); + + [StructLayout(LayoutKind.Sequential)] + struct AppxPackageSettings + { + public int forceZip32; + public IntPtr hashMethod; + } + + [ComImport] + [Guid("beb94909-e451-438b-b5a7-d79e767b75d8")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + interface IAppxFactory + { + [PreserveSig] + int CreatePackageWriter(IStream outputStream, ref AppxPackageSettings settings, out IAppxPackageWriter packageWriter); + } + + [ComImport] + [Guid("9099e33b-246f-41e4-881a-008eb613f858")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + interface IAppxPackageWriter + { + [PreserveSig] + int AddPayloadFile([MarshalAs(UnmanagedType.LPWStr)] string fileName, [MarshalAs(UnmanagedType.LPWStr)] string contentType, int compression, IStream inputStream); + + [PreserveSig] + int Close(IStream manifest); + } + + sealed class InstallIdentity + { + public string AppDir; + public string DllName; + public long DllTicks; + public long DllLength; + public string Version; + public string Arch; + + public bool Matches(InstallIdentity other) + { + return other != null + && string.Equals(AppDir, other.AppDir, StringComparison.OrdinalIgnoreCase) + && string.Equals(DllName, other.DllName, StringComparison.OrdinalIgnoreCase) + && DllTicks == other.DllTicks + && DllLength == other.DllLength + && Version == other.Version + && Arch == other.Arch; + } + } + + sealed class Marker + { + public InstallIdentity Identity; + public DateTime? FailedUntil; + + public bool Matches(InstallIdentity identity) + { + return Identity != null && Identity.Matches(identity); + } + } + + sealed class Logo + { + public Logo(string name, byte[] bytes) + { + Name = name; + Bytes = bytes; + } + + public string Name { get; } + public byte[] Bytes { get; } + } +}