1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 /* -------------------------------------------------------------------
7 To Build This:
8 
9   You need to add this to the the makefile.win in mozilla/dom/base:
10 
11         .\$(OBJDIR)\nsFlyOwnPrintDialog.obj	\
12 
13 
14   And this to the makefile.win in mozilla/content/build:
15 
16 WIN_LIBS=                                       \
17         winspool.lib                           \
18         comctl32.lib                           \
19         comdlg32.lib
20 
21 ---------------------------------------------------------------------- */
22 
23 #include "plstr.h"
24 #include <windows.h>
25 #include <tchar.h>
26 
27 #include <unknwn.h>
28 #include <commdlg.h>
29 
30 #include "mozilla/BackgroundHangMonitor.h"
31 #include "mozilla/ScopeExit.h"
32 #include "mozilla/Span.h"
33 #include "nsString.h"
34 #include "nsReadableUtils.h"
35 #include "nsIPrintSettings.h"
36 #include "nsIPrintSettingsWin.h"
37 #include "nsIPrinterList.h"
38 #include "nsServiceManagerUtils.h"
39 
40 #include "nsRect.h"
41 
42 #include "nsCRT.h"
43 #include "prenv.h" /* for PR_GetEnv */
44 
45 #include <windows.h>
46 #include <winspool.h>
47 
48 // For Localization
49 
50 // For NS_CopyUnicodeToNative
51 #include "nsNativeCharsetUtils.h"
52 
53 // This is for extending the dialog
54 #include <dlgs.h>
55 
56 #include "nsWindowsHelpers.h"
57 #include "WinUtils.h"
58 
59 //-----------------------------------------------
60 // Global Data
61 //-----------------------------------------------
62 
63 static HWND gParentWnd = nullptr;
64 
65 //----------------------------------------------------------------------------------
66 // Returns a Global Moveable Memory Handle to a DevMode
67 // from the Printer by the name of aPrintName
68 //
69 // NOTE:
70 //   This function assumes that aPrintName has already been converted from
71 //   unicode
72 //
CreateGlobalDevModeAndInit(const nsString & aPrintName,nsIPrintSettings * aPS)73 static nsReturnRef<nsHGLOBAL> CreateGlobalDevModeAndInit(
74     const nsString& aPrintName, nsIPrintSettings* aPS) {
75   nsHPRINTER hPrinter = nullptr;
76   // const cast kludge for silly Win32 api's
77   LPWSTR printName =
78       const_cast<wchar_t*>(static_cast<const wchar_t*>(aPrintName.get()));
79   BOOL status = ::OpenPrinterW(printName, &hPrinter, nullptr);
80   if (!status) {
81     return nsReturnRef<nsHGLOBAL>();
82   }
83 
84   // Make sure hPrinter is closed on all paths
85   nsAutoPrinter autoPrinter(hPrinter);
86 
87   // Get the buffer size
88   LONG needed = ::DocumentPropertiesW(gParentWnd, hPrinter, printName, nullptr,
89                                       nullptr, 0);
90   if (needed < 0) {
91     return nsReturnRef<nsHGLOBAL>();
92   }
93 
94   // Some drivers do not return the correct size for their DEVMODE, so we
95   // over-allocate to try and compensate.
96   // (See https://bugzilla.mozilla.org/show_bug.cgi?id=1664530#c5)
97   needed *= 2;
98   nsAutoDevMode newDevMode(
99       (LPDEVMODEW)::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, needed));
100   if (!newDevMode) {
101     return nsReturnRef<nsHGLOBAL>();
102   }
103 
104   nsHGLOBAL hDevMode = ::GlobalAlloc(GHND, needed);
105   nsAutoGlobalMem globalDevMode(hDevMode);
106   if (!hDevMode) {
107     return nsReturnRef<nsHGLOBAL>();
108   }
109 
110   LONG ret = ::DocumentPropertiesW(gParentWnd, hPrinter, printName, newDevMode,
111                                    nullptr, DM_OUT_BUFFER);
112   if (ret != IDOK) {
113     return nsReturnRef<nsHGLOBAL>();
114   }
115 
116   // Lock memory and copy contents from DEVMODE (current printer)
117   // to Global Memory DEVMODE
118   LPDEVMODEW devMode = (DEVMODEW*)::GlobalLock(hDevMode);
119   if (!devMode) {
120     return nsReturnRef<nsHGLOBAL>();
121   }
122 
123   memcpy(devMode, newDevMode.get(), needed);
124   // Initialize values from the PrintSettings
125   nsCOMPtr<nsIPrintSettingsWin> psWin = do_QueryInterface(aPS);
126   MOZ_ASSERT(psWin);
127   psWin->CopyToNative(devMode);
128 
129   // Sets back the changes we made to the DevMode into the Printer Driver
130   ret = ::DocumentPropertiesW(gParentWnd, hPrinter, printName, devMode, devMode,
131                               DM_IN_BUFFER | DM_OUT_BUFFER);
132   if (ret != IDOK) {
133     ::GlobalUnlock(hDevMode);
134     return nsReturnRef<nsHGLOBAL>();
135   }
136 
137   ::GlobalUnlock(hDevMode);
138 
139   return globalDevMode.out();
140 }
141 
142 //------------------------------------------------------------------
143 // helper
GetDefaultPrinterNameFromGlobalPrinters(nsAString & aPrinterName)144 static void GetDefaultPrinterNameFromGlobalPrinters(nsAString& aPrinterName) {
145   aPrinterName.Truncate();
146   nsCOMPtr<nsIPrinterList> printerList =
147       do_GetService("@mozilla.org/gfx/printerlist;1");
148   if (printerList) {
149     printerList->GetSystemDefaultPrinterName(aPrinterName);
150   }
151 }
152 
153 //------------------------------------------------------------------
154 // Displays the native Print Dialog
ShowNativePrintDialog(HWND aHWnd,nsIPrintSettings * aPrintSettings)155 static nsresult ShowNativePrintDialog(HWND aHWnd,
156                                       nsIPrintSettings* aPrintSettings) {
157   // NS_ENSURE_ARG_POINTER(aHWnd);
158   NS_ENSURE_ARG_POINTER(aPrintSettings);
159 
160   // Get the Print Name to be used
161   nsString printerName;
162   aPrintSettings->GetPrinterName(printerName);
163 
164   // If there is no name then use the default printer
165   if (printerName.IsEmpty()) {
166     GetDefaultPrinterNameFromGlobalPrinters(printerName);
167   } else {
168     HANDLE hPrinter = nullptr;
169     if (!::OpenPrinterW(const_cast<wchar_t*>(
170                             static_cast<const wchar_t*>(printerName.get())),
171                         &hPrinter, nullptr)) {
172       // If the last used printer is not found, we should use default printer.
173       GetDefaultPrinterNameFromGlobalPrinters(printerName);
174     } else {
175       ::ClosePrinter(hPrinter);
176     }
177   }
178 
179   // Now create a DEVNAMES struct so the the dialog is initialized correctly.
180 
181   uint32_t len = printerName.Length();
182   nsHGLOBAL hDevNames =
183       ::GlobalAlloc(GHND, sizeof(wchar_t) * (len + 1) + sizeof(DEVNAMES));
184   nsAutoGlobalMem autoDevNames(hDevNames);
185   if (!hDevNames) {
186     return NS_ERROR_OUT_OF_MEMORY;
187   }
188 
189   DEVNAMES* pDevNames = (DEVNAMES*)::GlobalLock(hDevNames);
190   if (!pDevNames) {
191     return NS_ERROR_FAILURE;
192   }
193   pDevNames->wDriverOffset = sizeof(DEVNAMES) / sizeof(wchar_t);
194   pDevNames->wDeviceOffset = sizeof(DEVNAMES) / sizeof(wchar_t);
195   pDevNames->wOutputOffset = sizeof(DEVNAMES) / sizeof(wchar_t) + len;
196   pDevNames->wDefault = 0;
197 
198   memcpy(pDevNames + 1, printerName.get(), (len + 1) * sizeof(wchar_t));
199   ::GlobalUnlock(hDevNames);
200 
201   // Create a Moveable Memory Object that holds a new DevMode
202   // from the Printer Name
203   // The PRINTDLG.hDevMode requires that it be a moveable memory object
204   // NOTE: autoDevMode is automatically freed when any error occurred
205   nsAutoGlobalMem autoDevMode(
206       CreateGlobalDevModeAndInit(printerName, aPrintSettings));
207 
208   // Prepare to Display the Print Dialog
209   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms646942(v=vs.85)
210   // https://docs.microsoft.com/en-us/windows/win32/api/commdlg/ns-commdlg-printdlgexw
211   PRINTDLGEXW prntdlg;
212   memset(&prntdlg, 0, sizeof(prntdlg));
213 
214   prntdlg.lStructSize = sizeof(prntdlg);
215   prntdlg.hwndOwner = aHWnd;
216   prntdlg.hDevMode = autoDevMode.get();
217   prntdlg.hDevNames = hDevNames;
218   prntdlg.hDC = nullptr;
219   prntdlg.Flags = PD_ALLPAGES | PD_RETURNIC | PD_USEDEVMODECOPIESANDCOLLATE |
220                   PD_COLLATE | PD_NOCURRENTPAGE;
221 
222   // If there is a current selection then enable the "Selection" radio button
223   if (!aPrintSettings->GetIsPrintSelectionRBEnabled()) {
224     prntdlg.Flags |= PD_NOSELECTION;
225   }
226 
227   // 10 seems like a reasonable max number of ranges to support by default if
228   // the user doesn't choose a greater thing in the UI.
229   constexpr size_t kMinSupportedRanges = 10;
230 
231   AutoTArray<PRINTPAGERANGE, kMinSupportedRanges> winPageRanges;
232   // Set up the page ranges.
233   {
234     AutoTArray<int32_t, kMinSupportedRanges * 2> pageRanges;
235     aPrintSettings->GetPageRanges(pageRanges);
236     // If there is a specified page range then enable the "Custom" radio button
237     if (!pageRanges.IsEmpty()) {
238       prntdlg.Flags |= PD_PAGENUMS;
239     }
240 
241     const size_t specifiedRanges = pageRanges.Length() / 2;
242     const size_t maxRanges = std::max(kMinSupportedRanges, specifiedRanges);
243 
244     prntdlg.nMaxPageRanges = maxRanges;
245     prntdlg.nPageRanges = specifiedRanges;
246 
247     winPageRanges.SetCapacity(maxRanges);
248     for (size_t i = 0; i < pageRanges.Length(); i += 2) {
249       PRINTPAGERANGE* range = winPageRanges.AppendElement();
250       range->nFromPage = pageRanges[i];
251       range->nToPage = pageRanges[i + 1];
252     }
253     prntdlg.lpPageRanges = winPageRanges.Elements();
254 
255     prntdlg.nMinPage = 1;
256     // TODO(emilio): Could probably get the right page number here from the
257     // new print UI.
258     prntdlg.nMaxPage = 0xFFFF;
259   }
260 
261   // NOTE(emilio): This can always be 1 because we use the DEVMODE copies
262   // feature (see PD_USEDEVMODECOPIESANDCOLLATE).
263   prntdlg.nCopies = 1;
264 
265   prntdlg.hInstance = nullptr;
266   prntdlg.lpPrintTemplateName = nullptr;
267 
268   prntdlg.lpCallback = nullptr;
269   prntdlg.nPropertyPages = 0;
270   prntdlg.lphPropertyPages = nullptr;
271 
272   prntdlg.nStartPage = START_PAGE_GENERAL;
273   prntdlg.dwResultAction = 0;
274 
275   HRESULT result;
276   {
277     mozilla::widget::WinUtils::AutoSystemDpiAware dpiAwareness;
278     mozilla::BackgroundHangMonitor().NotifyWait();
279     result = ::PrintDlgExW(&prntdlg);
280   }
281 
282   auto cancelOnExit = mozilla::MakeScopeExit([&] {
283     ::SetFocus(aHWnd);
284     aPrintSettings->SetIsCancelled(true);
285   });
286 
287   if (NS_WARN_IF(!SUCCEEDED(result))) {
288 #ifdef DEBUG
289     printf_stderr("PrintDlgExW failed with %x\n", result);
290 #endif
291     return NS_ERROR_FAILURE;
292   }
293   if (NS_WARN_IF(prntdlg.dwResultAction != PD_RESULT_PRINT)) {
294     return NS_ERROR_ABORT;
295   }
296   // check to make sure we don't have any nullptr pointers
297   NS_ENSURE_TRUE(prntdlg.hDevMode, NS_ERROR_ABORT);
298   NS_ENSURE_TRUE(prntdlg.hDevNames, NS_ERROR_ABORT);
299   // Lock the deviceNames and check for nullptr
300   DEVNAMES* devnames = (DEVNAMES*)::GlobalLock(prntdlg.hDevNames);
301   NS_ENSURE_TRUE(devnames, NS_ERROR_ABORT);
302 
303   char16_t* device = &(((char16_t*)devnames)[devnames->wDeviceOffset]);
304   char16_t* driver = &(((char16_t*)devnames)[devnames->wDriverOffset]);
305 
306   // Check to see if the "Print To File" control is checked
307   // then take the name from devNames and set it in the PrintSettings
308   //
309   // NOTE:
310   // As per Microsoft SDK documentation the returned value offset from
311   // devnames->wOutputOffset is either "FILE:" or nullptr
312   // if the "Print To File" checkbox is checked it MUST be "FILE:"
313   // We assert as an extra safety check.
314   if (prntdlg.Flags & PD_PRINTTOFILE) {
315     char16ptr_t fileName = &(((wchar_t*)devnames)[devnames->wOutputOffset]);
316     NS_ASSERTION(wcscmp(fileName, L"FILE:") == 0, "FileName must be `FILE:`");
317     aPrintSettings->SetToFileName(nsDependentString(fileName));
318     aPrintSettings->SetPrintToFile(true);
319   } else {
320     // clear "print to file" info
321     aPrintSettings->SetPrintToFile(false);
322     aPrintSettings->SetToFileName(u""_ns);
323   }
324 
325   nsCOMPtr<nsIPrintSettingsWin> psWin(do_QueryInterface(aPrintSettings));
326   MOZ_RELEASE_ASSERT(psWin);
327 
328   // Setup local Data members
329   psWin->SetDeviceName(nsDependentString(device));
330   psWin->SetDriverName(nsDependentString(driver));
331 
332   // Fill the print options with the info from the dialog
333   aPrintSettings->SetPrinterName(nsDependentString(device));
334   aPrintSettings->SetPrintSelectionOnly(prntdlg.Flags & PD_SELECTION);
335 
336   AutoTArray<int32_t, kMinSupportedRanges * 2> pageRanges;
337   if (prntdlg.Flags & PD_PAGENUMS) {
338     pageRanges.SetCapacity(prntdlg.nPageRanges * 2);
339     for (const auto& range :
340          mozilla::Span(prntdlg.lpPageRanges, prntdlg.nPageRanges)) {
341       pageRanges.AppendElement(range.nFromPage);
342       pageRanges.AppendElement(range.nToPage);
343     }
344   }
345   aPrintSettings->SetPageRanges(pageRanges);
346 
347   // Unlock DeviceNames
348   ::GlobalUnlock(prntdlg.hDevNames);
349 
350   // Transfer the settings from the native data to the PrintSettings
351   LPDEVMODEW devMode = (LPDEVMODEW)::GlobalLock(prntdlg.hDevMode);
352   if (!devMode || !prntdlg.hDC) {
353     return NS_ERROR_FAILURE;
354   }
355   psWin->SetDevMode(devMode);  // copies DevMode
356   psWin->CopyFromNative(prntdlg.hDC, devMode);
357   ::GlobalUnlock(prntdlg.hDevMode);
358   ::DeleteDC(prntdlg.hDC);
359 
360   cancelOnExit.release();
361   return NS_OK;
362 }
363 
364 //----------------------------------------------------------------------------------
365 //-- Show Print Dialog
366 //----------------------------------------------------------------------------------
NativeShowPrintDialog(HWND aHWnd,nsIPrintSettings * aPrintSettings)367 nsresult NativeShowPrintDialog(HWND aHWnd, nsIPrintSettings* aPrintSettings) {
368   nsresult rv = ShowNativePrintDialog(aHWnd, aPrintSettings);
369   if (aHWnd) {
370     ::DestroyWindow(aHWnd);
371   }
372 
373   return rv;
374 }
375