1 /*
2  * PROJECT:     shell32
3  * LICENSE:     LGPL-2.1-or-later (https://spdx.org/licenses/LGPL-2.1-or-later)
4  * PURPOSE:     Shell change notification
5  * COPYRIGHT:   Copyright 2020 Katayama Hirofumi MZ (katayama.hirofumi.mz@gmail.com)
6  */
7 #include "shelldesktop.h"
8 #include "shlwapi_undoc.h"
9 #include "CDirectoryWatcher.h"
10 #include <assert.h>      // for assert
11 
12 WINE_DEFAULT_DEBUG_CHANNEL(shcn);
13 
14 // TODO: SHCNRF_RecursiveInterrupt
15 
16 //////////////////////////////////////////////////////////////////////////////
17 
18 // notification target item
19 struct ITEM
20 {
21     UINT nRegID;        // The registration ID.
22     DWORD dwUserPID;    // The user PID; that is the process ID of the target window.
23     HANDLE hRegEntry;   // The registration entry.
24     HWND hwndBroker;    // Client broker window (if any).
25     CDirectoryWatcher *pDirWatch; // for filesystem notification
26 };
27 
28 typedef CWinTraits <
29     WS_POPUP | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
30     WS_EX_TOOLWINDOW
31 > CChangeNotifyServerTraits;
32 
33 //////////////////////////////////////////////////////////////////////////////
34 // CChangeNotifyServer
35 //
36 // CChangeNotifyServer implements a window that handles all shell change notifications.
37 // It runs in the context of explorer and specifically in the thread of the shell desktop.
38 // Shell change notification api exported from shell32 forwards all their calls
39 // to this window where all processing takes place.
40 
41 class CChangeNotifyServer :
42     public CWindowImpl<CChangeNotifyServer, CWindow, CChangeNotifyServerTraits>,
43     public CComObjectRootEx<CComMultiThreadModelNoCS>,
44     public IOleWindow
45 {
46 public:
47     CChangeNotifyServer();
48     virtual ~CChangeNotifyServer();
49     HRESULT Initialize();
50 
51     // *** IOleWindow methods ***
52     virtual HRESULT STDMETHODCALLTYPE GetWindow(HWND *lphwnd);
53     virtual HRESULT STDMETHODCALLTYPE ContextSensitiveHelp(BOOL fEnterMode);
54 
55     // Message handlers
56     LRESULT OnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
57     LRESULT OnUnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
58     LRESULT OnDeliverNotification(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
59     LRESULT OnSuspendResume(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
60     LRESULT OnRemoveByPID(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
61     LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
62 
63     DECLARE_NOT_AGGREGATABLE(CChangeNotifyServer)
64 
65     DECLARE_PROTECT_FINAL_CONSTRUCT()
66     BEGIN_COM_MAP(CChangeNotifyServer)
67         COM_INTERFACE_ENTRY_IID(IID_IOleWindow, IOleWindow)
68     END_COM_MAP()
69 
70     DECLARE_WND_CLASS_EX(L"WorkerW", 0, 0)
71 
72     BEGIN_MSG_MAP(CChangeNotifyServer)
73         MESSAGE_HANDLER(CN_REGISTER, OnRegister)
74         MESSAGE_HANDLER(CN_UNREGISTER, OnUnRegister)
75         MESSAGE_HANDLER(CN_DELIVER_NOTIFICATION, OnDeliverNotification)
76         MESSAGE_HANDLER(CN_SUSPEND_RESUME, OnSuspendResume)
77         MESSAGE_HANDLER(CN_UNREGISTER_PROCESS, OnRemoveByPID);
78         MESSAGE_HANDLER(WM_DESTROY, OnDestroy);
79     END_MSG_MAP()
80 
81 private:
82     UINT m_nNextRegID;
83     CSimpleArray<ITEM> m_items;
84 
85     BOOL AddItem(UINT nRegID, DWORD dwUserPID, HANDLE hRegEntry, HWND hwndBroker,
86                  CDirectoryWatcher *pDirWatch);
87     BOOL RemoveItemsByRegID(UINT nRegID, DWORD dwOwnerPID);
88     void RemoveItemsByProcess(DWORD dwOwnerPID, DWORD dwUserPID);
89     void DestroyItem(ITEM& item, DWORD dwOwnerPID, HWND *phwndBroker);
90 
91     UINT GetNextRegID();
92     BOOL DeliverNotification(HANDLE hTicket, DWORD dwOwnerPID);
93     BOOL ShouldNotify(LPDELITICKET pTicket, LPREGENTRY pRegEntry);
94 };
95 
96 CChangeNotifyServer::CChangeNotifyServer()
97     : m_nNextRegID(INVALID_REG_ID)
98 {
99 }
100 
101 CChangeNotifyServer::~CChangeNotifyServer()
102 {
103 }
104 
105 BOOL CChangeNotifyServer::AddItem(UINT nRegID, DWORD dwUserPID, HANDLE hRegEntry,
106                                   HWND hwndBroker, CDirectoryWatcher *pDirWatch)
107 {
108     // find the empty room
109     for (INT i = 0; i < m_items.GetSize(); ++i)
110     {
111         if (m_items[i].nRegID == INVALID_REG_ID)
112         {
113             // found the room, populate it
114             m_items[i].nRegID = nRegID;
115             m_items[i].dwUserPID = dwUserPID;
116             m_items[i].hRegEntry = hRegEntry;
117             m_items[i].hwndBroker = hwndBroker;
118             m_items[i].pDirWatch = pDirWatch;
119             return TRUE;
120         }
121     }
122 
123     // no empty room found
124     ITEM item = { nRegID, dwUserPID, hRegEntry, hwndBroker, pDirWatch };
125     m_items.Add(item);
126     return TRUE;
127 }
128 
129 void CChangeNotifyServer::DestroyItem(ITEM& item, DWORD dwOwnerPID, HWND *phwndBroker)
130 {
131     // destroy broker if any and if first time
132     HWND hwndBroker = item.hwndBroker;
133     item.hwndBroker = NULL;
134     if (hwndBroker && hwndBroker != *phwndBroker)
135     {
136         ::DestroyWindow(hwndBroker);
137         *phwndBroker = hwndBroker;
138     }
139 
140     // request termination of pDirWatch if any
141     CDirectoryWatcher *pDirWatch = item.pDirWatch;
142     item.pDirWatch = NULL;
143     if (pDirWatch)
144         pDirWatch->RequestTermination();
145 
146     // free
147     SHFreeShared(item.hRegEntry, dwOwnerPID);
148     item.nRegID = INVALID_REG_ID;
149     item.dwUserPID = 0;
150     item.hRegEntry = NULL;
151     item.hwndBroker = NULL;
152     item.pDirWatch = NULL;
153 }
154 
155 BOOL CChangeNotifyServer::RemoveItemsByRegID(UINT nRegID, DWORD dwOwnerPID)
156 {
157     BOOL bFound = FALSE;
158     HWND hwndBroker = NULL;
159     assert(nRegID != INVALID_REG_ID);
160     for (INT i = 0; i < m_items.GetSize(); ++i)
161     {
162         if (m_items[i].nRegID == nRegID)
163         {
164             bFound = TRUE;
165             DestroyItem(m_items[i], dwOwnerPID, &hwndBroker);
166         }
167     }
168     return bFound;
169 }
170 
171 void CChangeNotifyServer::RemoveItemsByProcess(DWORD dwOwnerPID, DWORD dwUserPID)
172 {
173     HWND hwndBroker = NULL;
174     assert(dwUserPID != 0);
175     for (INT i = 0; i < m_items.GetSize(); ++i)
176     {
177         if (m_items[i].dwUserPID == dwUserPID)
178         {
179             DestroyItem(m_items[i], dwOwnerPID, &hwndBroker);
180         }
181     }
182 }
183 
184 // create a CDirectoryWatcher from a REGENTRY
185 static CDirectoryWatcher *
186 CreateDirectoryWatcherFromRegEntry(LPREGENTRY pRegEntry)
187 {
188     if (pRegEntry->ibPidl == 0)
189         return NULL;
190 
191     // it must be interrupt level if pRegEntry is a filesystem watch
192     if (!(pRegEntry->fSources & SHCNRF_InterruptLevel))
193         return NULL;
194 
195     // get the path
196     WCHAR szPath[MAX_PATH];
197     LPITEMIDLIST pidl = (LPITEMIDLIST)((LPBYTE)pRegEntry + pRegEntry->ibPidl);
198     if (!SHGetPathFromIDListW(pidl, szPath) || !PathIsDirectoryW(szPath))
199         return NULL;
200 
201     // create a CDirectoryWatcher
202     CDirectoryWatcher *pDirectoryWatcher = CDirectoryWatcher::Create(szPath, pRegEntry->fRecursive);
203     if (pDirectoryWatcher == NULL)
204         return NULL;
205 
206     return pDirectoryWatcher;
207 }
208 
209 // Message CN_REGISTER: Register the registration entry.
210 //   wParam: The handle of registration entry.
211 //   lParam: The owner PID of registration entry.
212 //   return: TRUE if successful.
213 LRESULT CChangeNotifyServer::OnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
214 {
215     TRACE("OnRegister(%p, %u, %p, %p)\n", m_hWnd, uMsg, wParam, lParam);
216 
217     // lock the registration entry
218     HANDLE hRegEntry = (HANDLE)wParam;
219     DWORD dwOwnerPID = (DWORD)lParam;
220     LPREGENTRY pRegEntry = (LPREGENTRY)SHLockSharedEx(hRegEntry, dwOwnerPID, TRUE);
221     if (pRegEntry == NULL || pRegEntry->dwMagic != REGENTRY_MAGIC)
222     {
223         ERR("pRegEntry is invalid\n");
224         SHUnlockShared(pRegEntry);
225         return FALSE;
226     }
227 
228     // update registration ID if necessary
229     if (pRegEntry->nRegID == INVALID_REG_ID)
230         pRegEntry->nRegID = GetNextRegID();
231 
232     TRACE("pRegEntry->nRegID: %u\n", pRegEntry->nRegID);
233 
234     // get the user PID; that is the process ID of the target window
235     DWORD dwUserPID;
236     GetWindowThreadProcessId(pRegEntry->hwnd, &dwUserPID);
237 
238     // get broker if any
239     HWND hwndBroker = pRegEntry->hwndBroker;
240 
241     // clone the registration entry
242     HANDLE hNewEntry = SHAllocShared(pRegEntry, pRegEntry->cbSize, dwOwnerPID);
243     if (hNewEntry == NULL)
244     {
245         ERR("Out of memory\n");
246         pRegEntry->nRegID = INVALID_REG_ID;
247         SHUnlockShared(pRegEntry);
248         return FALSE;
249     }
250 
251     // create a directory watch if necessary
252     CDirectoryWatcher *pDirWatch = CreateDirectoryWatcherFromRegEntry(pRegEntry);
253     if (pDirWatch && !pDirWatch->RequestAddWatcher())
254     {
255         pRegEntry->nRegID = INVALID_REG_ID;
256         SHUnlockShared(pRegEntry);
257         delete pDirWatch;
258         return FALSE;
259     }
260 
261     // unlock the registry entry
262     SHUnlockShared(pRegEntry);
263 
264     // add an ITEM
265     return AddItem(m_nNextRegID, dwUserPID, hNewEntry, hwndBroker, pDirWatch);
266 }
267 
268 // Message CN_UNREGISTER: Unregister registration entries.
269 //   wParam: The registration ID.
270 //   lParam: Ignored.
271 //   return: TRUE if successful.
272 LRESULT CChangeNotifyServer::OnUnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
273 {
274     TRACE("OnUnRegister(%p, %u, %p, %p)\n", m_hWnd, uMsg, wParam, lParam);
275 
276     // validate registration ID
277     UINT nRegID = (UINT)wParam;
278     if (nRegID == INVALID_REG_ID)
279     {
280         ERR("INVALID_REG_ID\n");
281         return FALSE;
282     }
283 
284     // remove it
285     DWORD dwOwnerPID;
286     GetWindowThreadProcessId(m_hWnd, &dwOwnerPID);
287     return RemoveItemsByRegID(nRegID, dwOwnerPID);
288 }
289 
290 // Message CN_DELIVER_NOTIFICATION: Perform a delivery.
291 //   wParam: The handle of delivery ticket.
292 //   lParam: The owner PID of delivery ticket.
293 //   return: TRUE if necessary.
294 LRESULT CChangeNotifyServer::OnDeliverNotification(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
295 {
296     TRACE("OnDeliverNotification(%p, %u, %p, %p)\n", m_hWnd, uMsg, wParam, lParam);
297 
298     HANDLE hTicket = (HANDLE)wParam;
299     DWORD dwOwnerPID = (DWORD)lParam;
300 
301     // do delivery
302     BOOL ret = DeliverNotification(hTicket, dwOwnerPID);
303 
304     // free the ticket
305     SHFreeShared(hTicket, dwOwnerPID);
306     return ret;
307 }
308 
309 // Message CN_SUSPEND_RESUME: Suspend or resume the change notification.
310 //   (specification is unknown)
311 LRESULT CChangeNotifyServer::OnSuspendResume(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
312 {
313     TRACE("OnSuspendResume\n");
314 
315     // FIXME
316     return FALSE;
317 }
318 
319 // Message CN_UNREGISTER_PROCESS: Remove registration entries by PID.
320 //   wParam: The user PID.
321 //   lParam: Ignored.
322 //   return: Zero.
323 LRESULT CChangeNotifyServer::OnRemoveByPID(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
324 {
325     DWORD dwOwnerPID, dwUserPID = (DWORD)wParam;
326     GetWindowThreadProcessId(m_hWnd, &dwOwnerPID);
327     RemoveItemsByProcess(dwOwnerPID, dwUserPID);
328     return 0;
329 }
330 
331 LRESULT CChangeNotifyServer::OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
332 {
333     CDirectoryWatcher::RequestAllWatchersTermination();
334     return 0;
335 }
336 
337 // get next valid registration ID
338 UINT CChangeNotifyServer::GetNextRegID()
339 {
340     m_nNextRegID++;
341     if (m_nNextRegID == INVALID_REG_ID)
342         m_nNextRegID++;
343     return m_nNextRegID;
344 }
345 
346 // This function is called from CChangeNotifyServer::OnDeliverNotification.
347 // The function notifies to the registration entries that should be notified.
348 BOOL CChangeNotifyServer::DeliverNotification(HANDLE hTicket, DWORD dwOwnerPID)
349 {
350     TRACE("DeliverNotification(%p, %p, 0x%lx)\n", m_hWnd, hTicket, dwOwnerPID);
351 
352     // lock the delivery ticket
353     LPDELITICKET pTicket = (LPDELITICKET)SHLockSharedEx(hTicket, dwOwnerPID, FALSE);
354     if (pTicket == NULL || pTicket->dwMagic != DELITICKET_MAGIC)
355     {
356         ERR("pTicket is invalid\n");
357         SHUnlockShared(pTicket);
358         return FALSE;
359     }
360 
361     // for all items
362     for (INT i = 0; i < m_items.GetSize(); ++i)
363     {
364         // validate the item
365         if (m_items[i].nRegID == INVALID_REG_ID)
366             continue;
367 
368         HANDLE hRegEntry = m_items[i].hRegEntry;
369         if (hRegEntry == NULL)
370             continue;
371 
372         // lock the registration entry
373         LPREGENTRY pRegEntry = (LPREGENTRY)SHLockSharedEx(hRegEntry, dwOwnerPID, FALSE);
374         if (pRegEntry == NULL || pRegEntry->dwMagic != REGENTRY_MAGIC)
375         {
376             ERR("pRegEntry is invalid\n");
377             SHUnlockShared(pRegEntry);
378             continue;
379         }
380 
381         // should we notify for it?
382         BOOL bNotify = ShouldNotify(pTicket, pRegEntry);
383         if (bNotify)
384         {
385             // do notify
386             TRACE("Notifying: %p, 0x%x, %p, %lu\n",
387                   pRegEntry->hwnd, pRegEntry->uMsg, hTicket, dwOwnerPID);
388             SendMessageW(pRegEntry->hwnd, pRegEntry->uMsg, (WPARAM)hTicket, dwOwnerPID);
389         }
390 
391         // unlock the registration entry
392         SHUnlockShared(pRegEntry);
393     }
394 
395     // unlock the ticket
396     SHUnlockShared(pTicket);
397 
398     return TRUE;
399 }
400 
401 BOOL CChangeNotifyServer::ShouldNotify(LPDELITICKET pTicket, LPREGENTRY pRegEntry)
402 {
403     LPITEMIDLIST pidl, pidl1 = NULL, pidl2 = NULL;
404     WCHAR szPath[MAX_PATH], szPath1[MAX_PATH], szPath2[MAX_PATH];
405     INT cch, cch1, cch2;
406 
407     // check fSources
408     if (pTicket->uFlags & SHCNE_INTERRUPT)
409     {
410         if (!(pRegEntry->fSources & SHCNRF_InterruptLevel))
411             return FALSE;
412     }
413     else
414     {
415         if (!(pRegEntry->fSources & SHCNRF_ShellLevel))
416             return FALSE;
417     }
418 
419     if (pRegEntry->ibPidl == 0)
420         return TRUE; // there is no PIDL
421 
422     // get the stored pidl
423     pidl = (LPITEMIDLIST)((LPBYTE)pRegEntry + pRegEntry->ibPidl);
424     if (pidl->mkid.cb == 0 && pRegEntry->fRecursive)
425         return TRUE;    // desktop is the root
426 
427     // check pidl1
428     if (pTicket->ibOffset1)
429     {
430         pidl1 = (LPITEMIDLIST)((LPBYTE)pTicket + pTicket->ibOffset1);
431         if (ILIsEqual(pidl, pidl1) || ILIsParent(pidl, pidl1, !pRegEntry->fRecursive))
432             return TRUE;
433     }
434 
435     // check pidl2
436     if (pTicket->ibOffset2)
437     {
438         pidl2 = (LPITEMIDLIST)((LPBYTE)pTicket + pTicket->ibOffset2);
439         if (ILIsEqual(pidl, pidl2) || ILIsParent(pidl, pidl2, !pRegEntry->fRecursive))
440             return TRUE;
441     }
442 
443     // The paths:
444     //   "C:\\Path\\To\\File1"
445     //   "C:\\Path\\To\\File1Test"
446     // should be distinguished in comparison, so we add backslash at last as follows:
447     //   "C:\\Path\\To\\File1\\"
448     //   "C:\\Path\\To\\File1Test\\"
449     if (SHGetPathFromIDListW(pidl, szPath))
450     {
451         PathAddBackslashW(szPath);
452         cch = lstrlenW(szPath);
453 
454         if (pidl1 && SHGetPathFromIDListW(pidl1, szPath1))
455         {
456             PathAddBackslashW(szPath1);
457             cch1 = lstrlenW(szPath1);
458 
459             // Is szPath1 a subfile or subdirectory of szPath?
460             if (cch < cch1 &&
461                 (pRegEntry->fRecursive ||
462                  wcschr(&szPath1[cch], L'\\') == &szPath1[cch1 - 1]))
463             {
464                 szPath1[cch] = 0;
465                 if (lstrcmpiW(szPath, szPath1) == 0)
466                     return TRUE;
467             }
468         }
469 
470         if (pidl2 && SHGetPathFromIDListW(pidl2, szPath2))
471         {
472             PathAddBackslashW(szPath2);
473             cch2 = lstrlenW(szPath2);
474 
475             // Is szPath2 a subfile or subdirectory of szPath?
476             if (cch < cch2 &&
477                 (pRegEntry->fRecursive ||
478                  wcschr(&szPath2[cch], L'\\') == &szPath2[cch2 - 1]))
479             {
480                 szPath2[cch] = 0;
481                 if (lstrcmpiW(szPath, szPath2) == 0)
482                     return TRUE;
483             }
484         }
485     }
486 
487     return FALSE;
488 }
489 
490 HRESULT WINAPI CChangeNotifyServer::GetWindow(HWND* phwnd)
491 {
492     if (!phwnd)
493         return E_INVALIDARG;
494     *phwnd = m_hWnd;
495     return S_OK;
496 }
497 
498 HRESULT WINAPI CChangeNotifyServer::ContextSensitiveHelp(BOOL fEnterMode)
499 {
500     return E_NOTIMPL;
501 }
502 
503 HRESULT CChangeNotifyServer::Initialize()
504 {
505     // This is called by CChangeNotifyServer_CreateInstance right after instantiation.
506     // Create the window of the server here.
507     Create(0);
508     if (!m_hWnd)
509         return E_FAIL;
510     return S_OK;
511 }
512 
513 HRESULT CChangeNotifyServer_CreateInstance(REFIID riid, void **ppv)
514 {
515     return ShellObjectCreatorInit<CChangeNotifyServer>(riid, ppv);
516 }
517