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 //////////////////////////////////////////////////////////////////////////////
15 
16 // notification target item
17 struct CWatchItem
18 {
19     UINT nRegID;        // The registration ID.
20     DWORD dwUserPID;    // The user PID; that is the process ID of the target window.
21     LPREGENTRY pRegEntry;   // The registration entry.
22     HWND hwndBroker;    // Client broker window (if any).
23     CDirectoryWatcher *pDirWatch; // for filesystem notification
24 };
25 
26 //////////////////////////////////////////////////////////////////////////////
27 // CChangeNotifyServer
28 //
29 // CChangeNotifyServer implements a window that handles all shell change notifications.
30 // It runs in the context of explorer and specifically in the thread of the shell desktop.
31 // Shell change notification api exported from shell32 forwards all their calls
32 // to this window where all processing takes place.
33 
34 class CChangeNotifyServer :
35     public CWindowImpl<CChangeNotifyServer, CWindow, CWorkerTraits>,
36     public CComObjectRootEx<CComMultiThreadModelNoCS>,
37     public IOleWindow
38 {
39 public:
40     CChangeNotifyServer();
41     virtual ~CChangeNotifyServer();
42     HRESULT Initialize();
43 
44     // *** IOleWindow methods ***
45     STDMETHOD(GetWindow)(HWND *lphwnd) override;
46     STDMETHOD(ContextSensitiveHelp)(BOOL fEnterMode) override;
47 
48     // Message handlers
49     LRESULT OnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
50     LRESULT OnUnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
51     LRESULT OnDeliverNotification(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
52     LRESULT OnSuspendResume(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
53     LRESULT OnRemoveByPID(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
54     LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled);
55 
56     DECLARE_NOT_AGGREGATABLE(CChangeNotifyServer)
57 
58     DECLARE_PROTECT_FINAL_CONSTRUCT()
59     BEGIN_COM_MAP(CChangeNotifyServer)
60         COM_INTERFACE_ENTRY_IID(IID_IOleWindow, IOleWindow)
61     END_COM_MAP()
62 
63     DECLARE_WND_CLASS_EX(L"WorkerW", 0, 0)
64 
65     BEGIN_MSG_MAP(CChangeNotifyServer)
66         MESSAGE_HANDLER(CN_REGISTER, OnRegister)
67         MESSAGE_HANDLER(CN_UNREGISTER, OnUnRegister)
68         MESSAGE_HANDLER(CN_DELIVER_NOTIFICATION, OnDeliverNotification)
69         MESSAGE_HANDLER(CN_SUSPEND_RESUME, OnSuspendResume)
70         MESSAGE_HANDLER(CN_UNREGISTER_PROCESS, OnRemoveByPID);
71         MESSAGE_HANDLER(WM_DESTROY, OnDestroy);
72     END_MSG_MAP()
73 
74 private:
75     UINT m_nNextRegID;
76     CSimpleArray<CWatchItem*> m_items;
77 
78     BOOL AddItem(CWatchItem *pItem);
79     BOOL RemoveItemsByRegID(UINT nRegID);
80     BOOL RemoveItemsByProcess(DWORD dwUserPID);
81     void DestroyItem(CWatchItem *pItem, HWND *phwndBroker);
82     void DestroyAllItems();
83 
84     UINT GetNextRegID();
85     BOOL DeliverNotification(HANDLE hTicket, DWORD dwOwnerPID);
86     BOOL ShouldNotify(LPDELITICKET pTicket, LPREGENTRY pRegEntry);
87 };
88 
89 CChangeNotifyServer::CChangeNotifyServer()
90     : m_nNextRegID(INVALID_REG_ID)
91 {
92 }
93 
94 CChangeNotifyServer::~CChangeNotifyServer()
95 {
96 }
97 
98 BOOL CChangeNotifyServer::AddItem(CWatchItem *pItem)
99 {
100     // find the empty room
101     for (INT i = 0; i < m_items.GetSize(); ++i)
102     {
103         if (m_items[i] == NULL)
104         {
105             // found the room, populate it
106             m_items[i] = pItem;
107             return TRUE;
108         }
109     }
110 
111     // no empty room found
112     m_items.Add(pItem);
113     return TRUE;
114 }
115 
116 void CChangeNotifyServer::DestroyItem(CWatchItem *pItem, HWND *phwndBroker)
117 {
118     assert(pItem);
119 
120     // destroy broker if any and if first time
121     HWND hwndBroker = pItem->hwndBroker;
122     pItem->hwndBroker = NULL;
123     if (hwndBroker && hwndBroker != *phwndBroker)
124     {
125         ::DestroyWindow(hwndBroker);
126         *phwndBroker = hwndBroker;
127     }
128 
129     // request termination of pDirWatch if any
130     CDirectoryWatcher *pDirWatch = pItem->pDirWatch;
131     pItem->pDirWatch = NULL;
132     if (pDirWatch)
133         pDirWatch->RequestTermination();
134 
135     // free
136     SHFree(pItem->pRegEntry);
137     delete pItem;
138 }
139 
140 void CChangeNotifyServer::DestroyAllItems()
141 {
142     for (INT i = 0; i < m_items.GetSize(); ++i)
143     {
144         if (m_items[i])
145         {
146             HWND hwndBroker = NULL;
147             DestroyItem(m_items[i], &hwndBroker);
148             m_items[i] = NULL;
149         }
150     }
151     m_items.RemoveAll();
152 }
153 
154 BOOL CChangeNotifyServer::RemoveItemsByRegID(UINT nRegID)
155 {
156     BOOL bFound = FALSE;
157     HWND hwndBroker = NULL;
158     assert(nRegID != INVALID_REG_ID);
159     for (INT i = 0; i < m_items.GetSize(); ++i)
160     {
161         if (m_items[i] && m_items[i]->nRegID == nRegID)
162         {
163             bFound = TRUE;
164             DestroyItem(m_items[i], &hwndBroker);
165             m_items[i] = NULL;
166         }
167     }
168     return bFound;
169 }
170 
171 BOOL CChangeNotifyServer::RemoveItemsByProcess(DWORD dwUserPID)
172 {
173     BOOL bFound = FALSE;
174     HWND hwndBroker = NULL;
175     assert(dwUserPID != 0);
176     for (INT i = 0; i < m_items.GetSize(); ++i)
177     {
178         if (m_items[i] && m_items[i]->dwUserPID == dwUserPID)
179         {
180             bFound = TRUE;
181             DestroyItem(m_items[i], &hwndBroker);
182             m_items[i] = NULL;
183         }
184     }
185     return bFound;
186 }
187 
188 // create a CDirectoryWatcher from a REGENTRY
189 static CDirectoryWatcher *
190 CreateDirectoryWatcherFromRegEntry(LPREGENTRY pRegEntry)
191 {
192     if (pRegEntry->ibPidl == 0)
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     LPREGENTRY pNewEntry = (LPREGENTRY)SHAlloc(pRegEntry->cbSize);
243     if (pNewEntry == NULL)
244     {
245         ERR("Out of memory\n");
246         pRegEntry->nRegID = INVALID_REG_ID;
247         SHUnlockShared(pRegEntry);
248         return FALSE;
249     }
250     CopyMemory(pNewEntry, pRegEntry, pRegEntry->cbSize);
251 
252     // create a directory watch if necessary
253     CDirectoryWatcher *pDirWatch = NULL;
254     if (pRegEntry->ibPidl && (pRegEntry->fSources & SHCNRF_InterruptLevel))
255     {
256         pDirWatch = CreateDirectoryWatcherFromRegEntry(pRegEntry);
257         if (pDirWatch && !pDirWatch->RequestAddWatcher())
258         {
259             ERR("RequestAddWatcher failed: %u\n", pRegEntry->nRegID);
260             pRegEntry->nRegID = INVALID_REG_ID;
261             SHUnlockShared(pRegEntry);
262             delete pDirWatch;
263             return FALSE;
264         }
265     }
266 
267     // unlock the registry entry
268     SHUnlockShared(pRegEntry);
269 
270     // add an item
271     CWatchItem *pItem = new CWatchItem { m_nNextRegID, dwUserPID, pNewEntry, hwndBroker, pDirWatch };
272     return AddItem(pItem);
273 }
274 
275 // Message CN_UNREGISTER: Unregister registration entries.
276 //   wParam: The registration ID.
277 //   lParam: Ignored.
278 //   return: TRUE if successful.
279 LRESULT CChangeNotifyServer::OnUnRegister(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
280 {
281     TRACE("OnUnRegister(%p, %u, %p, %p)\n", m_hWnd, uMsg, wParam, lParam);
282 
283     // validate registration ID
284     UINT nRegID = (UINT)wParam;
285     if (nRegID == INVALID_REG_ID)
286     {
287         ERR("INVALID_REG_ID\n");
288         return FALSE;
289     }
290 
291     // remove it
292     return RemoveItemsByRegID(nRegID);
293 }
294 
295 // Message CN_DELIVER_NOTIFICATION: Perform a delivery.
296 //   wParam: The handle of delivery ticket.
297 //   lParam: The owner PID of delivery ticket.
298 //   return: TRUE if necessary.
299 LRESULT CChangeNotifyServer::OnDeliverNotification(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
300 {
301     TRACE("OnDeliverNotification(%p, %u, %p, %p)\n", m_hWnd, uMsg, wParam, lParam);
302 
303     HANDLE hTicket = (HANDLE)wParam;
304     DWORD dwOwnerPID = (DWORD)lParam;
305 
306     // do delivery
307     BOOL ret = DeliverNotification(hTicket, dwOwnerPID);
308 
309     // free the ticket
310     SHFreeShared(hTicket, dwOwnerPID);
311     return ret;
312 }
313 
314 // Message CN_SUSPEND_RESUME: Suspend or resume the change notification.
315 //   (specification is unknown)
316 LRESULT CChangeNotifyServer::OnSuspendResume(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
317 {
318     TRACE("OnSuspendResume\n");
319 
320     // FIXME
321     return FALSE;
322 }
323 
324 // Message CN_UNREGISTER_PROCESS: Remove registration entries by PID.
325 //   wParam: The user PID.
326 //   lParam: Ignored.
327 //   return: Zero.
328 LRESULT CChangeNotifyServer::OnRemoveByPID(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
329 {
330     DWORD dwUserPID = (DWORD)wParam;
331     RemoveItemsByProcess(dwUserPID);
332     return 0;
333 }
334 
335 LRESULT CChangeNotifyServer::OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
336 {
337     DestroyAllItems();
338     CDirectoryWatcher::RequestAllWatchersTermination();
339     return 0;
340 }
341 
342 // get next valid registration ID
343 UINT CChangeNotifyServer::GetNextRegID()
344 {
345     m_nNextRegID++;
346     if (m_nNextRegID == INVALID_REG_ID)
347         m_nNextRegID++;
348     return m_nNextRegID;
349 }
350 
351 // This function is called from CChangeNotifyServer::OnDeliverNotification.
352 // The function notifies to the registration entries that should be notified.
353 BOOL CChangeNotifyServer::DeliverNotification(HANDLE hTicket, DWORD dwOwnerPID)
354 {
355     TRACE("DeliverNotification(%p, %p, 0x%lx)\n", m_hWnd, hTicket, dwOwnerPID);
356 
357     // lock the delivery ticket
358     LPDELITICKET pTicket = (LPDELITICKET)SHLockSharedEx(hTicket, dwOwnerPID, FALSE);
359     if (pTicket == NULL || pTicket->dwMagic != DELITICKET_MAGIC)
360     {
361         ERR("pTicket is invalid\n");
362         SHUnlockShared(pTicket);
363         return FALSE;
364     }
365 
366     // for all items
367     for (INT i = 0; i < m_items.GetSize(); ++i)
368     {
369         if (m_items[i] == NULL)
370             continue;
371 
372         LPREGENTRY pRegEntry = m_items[i]->pRegEntry;
373         if (pRegEntry == NULL || pRegEntry->dwMagic != REGENTRY_MAGIC)
374         {
375             ERR("pRegEntry is invalid\n");
376             continue;
377         }
378 
379         // should we notify for it?
380         BOOL bNotify = ShouldNotify(pTicket, pRegEntry);
381         if (bNotify)
382         {
383             // do notify
384             TRACE("Notifying: %p, 0x%x, %p, %lu\n",
385                   pRegEntry->hwnd, pRegEntry->uMsg, hTicket, dwOwnerPID);
386             SendMessageW(pRegEntry->hwnd, pRegEntry->uMsg, (WPARAM)hTicket, dwOwnerPID);
387             TRACE("GetLastError(): %ld\n", ::GetLastError());
388         }
389     }
390 
391     // unlock the ticket
392     SHUnlockShared(pTicket);
393 
394     return TRUE;
395 }
396 
397 BOOL CChangeNotifyServer::ShouldNotify(LPDELITICKET pTicket, LPREGENTRY pRegEntry)
398 {
399 #define RETURN(x) do { \
400     TRACE("ShouldNotify return %d\n", (x)); \
401     return (x); \
402 } while (0)
403 
404     if (pTicket->wEventId & SHCNE_INTERRUPT)
405     {
406         if (!(pRegEntry->fSources & SHCNRF_InterruptLevel))
407             RETURN(FALSE);
408         if (!pRegEntry->ibPidl)
409             RETURN(FALSE);
410     }
411     else
412     {
413         if (!(pRegEntry->fSources & SHCNRF_ShellLevel))
414             RETURN(FALSE);
415     }
416 
417     if (!(pTicket->wEventId & pRegEntry->fEvents))
418         RETURN(FALSE);
419 
420     LPITEMIDLIST pidl = NULL, pidl1 = NULL, pidl2 = NULL;
421     if (pRegEntry->ibPidl)
422         pidl = (LPITEMIDLIST)((LPBYTE)pRegEntry + pRegEntry->ibPidl);
423     if (pTicket->ibOffset1)
424         pidl1 = (LPITEMIDLIST)((LPBYTE)pTicket + pTicket->ibOffset1);
425     if (pTicket->ibOffset2)
426         pidl2 = (LPITEMIDLIST)((LPBYTE)pTicket + pTicket->ibOffset2);
427 
428     if (pidl == NULL || (pTicket->wEventId & SHCNE_GLOBALEVENTS))
429         RETURN(TRUE);
430 
431     if (pRegEntry->fRecursive)
432     {
433         if (ILIsParent(pidl, pidl1, FALSE) ||
434             (pidl2 && ILIsParent(pidl, pidl2, FALSE)))
435         {
436             RETURN(TRUE);
437         }
438     }
439     else
440     {
441         if (ILIsEqual(pidl, pidl1) ||
442             ILIsParent(pidl, pidl1, TRUE) ||
443             (pidl2 && ILIsParent(pidl, pidl2, TRUE)))
444         {
445             RETURN(TRUE);
446         }
447     }
448 
449     RETURN(FALSE);
450 #undef RETURN
451 }
452 
453 HRESULT WINAPI CChangeNotifyServer::GetWindow(HWND* phwnd)
454 {
455     if (!phwnd)
456         return E_INVALIDARG;
457     *phwnd = m_hWnd;
458     return S_OK;
459 }
460 
461 HRESULT WINAPI CChangeNotifyServer::ContextSensitiveHelp(BOOL fEnterMode)
462 {
463     return E_NOTIMPL;
464 }
465 
466 HRESULT CChangeNotifyServer::Initialize()
467 {
468     // This is called by CChangeNotifyServer_CreateInstance right after instantiation.
469     HWND hwnd = SHCreateDefaultWorkerWindow();
470     if (!hwnd)
471         return E_FAIL;
472     SubclassWindow(hwnd);
473     return S_OK;
474 }
475 
476 HRESULT CChangeNotifyServer_CreateInstance(REFIID riid, void **ppv)
477 {
478     return ShellObjectCreatorInit<CChangeNotifyServer>(riid, ppv);
479 }
480