terminal/src/propsheet/PropSheetHandler.cpp
Malcolm Smith 601286ac69
Find icon from shortcut target if shortcut doesn't specify it (#6277)
Implements what I was suggesting in #6266 where if a shortcut doesn't
specify an icon, the shortcut target full path is used before searching
for a matching executable in the path.

## References

Found due to not getting the right icon in conhost from the Yori
installer.  It's fixed in the installer from
5af366b6a5
for all current users of conhost though, so this PR is just trying to
minimize surprises for the next guy.

## Detailed Description of the Pull Request / Additional comments

I know conhost and shortcut settings aren't really the team's focus
which is why I'm doing this.  I understand though if there's a better
way or there are factors that I hadn't considered.  Note that the path
searching code is used when programs are launched without using a
shortcut, and it will match if the working directory of the shortcut is
the directory containing the executable.

## Validation Steps Performed

Created a shortcut that didn't specify an icon to a binary that wasn't
in the path, and verified that the icon in the upper left of the console
window could resolve correctly when opening the shortcut.  I'm not aware
of a way to get into this path (of launching via a shortcut to a command
line process) without replacing the system conhost, which is what I did
to verify it.  In order to diagnose it, I used hardcoded DebugBreak()
since even ImageFileExecutionOptions didn't like running against conhost-
is there are better way to debug and test these cases without being so
invasive on the system?

Closes #6266
2020-06-01 17:19:05 +00:00

260 lines
9.9 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include <wrl\implements.h>
#include <wrl\module.h>
#include <wil\resource.h>
#include <shlwapi.h>
#include <shellapi.h>
#include <shlguid.h>
#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<RuntimeClassFlags<ClassicCom>,
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<IShellItem> 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<IShellItemArray> 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<IShellItem> 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);