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