1 /*
2  * PROJECT:     shlextdbg
3  * LICENSE:     GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+)
4  * PURPOSE:     Shell extension debug utility
5  * COPYRIGHT:   Copyright 2017 Mark Jansen (mark.jansen@reactos.org)
6  */
7 
8 #include <windows.h>
9 #include <shlobj.h>
10 #include <atlbase.h>   // thanks gcc
11 #include <atlcom.h>    // thanks gcc
12 #include <atlstr.h>
13 #include <atlsimpcoll.h>
14 #include <conio.h>
15  // hack for gcc:
16 #define DbgPrint(...) printf(__VA_ARGS__)
17 #include <shellutils.h>
18 
19 enum WaitType
20 {
21     Wait_None,
22     Wait_Infinite,
23     Wait_OpenWindows,
24     Wait_Input,
25 };
26 
27 CLSID g_CLSID = { 0 };
28 CStringW g_DLL;
29 CStringW g_ShellExtInit;
30 bool g_bIShellPropSheetExt = false;
31 CStringA g_ContextMenu;
32 WaitType g_Wait = Wait_None;
33 
34 HRESULT CreateIDataObject(CComHeapPtr<ITEMIDLIST>& pidl, CComPtr<IDataObject>& dataObject, PCWSTR FileName)
35 {
36     HRESULT hr = SHParseDisplayName(FileName, NULL, &pidl, 0, NULL);
37     if (!SUCCEEDED(hr))
38     {
39         wprintf(L"Failed to create pidl from '%s': 0x%x\n", FileName, hr);
40         return hr;
41     }
42 
43     CComPtr<IShellFolder> shellFolder;
44     PCUITEMID_CHILD childs;
45     hr = SHBindToParent(pidl, IID_PPV_ARG(IShellFolder, &shellFolder), &childs);
46     if (!SUCCEEDED(hr))
47     {
48         wprintf(L"Failed to bind to parent: 0x%x\n", hr);
49         return hr;
50     }
51     hr = shellFolder->GetUIObjectOf(NULL, 1, &childs, IID_IDataObject, NULL, (PVOID*)&dataObject);
52     if (!SUCCEEDED(hr))
53     {
54         wprintf(L"Failed to query IDataObject: 0x%x\n", hr);
55     }
56     return hr;
57 }
58 
59 HRESULT LoadAndInitialize(REFIID riid, LPVOID* ppv)
60 {
61     CComPtr<IShellExtInit> spShellExtInit;
62     HRESULT hr;
63     if (g_DLL.IsEmpty())
64     {
65         hr = CoCreateInstance(g_CLSID, NULL, CLSCTX_ALL, IID_PPV_ARG(IShellExtInit, &spShellExtInit));
66         if (!SUCCEEDED(hr))
67         {
68             WCHAR Buffer[100];
69             StringFromGUID2(g_CLSID, Buffer, _countof(Buffer));
70             wprintf(L"Failed to Create %s:IShellExtInit: 0x%x\n", Buffer, hr);
71             return hr;
72         }
73     }
74     else
75     {
76         typedef HRESULT (STDAPICALLTYPE *tDllGetClassObject)(REFCLSID rclsid, REFIID riid, LPVOID *ppv);
77         HMODULE mod = LoadLibraryW(g_DLL);
78         if (!mod)
79         {
80             wprintf(L"Failed to Load %s:(0x%x)\n", g_DLL.GetString(), GetLastError());
81             return E_FAIL;
82         }
83         tDllGetClassObject DllGet = (tDllGetClassObject)GetProcAddress(mod, "DllGetClassObject");
84         if (!DllGet)
85         {
86             wprintf(L"%s does not export DllGetClassObject\n", g_DLL.GetString());
87             return E_FAIL;
88         }
89         CComPtr<IClassFactory> spClassFactory;
90         hr = DllGet(g_CLSID, IID_PPV_ARG(IClassFactory, &spClassFactory));
91         if (!SUCCEEDED(hr))
92         {
93             wprintf(L"Failed to create IClassFactory: 0x%x\n", hr);
94             return hr;
95         }
96         hr = spClassFactory->CreateInstance(NULL, IID_PPV_ARG(IShellExtInit, &spShellExtInit));
97         if (!SUCCEEDED(hr))
98         {
99             wprintf(L"Failed to Request IShellExtInit from IClassFactory: 0x%x\n", hr);
100             return hr;
101         }
102     }
103 
104     CComPtr<IDataObject> spDataObject;
105     CComHeapPtr<ITEMIDLIST> pidl;
106     hr = CreateIDataObject(pidl, spDataObject, g_ShellExtInit.GetString());
107     if (!SUCCEEDED(hr))
108         return hr;
109 
110     hr = spShellExtInit->Initialize(pidl, spDataObject, NULL);
111     if (!SUCCEEDED(hr))
112     {
113         wprintf(L"IShellExtInit->Initialize failed: 0x%x\n", hr);
114         return hr;
115     }
116     hr = spShellExtInit->QueryInterface(riid, ppv);
117     if (!SUCCEEDED(hr))
118     {
119         WCHAR Buffer[100];
120         StringFromGUID2(riid, Buffer, _countof(Buffer));
121         wprintf(L"Failed to query %s from IShellExtInit: 0x%x\n", Buffer, hr);
122     }
123     return hr;
124 }
125 
126 
127 CSimpleArray<HWND> g_Windows;
128 HWND g_ConsoleWindow;
129 BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
130 {
131     if (hwnd != g_ConsoleWindow)
132     {
133         DWORD pid = 0;
134         GetWindowThreadProcessId(hwnd, &pid);
135         if (pid == GetCurrentProcessId())
136         {
137             g_Windows.Add(hwnd);
138         }
139     }
140     return TRUE;
141 }
142 
143 void WaitWindows()
144 {
145     /* Give the windows some time to spawn */
146     Sleep(2000);
147     g_ConsoleWindow = GetConsoleWindow();
148     while (true)
149     {
150         g_Windows.RemoveAll();
151         EnumWindows(EnumWindowsProc, NULL);
152         if (g_Windows.GetSize() == 0)
153             break;
154         Sleep(500);
155     }
156     wprintf(L"All windows closed (ignoring console window)\n");
157 }
158 
159 
160 
161 CSimpleArray<HPROPSHEETPAGE> g_Pages;
162 static BOOL CALLBACK cb_AddPage(HPROPSHEETPAGE page, LPARAM lParam)
163 {
164     g_Pages.Add(page);
165     if (lParam != (LPARAM)&g_Pages)
166     {
167         wprintf(L"Propsheet failed to pass lParam, got: 0x%x\n", lParam);
168     }
169     return TRUE;
170 }
171 
172 static bool isCmdWithArg(int argc, WCHAR** argv, int& n, PCWSTR check, PCWSTR &arg)
173 {
174     arg = NULL;
175     size_t len = wcslen(check);
176     if (!_wcsnicmp(argv[n] + 1, check, len))
177     {
178         PCWSTR cmd = argv[n] + len + 1;
179         if (*cmd == ':' || *cmd == '=')
180         {
181             arg = cmd + 1;
182             return true;
183         }
184         if (n + 1 < argc)
185         {
186             arg = argv[n+1];
187             n++;
188             return true;
189         }
190         wprintf(L"Command %s has no required argument!\n", check);
191         return false;
192     }
193     return false;
194 }
195 
196 static bool isCmd(int argc, WCHAR** argv, int n, PCWSTR check)
197 {
198     return !wcsicmp(argv[n] + 1, check);
199 }
200 
201 static void PrintHelp(PCWSTR ExtraLine)
202 {
203     if (ExtraLine)
204         wprintf(L"%s\n", ExtraLine);
205 
206     wprintf(L"shlextdbg /clsid={clsid} [/dll=dllname] /IShellExtInit=filename |shlextype| |waitoptions|\n");
207     wprintf(L"    {clsid}: The CLSID or ProgID of the object to create\n");
208     wprintf(L"    dll: Optional dllname to create the object from, instead of CoCreateInstance\n");
209     wprintf(L"    filename: The filename to pass to IShellExtInit->Initialze\n");
210     wprintf(L"    shlextype: The type of shell extention to run:\n");
211     wprintf(L"               /IShellPropSheetExt to create a property sheet\n");
212     wprintf(L"               /IContextMenu=verb to activate the specified verb\n");
213     wprintf(L"    waitoptions: Specify how to wait:\n");
214     wprintf(L"                 /infinite: Keep on waiting infinitely\n");
215     wprintf(L"                 /openwindows: Wait for all windows from the current application to close\n");
216     wprintf(L"                 /input: Wait for input\n");
217     wprintf(L"\n");
218 }
219 
220 /*
221 Examples:
222 
223 /clsid={513D916F-2A8E-4F51-AEAB-0CBC76FB1AF8} /IShellExtInit=C:\RosBE\Uninstall.exe /IShellPropSheetExt
224 /clsid=CompressedFolder /IShellExtInit=e:\test.zip /IContextMenu=extract /openwindows
225 /clsid=CompressedFolder /IShellExtInit=e:\test.zip /IContextMenu=extract /openwindows /dll=R:\build\dev\devenv\dll\shellext\zipfldr\Debug\zipfldr.dll
226 
227 */
228 extern "C"  // and another hack for gcc
229 int wmain(int argc, WCHAR **argv)
230 {
231     bool failArgs = false;
232     for (int n = 1; n < argc; ++n)
233     {
234         WCHAR* cmd = argv[n];
235         if (cmd[0] == '-' || cmd[0] == '/')
236         {
237             PCWSTR arg;
238             if (isCmdWithArg(argc, argv, n, L"clsid", arg))
239             {
240                 HRESULT hr = CLSIDFromString(arg, &g_CLSID);
241                 if (!SUCCEEDED(hr))
242                 {
243                     wprintf(L"Failed to convert %s to CLSID\n", arg);
244                     failArgs = true;
245                 }
246             }
247             else if (isCmdWithArg(argc, argv, n, L"dll", arg))
248             {
249                 g_DLL = arg;
250             }
251             else if (isCmdWithArg(argc, argv, n, L"IShellExtInit", arg))
252             {
253                 g_ShellExtInit = arg;
254             }
255             else if (isCmd(argc, argv, n, L"IShellPropSheetExt"))
256             {
257                 g_bIShellPropSheetExt = true;
258             }
259             else if (isCmdWithArg(argc, argv, n, L"IContextMenu", arg))
260             {
261                 g_ContextMenu = arg;
262             }
263             else if (isCmd(argc, argv, n, L"infinite"))
264             {
265                 g_Wait = Wait_Infinite;
266             }
267             else if (isCmd(argc, argv, n, L"openwindows"))
268             {
269                 g_Wait = Wait_OpenWindows;
270             }
271             else if (isCmd(argc, argv, n, L"input"))
272             {
273                 g_Wait = Wait_Input;
274             }
275             else
276             {
277                 wprintf(L"Unknown argument: %s\n", cmd);
278                 failArgs = true;
279             }
280         }
281     }
282 
283     if (failArgs)
284     {
285         PrintHelp(NULL);
286         return E_INVALIDARG;
287     }
288 
289 
290     CLSID EmptyCLSID = { 0 };
291     if (EmptyCLSID == g_CLSID)
292     {
293         PrintHelp(L"No CLSID specified");
294         return E_INVALIDARG;
295     }
296 
297     if (g_ShellExtInit.IsEmpty())
298     {
299         PrintHelp(L"No filename specified");
300         return E_INVALIDARG;
301     }
302 
303     INITCOMMONCONTROLSEX icc = { sizeof(icc), ICC_LINK_CLASS | ICC_STANDARD_CLASSES };
304     InitCommonControlsEx(&icc);
305     CoInitialize(NULL);
306 
307     HRESULT hr;
308     if (g_bIShellPropSheetExt)
309     {
310         CComPtr<IShellPropSheetExt> spSheetExt;
311         hr = LoadAndInitialize(IID_PPV_ARG(IShellPropSheetExt, &spSheetExt));
312         if (!SUCCEEDED(hr))
313             return hr;
314 
315         hr = spSheetExt->AddPages(cb_AddPage, (LPARAM)&g_Pages);
316         if (!SUCCEEDED(hr))
317         {
318             wprintf(L"IShellPropSheetExt->AddPages failed: 0x%x\n", hr);
319             return hr;
320         }
321 
322         USHORT ActivePage = HRESULT_CODE(hr);
323         PROPSHEETHEADERW psh = { 0 };
324 
325         psh.dwSize = sizeof(psh);
326         psh.dwFlags = PSH_PROPTITLE;
327         psh.pszCaption = L"shlextdbg";
328         psh.phpage = g_Pages.GetData();
329         psh.nPages = g_Pages.GetSize();
330         psh.nStartPage = ActivePage ? (ActivePage-1) : 0;
331         hr = PropertySheetW(&psh);
332 
333         wprintf(L"PropertySheetW returned: 0x%x\n", hr);
334     }
335     if (!g_ContextMenu.IsEmpty())
336     {
337         CComPtr<IContextMenu> spContextMenu;
338         hr = LoadAndInitialize(IID_PPV_ARG(IContextMenu, &spContextMenu));
339         if (!SUCCEEDED(hr))
340             return hr;
341 
342         CMINVOKECOMMANDINFO cm = { sizeof(cm), 0 };
343         cm.lpVerb = g_ContextMenu.GetString();
344         cm.nShow = SW_SHOW;
345         hr = spContextMenu->InvokeCommand(&cm);
346 
347         if (!SUCCEEDED(hr))
348         {
349             wprintf(L"IContextMenu->InvokeCommand failed: 0x%x\n", hr);
350             return hr;
351         }
352         wprintf(L"IContextMenu->InvokeCommand returned: 0x%x\n", hr);
353     }
354 
355     switch (g_Wait)
356     {
357     case Wait_None:
358         break;
359     case Wait_Infinite:
360         while (true) {
361             Sleep(1000);
362         }
363         break;
364     case Wait_OpenWindows:
365         WaitWindows();
366         break;
367     case Wait_Input:
368         wprintf(L"Press any key to continue...\n");
369         _getch();
370         break;
371 
372     }
373     return 0;
374 }
375