1 /*
2  * PROJECT:     ReactOS Zip Shell Extension
3  * LICENSE:     GPL-2.0+ (https://spdx.org/licenses/GPL-2.0+)
4  * PURPOSE:     Zip extraction
5  * COPYRIGHT:   Copyright 2017 Mark Jansen (mark.jansen@reactos.org)
6  */
7 
8 #include "precomp.h"
9 
10 class CZipExtract :
11     public IZip
12 {
13     CStringW m_Filename;
14     CStringW m_Directory;
15     bool m_DirectoryChanged;
16     unzFile uf;
17 public:
18     CZipExtract(PCWSTR Filename)
19         :m_DirectoryChanged(false)
20         ,uf(NULL)
21     {
22         m_Filename = Filename;
23         m_Directory = m_Filename;
24         PWSTR Dir = m_Directory.GetBuffer();
25         PathRemoveExtensionW(Dir);
26         m_Directory.ReleaseBuffer();
27     }
28 
29     ~CZipExtract()
30     {
31         if (uf)
32         {
33             DPRINT1("WARNING: uf not closed!\n");
34             Close();
35         }
36     }
37 
38     void Close()
39     {
40         if (uf)
41             unzClose(uf);
42         uf = NULL;
43     }
44 
45     // *** IZip methods ***
46     STDMETHODIMP QueryInterface(REFIID riid, void  **ppvObject)
47     {
48         if (riid == IID_IUnknown)
49         {
50             *ppvObject = this;
51             AddRef();
52             return S_OK;
53         }
54         return E_NOINTERFACE;
55     }
56     STDMETHODIMP_(ULONG) AddRef(void)
57     {
58         return 2;
59     }
60     STDMETHODIMP_(ULONG) Release(void)
61     {
62         return 1;
63     }
64     STDMETHODIMP_(unzFile) getZip()
65     {
66         return uf;
67     }
68 
69     class CConfirmReplace : public CDialogImpl<CConfirmReplace>
70     {
71     private:
72         CStringA m_Filename;
73     public:
74         enum DialogResult
75         {
76             Yes,
77             YesToAll,
78             No,
79             Cancel
80         };
81 
82         static DialogResult ShowDlg(HWND hDlg, PCSTR FullPath)
83         {
84             PCSTR Filename = PathFindFileNameA(FullPath);
85             CConfirmReplace confirm(Filename);
86             INT_PTR Result = confirm.DoModal(hDlg);
87             switch (Result)
88             {
89             case IDYES: return Yes;
90             case IDYESALL: return YesToAll;
91             default:
92             case IDNO: return No;
93             case IDCANCEL: return Cancel;
94             }
95         }
96 
97         CConfirmReplace(const char* filename)
98         {
99             m_Filename = filename;
100         }
101 
102         LRESULT OnInitDialog(UINT nMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
103         {
104             CenterWindow(GetParent());
105 
106             HICON hIcon = LoadIcon(NULL, IDI_EXCLAMATION);
107             SendDlgItemMessage(IDC_EXCLAMATION_ICON, STM_SETICON, (WPARAM)hIcon);
108 
109             /* Our CString does not support FormatMessage yet */
110             CStringA message(MAKEINTRESOURCE(IDS_OVERWRITEFILE_TEXT));
111             CHeapPtr<CHAR, CLocalAllocator> formatted;
112 
113             DWORD_PTR args[2] =
114             {
115                 (DWORD_PTR)m_Filename.GetString(),
116                 NULL
117             };
118 
119             ::FormatMessageA(FORMAT_MESSAGE_FROM_STRING | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_ARGUMENT_ARRAY,
120                              message, 0, 0, (LPSTR)&formatted, 0, (va_list*)args);
121 
122             ::SetDlgItemTextA(m_hWnd, IDC_MESSAGE, formatted);
123             return 0;
124         }
125 
126         LRESULT OnButton(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
127         {
128             EndDialog(wID);
129             return 0;
130         }
131 
132     public:
133         enum { IDD = IDD_CONFIRM_FILE_REPLACE };
134 
135         BEGIN_MSG_MAP(CConfirmReplace)
136             MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
137             COMMAND_ID_HANDLER(IDYES, OnButton)
138             COMMAND_ID_HANDLER(IDYESALL, OnButton)
139             COMMAND_ID_HANDLER(IDNO, OnButton)
140             COMMAND_ID_HANDLER(IDCANCEL, OnButton)
141         END_MSG_MAP()
142     };
143 
144 
145     class CExtractSettingsPage : public CPropertyPageImpl<CExtractSettingsPage>
146     {
147     private:
148         CZipExtract* m_pExtract;
149 
150     public:
151         CExtractSettingsPage(CZipExtract* extract)
152             :CPropertyPageImpl<CExtractSettingsPage>(MAKEINTRESOURCE(IDS_WIZ_TITLE))
153             ,m_pExtract(extract)
154         {
155             m_psp.pszHeaderTitle = MAKEINTRESOURCE(IDS_WIZ_DEST_TITLE);
156             m_psp.pszHeaderSubTitle = MAKEINTRESOURCE(IDS_WIZ_DEST_SUBTITLE);
157             m_psp.dwFlags |= PSP_USETITLE | PSP_USEHEADERTITLE | PSP_USEHEADERSUBTITLE;
158         }
159 
160         int OnSetActive()
161         {
162             SetDlgItemTextW(IDC_DIRECTORY, m_pExtract->m_Directory);
163             m_pExtract->m_DirectoryChanged = false;
164             ::EnableWindow(GetDlgItem(IDC_PASSWORD), FALSE);    /* Not supported for now */
165             GetParent().CenterWindow(::GetDesktopWindow());
166             return 0;
167         }
168 
169         int OnWizardNext()
170         {
171             ::EnableWindow(GetDlgItem(IDC_BROWSE), FALSE);
172             ::EnableWindow(GetDlgItem(IDC_DIRECTORY), FALSE);
173             ::EnableWindow(GetDlgItem(IDC_PASSWORD), FALSE);
174 
175             if (m_pExtract->m_DirectoryChanged)
176                 UpdateDirectory();
177 
178             if (!m_pExtract->Extract(m_hWnd, GetDlgItem(IDC_PROGRESS)))
179             {
180                 /* Extraction failed, do not go to the next page */
181                 SetWindowLongPtr(DWLP_MSGRESULT, -1);
182 
183                 ::EnableWindow(GetDlgItem(IDC_BROWSE), TRUE);
184                 ::EnableWindow(GetDlgItem(IDC_DIRECTORY), TRUE);
185                 ::EnableWindow(GetDlgItem(IDC_PASSWORD), FALSE);    /* Not supported for now */
186 
187                 return TRUE;
188             }
189             return 0;
190         }
191 
192         struct browse_info
193         {
194             HWND hWnd;
195             LPCWSTR Directory;
196         };
197 
198         static INT CALLBACK s_BrowseCallbackProc(HWND hWnd, UINT uMsg, LPARAM lp, LPARAM pData)
199         {
200             if (uMsg == BFFM_INITIALIZED)
201             {
202                 browse_info* info = (browse_info*)pData;
203                 CWindow dlg(hWnd);
204                 dlg.SendMessage(BFFM_SETSELECTION, TRUE, (LPARAM)info->Directory);
205                 dlg.CenterWindow(info->hWnd);
206             }
207             return 0;
208         }
209 
210         LRESULT OnBrowse(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
211         {
212             BROWSEINFOW bi = { m_hWnd };
213             WCHAR path[MAX_PATH];
214             bi.pszDisplayName = path;
215             bi.lpfn = s_BrowseCallbackProc;
216             bi.ulFlags = BIF_NEWDIALOGSTYLE | BIF_RETURNFSANCESTORS | BIF_RETURNONLYFSDIRS;
217             CStringW title(MAKEINTRESOURCEW(IDS_WIZ_BROWSE_TITLE));
218             bi.lpszTitle = title;
219 
220             if (m_pExtract->m_DirectoryChanged)
221                 UpdateDirectory();
222 
223             browse_info info = { m_hWnd, m_pExtract->m_Directory.GetString() };
224             bi.lParam = (LPARAM)&info;
225 
226             CComHeapPtr<ITEMIDLIST> pidl;
227             pidl.Attach(SHBrowseForFolderW(&bi));
228 
229             WCHAR tmpPath[MAX_PATH];
230             if (pidl && SHGetPathFromIDListW(pidl, tmpPath))
231             {
232                 m_pExtract->m_Directory = tmpPath;
233                 SetDlgItemTextW(IDC_DIRECTORY, m_pExtract->m_Directory);
234                 m_pExtract->m_DirectoryChanged = false;
235             }
236             return 0;
237         }
238 
239         LRESULT OnEnChangeDirectory(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
240         {
241             m_pExtract->m_DirectoryChanged = true;
242             return 0;
243         }
244 
245         LRESULT OnPassword(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
246         {
247             return 0;
248         }
249 
250         void UpdateDirectory()
251         {
252             GetDlgItemText(IDC_DIRECTORY, m_pExtract->m_Directory);
253             m_pExtract->m_DirectoryChanged = false;
254         }
255 
256     public:
257         enum { IDD = IDD_PROPPAGEDESTINATION };
258 
259         BEGIN_MSG_MAP(CCompleteSettingsPage)
260             COMMAND_ID_HANDLER(IDC_BROWSE, OnBrowse)
261             COMMAND_ID_HANDLER(IDC_PASSWORD, OnPassword)
262             COMMAND_HANDLER(IDC_DIRECTORY, EN_CHANGE, OnEnChangeDirectory)
263             CHAIN_MSG_MAP(CPropertyPageImpl<CExtractSettingsPage>)
264         END_MSG_MAP()
265     };
266 
267 
268     class CCompleteSettingsPage : public CPropertyPageImpl<CCompleteSettingsPage>
269     {
270     private:
271         CZipExtract* m_pExtract;
272 
273     public:
274         CCompleteSettingsPage(CZipExtract* extract)
275             :CPropertyPageImpl<CCompleteSettingsPage>(MAKEINTRESOURCE(IDS_WIZ_TITLE))
276             , m_pExtract(extract)
277         {
278             m_psp.pszHeaderTitle = MAKEINTRESOURCE(IDS_WIZ_COMPL_TITLE);
279             m_psp.pszHeaderSubTitle = MAKEINTRESOURCE(IDS_WIZ_COMPL_SUBTITLE);
280             m_psp.dwFlags |= PSP_USETITLE | PSP_USEHEADERTITLE | PSP_USEHEADERSUBTITLE;
281         }
282 
283 
284         int OnSetActive()
285         {
286             SetWizardButtons(PSWIZB_FINISH);
287             CStringW Path = m_pExtract->m_Directory;
288             PWSTR Ptr = Path.GetBuffer();
289             RECT rc;
290             ::GetWindowRect(GetDlgItem(IDC_DESTDIR), &rc);
291             HDC dc = GetDC();
292             PathCompactPathW(dc, Ptr, rc.right - rc.left);
293             ReleaseDC(dc);
294             Path.ReleaseBuffer();
295             SetDlgItemTextW(IDC_DESTDIR, Path);
296             CheckDlgButton(IDC_SHOW_EXTRACTED, BST_CHECKED);
297             return 0;
298         }
299         BOOL OnWizardFinish()
300         {
301             if (IsDlgButtonChecked(IDC_SHOW_EXTRACTED) == BST_CHECKED)
302             {
303                 ShellExecuteW(NULL, L"explore", m_pExtract->m_Directory, NULL, NULL, SW_SHOW);
304             }
305             return FALSE;
306         }
307 
308     public:
309         enum { IDD = IDD_PROPPAGECOMPLETE };
310 
311         BEGIN_MSG_MAP(CCompleteSettingsPage)
312             CHAIN_MSG_MAP(CPropertyPageImpl<CCompleteSettingsPage>)
313         END_MSG_MAP()
314     };
315 
316 
317     void runWizard()
318     {
319         PROPSHEETHEADERW psh = { sizeof(psh), 0 };
320         psh.dwFlags = PSH_WIZARD97 | PSH_HEADER;
321         psh.hInstance = _AtlBaseModule.GetResourceInstance();
322 
323         CExtractSettingsPage extractPage(this);
324         CCompleteSettingsPage completePage(this);
325         HPROPSHEETPAGE hpsp[] =
326         {
327             extractPage.Create(),
328             completePage.Create()
329         };
330 
331         psh.phpage = hpsp;
332         psh.nPages = _countof(hpsp);
333 
334         PropertySheetW(&psh);
335     }
336 
337     bool Extract(HWND hDlg, HWND hProgress)
338     {
339         unz_global_info64 gi;
340         uf = unzOpen2_64(m_Filename.GetString(), &g_FFunc);
341         int err = unzGetGlobalInfo64(uf, &gi);
342         if (err != UNZ_OK)
343         {
344             DPRINT1("ERROR, unzGetGlobalInfo64: 0x%x\n", err);
345             Close();
346             return false;
347         }
348 
349         CZipEnumerator zipEnum;
350         if (!zipEnum.initialize(this))
351         {
352             DPRINT1("ERROR, zipEnum.initialize\n");
353             Close();
354             return false;
355         }
356 
357         CWindow Progress(hProgress);
358         Progress.SendMessage(PBM_SETRANGE32, 0, gi.number_entry);
359         Progress.SendMessage(PBM_SETPOS, 0, 0);
360 
361         BYTE Buffer[2048];
362         CStringA BaseDirectory = m_Directory;
363         CStringA Name;
364         unz_file_info64 Info;
365         int CurrentFile = 0;
366         bool bOverwriteAll = false;
367         while (zipEnum.next(Name, Info))
368         {
369             bool is_dir = Name.GetLength() > 0 && Name[Name.GetLength()-1] == '/';
370 
371             char CombinedPath[MAX_PATH * 2] = { 0 };
372             PathCombineA(CombinedPath, BaseDirectory, Name);
373             CStringA FullPath = CombinedPath;
374             FullPath.Replace('/', '\\');    /* SHPathPrepareForWriteA does not handle '/' */
375             DWORD dwFlags = SHPPFW_DIRCREATE | (is_dir ? SHPPFW_NONE : SHPPFW_IGNOREFILENAME);
376             HRESULT hr = SHPathPrepareForWriteA(hDlg, NULL, FullPath, dwFlags);
377             if (FAILED_UNEXPECTEDLY(hr))
378             {
379                 Close();
380                 return false;
381             }
382             CurrentFile++;
383             if (is_dir)
384                 continue;
385 
386             const char* password = NULL;
387             /* FIXME: Process password, if required and not specified, prompt the user */
388             err = unzOpenCurrentFilePassword(uf, password);
389             if (err != UNZ_OK)
390             {
391                 DPRINT1("ERROR, unzOpenCurrentFilePassword: 0x%x\n", err);
392                 Close();
393                 return false;
394             }
395 
396             HANDLE hFile = CreateFileA(FullPath, GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
397             if (hFile == INVALID_HANDLE_VALUE)
398             {
399                 DWORD dwErr = GetLastError();
400                 if (dwErr == ERROR_FILE_EXISTS)
401                 {
402                     bool bOverwrite = bOverwriteAll;
403                     if (!bOverwriteAll)
404                     {
405                         CConfirmReplace::DialogResult Result = CConfirmReplace::ShowDlg(hDlg, FullPath);
406                         switch (Result)
407                         {
408                         case CConfirmReplace::YesToAll:
409                             bOverwriteAll = true;
410                         case CConfirmReplace::Yes:
411                             bOverwrite = true;
412                             break;
413                         case CConfirmReplace::No:
414                             break;
415                         case CConfirmReplace::Cancel:
416                             unzCloseCurrentFile(uf);
417                             Close();
418                             return false;
419                         }
420                     }
421 
422                     if (bOverwrite)
423                     {
424                         hFile = CreateFileA(FullPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
425                         if (hFile == INVALID_HANDLE_VALUE)
426                         {
427                             dwErr = GetLastError();
428                         }
429                     }
430                     else
431                     {
432                         unzCloseCurrentFile(uf);
433                         continue;
434                     }
435                 }
436                 if (hFile == INVALID_HANDLE_VALUE)
437                 {
438                     unzCloseCurrentFile(uf);
439                     DPRINT1("ERROR, CreateFileA: 0x%x (%s)\n", dwErr, bOverwriteAll ? "Y" : "N");
440                     Close();
441                     return false;
442                 }
443             }
444 
445             do
446             {
447                 err = unzReadCurrentFile(uf, Buffer, sizeof(Buffer));
448 
449                 if (err < 0)
450                 {
451                     DPRINT1("ERROR, unzReadCurrentFile: 0x%x\n", err);
452                     break;
453                 }
454                 else if (err > 0)
455                 {
456                     DWORD dwWritten;
457                     if (!WriteFile(hFile, Buffer, err, &dwWritten, NULL))
458                     {
459                         DPRINT1("ERROR, WriteFile: 0x%x\n", GetLastError());
460                         break;
461                     }
462                     if (dwWritten != (DWORD)err)
463                     {
464                         DPRINT1("ERROR, WriteFile: dwWritten:%d err:%d\n", dwWritten, err);
465                         break;
466                     }
467                 }
468 
469             } while (err > 0);
470 
471             /* Update Filetime */
472             FILETIME LastAccessTime;
473             GetFileTime(hFile, NULL, &LastAccessTime, NULL);
474             FILETIME LocalFileTime;
475             DosDateTimeToFileTime((WORD)(Info.dosDate >> 16), (WORD)Info.dosDate, &LocalFileTime);
476             FILETIME FileTime;
477             LocalFileTimeToFileTime(&LocalFileTime, &FileTime);
478             SetFileTime(hFile, &FileTime, &LastAccessTime, &FileTime);
479 
480             /* Done.. */
481             CloseHandle(hFile);
482 
483             if (err)
484             {
485                 unzCloseCurrentFile(uf);
486                 DPRINT1("ERROR, unzReadCurrentFile2: 0x%x\n", err);
487                 Close();
488                 return false;
489             }
490             else
491             {
492                 err = unzCloseCurrentFile(uf);
493                 if (err != UNZ_OK)
494                 {
495                     DPRINT1("ERROR(non-fatal), unzCloseCurrentFile: 0x%x\n", err);
496                 }
497             }
498             Progress.SendMessage(PBM_SETPOS, CurrentFile, 0);
499         }
500 
501         Close();
502         return true;
503     }
504 };
505 
506 
507 void _CZipExtract_runWizard(PCWSTR Filename)
508 {
509     CZipExtract extractor(Filename);
510     extractor.runWizard();
511 }
512 
513