// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include #include #include #include #include #include #define PEMAGIC ((WORD)'P' + ((WORD)'E' << 8)) static CONSOLE_STATE_INFO g_csi; using namespace Microsoft::WRL; // This class exposes console property sheets for use when launching the filesystem shortcut properties dialog. // clang-format off [uuid(D2942F8E-478E-41D3-870A-35A16238F4EE)] class ConsolePropertySheetHandler WrlFinal : public RuntimeClass, IShellExtInit, IShellPropSheetExt, IPersist, FtmBase> // clang-format on { public: HRESULT RuntimeClassInitialize() { return S_OK; } // IPersist STDMETHODIMP GetClassID(_Out_ CLSID * clsid) override { *clsid = __uuidof(this); return S_OK; } // IShellExtInit // Shell QI's for IShellExtInit and calls Initialize first. If we return a succeeding HRESULT, the shell will QI for // IShellPropSheetExt and call AddPages. A failing HRESULT causes the shell to skip us. STDMETHODIMP Initialize(_In_ PCIDLIST_ABSOLUTE /*pidlFolder*/, _In_ IDataObject * pdtobj, _In_ HKEY /*hkeyProgID*/) { WCHAR szLinkFileName[MAX_PATH]; HRESULT hr = _ShouldAddPropertySheet(pdtobj, szLinkFileName, ARRAYSIZE(szLinkFileName)); if (SUCCEEDED(hr)) { hr = InitializeConsoleState() ? S_OK : E_FAIL; if (SUCCEEDED(hr)) { hr = _InitializeGlobalStateInfo(szLinkFileName); } } return hr; } // IShellPropSheetExt STDMETHODIMP AddPages(_In_ LPFNADDPROPSHEETPAGE pfnAddPage, _In_ LPARAM lParam) { PROPSHEETPAGE psp[NUMBER_OF_PAGES] = {}; HRESULT hr = PopulatePropSheetPageArray(psp, ARRAYSIZE(psp), TRUE /*fRegisterCallbacks*/) ? S_OK : E_FAIL; if (SUCCEEDED(hr)) { for (UINT ipsp = 0; ipsp < ARRAYSIZE(psp) && SUCCEEDED(hr); ipsp++) { HPROPSHEETPAGE hPage = CreatePropertySheetPage(&psp[ipsp]); hr = (hPage == nullptr) ? E_FAIL : S_OK; if (SUCCEEDED(hr)) { pfnAddPage(hPage, lParam); } } } return hr; } STDMETHODIMP ReplacePage(_In_ UINT /*uPageID*/, _In_ LPFNADDPROPSHEETPAGE /*pfnReplacePage*/, _In_ LPARAM /*lParam*/) { // Implementation not needed -- MSDN says "Replaces a page in a property sheet for a Control Panel object.", // which we don't need to do. return E_NOTIMPL; } private: ~ConsolePropertySheetHandler() = default; HRESULT _InitializeGlobalStateInfo(_In_ PCWSTR pszLinkFileName) { g_fHostedInFileProperties = TRUE; gpStateInfo = &g_csi; // Initialize the fIsV2Console with whatever the current v2 setting is // in the registry. Usually this is set by conhost, but in this path, // we're being launched straight from explorer. See GH#2319, GH#2651 gpStateInfo->fIsV2Console = GetConsoleBoolValue(CONSOLE_REGISTRY_FORCEV2, TRUE); InitRegistryValues(gpStateInfo); gpStateInfo->Defaults = TRUE; GetRegistryValues(gpStateInfo); PWSTR pszAllocatedFileName; HRESULT hr = SHStrDup(pszLinkFileName, &pszAllocatedFileName); if (SUCCEEDED(hr)) { hr = StringCchCopyW(pszAllocatedFileName, MAX_PATH, pszLinkFileName); if (SUCCEEDED(hr)) { // gpStateInfo now owns lifetime of the allocated filename gpStateInfo->LinkTitle = pszAllocatedFileName; pszAllocatedFileName = nullptr; // Not all console shortcuts have console-specific properties. We just take the registry defaults in // those cases. BOOL readSettings = FALSE; NTSTATUS s = ShortcutSerialization::s_GetLinkValues(gpStateInfo, &readSettings, nullptr, 0, nullptr, 0, nullptr, 0, nullptr, nullptr, nullptr); hr = HRESULT_FROM_NT(s); } else { CoTaskMemFree(pszAllocatedFileName); } } if (SUCCEEDED(hr)) { InitializeFonts(); hr = FindFontAndUpdateState(); } return hr; } /////////////////////////////////////////////////////////////////////////// // CODE FROM THE SHELL DEPOT'S `idllib.h` // get a link target item without resolving it. HRESULT GetTargetIdList(_In_ IShellItem * psiLink, _COM_Outptr_ PIDLIST_ABSOLUTE * ppidl) { *ppidl = nullptr; IShellLink* psl; HRESULT hr = psiLink->BindToHandler(nullptr, BHID_SFUIObject, IID_PPV_ARGS(&psl)); if (SUCCEEDED(hr)) { hr = psl->GetIDList(ppidl); if (SUCCEEDED(hr) && (*ppidl == nullptr)) { hr = E_FAIL; } psl->Release(); } return hr; } HRESULT GetTargetItem(_In_ IShellItem * psiLink, _In_ REFIID riid, _COM_Outptr_ void** ppv) { *ppv = nullptr; PIDLIST_ABSOLUTE pidl; HRESULT hr = GetTargetIdList(psiLink, &pidl); if (SUCCEEDED(hr)) { hr = SHCreateItemFromIDList(pidl, riid, ppv); ILFree(pidl); } return hr; } /////////////////////////////////////////////////////////////////////////// HRESULT _GetShellItemLinkTargetExpanded(_In_ IShellItem * pShellItem, _Out_writes_(cchFilePathExtended) PWSTR pszFilePathExtended, const size_t cchFilePathExtended) { ComPtr shellItemLinkTarget; HRESULT hr = GetTargetItem(pShellItem, IID_PPV_ARGS(&shellItemLinkTarget)); if (SUCCEEDED(hr)) { wil::unique_cotaskmem_string linkTargetPath; hr = shellItemLinkTarget->GetDisplayName(SIGDN_FILESYSPATH, &linkTargetPath); if (SUCCEEDED(hr)) { hr = StringCchCopy(pszFilePathExtended, cchFilePathExtended, linkTargetPath.get()); } } return hr; } HRESULT _ShouldAddPropertySheet(_In_ IDataObject * pdtobj, _Out_writes_(cchLinkFileName) PWSTR pszLinkFileName, const size_t cchLinkFileName) { ComPtr shellItemArray; HRESULT hr = SHCreateShellItemArrayFromDataObject(pdtobj, IID_PPV_ARGS(&shellItemArray)); if (SUCCEEDED(hr)) { DWORD dwItemCount; hr = shellItemArray->GetCount(&dwItemCount); if (SUCCEEDED(hr)) { // only consider being available for selections of a single file hr = dwItemCount == 1 ? S_OK : E_FAIL; if (SUCCEEDED(hr)) { ComPtr shellItem; hr = shellItemArray->GetItemAt(0, &shellItem); if (SUCCEEDED(hr)) { // First expensive portion of this method -- reads .lnk file WCHAR szFileExpanded[MAX_PATH]; hr = _GetShellItemLinkTargetExpanded(shellItem.Get(), szFileExpanded, ARRAYSIZE(szFileExpanded)); if (SUCCEEDED(hr)) { // Second expensive portion of this method -- cracks the PE header of the .lnk file target // if it's an executable SHFILEINFO sfi = { 0 }; DWORD_PTR dwFileType = SHGetFileInfo(szFileExpanded, 0 /*dwFileAttributes*/, &sfi, sizeof(sfi), SHGFI_EXETYPE); if (HIWORD(dwFileType) == 0 && LOWORD(dwFileType) == PEMAGIC) { // link target is a console application -- we should show our UI hr = S_OK; } else { // link target is not a console application -- we should not show our UI hr = E_FAIL; } } } if (hr == S_OK) { // We're going to show the UI, write out the link filename while we're here. This is needed // so we know where changes should be written. wil::unique_cotaskmem_string linkDisplayName; hr = shellItem->GetDisplayName(SIGDN_FILESYSPATH, &linkDisplayName); if (SUCCEEDED(hr)) { hr = StringCchCopy(pszLinkFileName, cchLinkFileName, linkDisplayName.get()); } } } } } return hr; } }; CoCreatableClass(ConsolePropertySheetHandler);