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 #include "nsDeviceContextSpecG.h"
7 
8 #include "mozilla/gfx/PrintTargetPDF.h"
9 #include "mozilla/gfx/PrintTargetPS.h"
10 #include "mozilla/Logging.h"
11 #include "mozilla/Services.h"
12 
13 #include "plstr.h"
14 #include "prenv.h" /* for PR_GetEnv */
15 
16 #include "nsComponentManagerUtils.h"
17 #include "nsIObserverService.h"
18 #include "nsPrintfCString.h"
19 #include "nsReadableUtils.h"
20 #include "nsThreadUtils.h"
21 
22 #include "nsCUPSShim.h"
23 #include "nsPrinterCUPS.h"
24 
25 #include "nsPrintSettingsGTK.h"
26 
27 #include "nsIFileStreams.h"
28 #include "nsIFile.h"
29 #include "nsTArray.h"
30 #include "nsThreadUtils.h"
31 
32 #include "mozilla/Preferences.h"
33 #include "mozilla/StaticPrefs_print.h"
34 
35 #include <dlfcn.h>
36 #include <unistd.h>
37 #include <sys/types.h>
38 #include <sys/stat.h>
39 #include <fcntl.h>
40 
41 // To check if we need to use flatpak portal for printing
42 #include "nsIGIOService.h"
43 
44 using namespace mozilla;
45 
46 using mozilla::gfx::IntSize;
47 using mozilla::gfx::PrintTarget;
48 using mozilla::gfx::PrintTargetPDF;
49 using mozilla::gfx::PrintTargetPS;
50 
nsDeviceContextSpecGTK()51 nsDeviceContextSpecGTK::nsDeviceContextSpecGTK()
52     : mGtkPrintSettings(nullptr), mGtkPageSetup(nullptr) {}
53 
~nsDeviceContextSpecGTK()54 nsDeviceContextSpecGTK::~nsDeviceContextSpecGTK() {
55   if (mGtkPageSetup) {
56     g_object_unref(mGtkPageSetup);
57   }
58 
59   if (mGtkPrintSettings) {
60     g_object_unref(mGtkPrintSettings);
61   }
62 
63   if (mSpoolFile) {
64     mSpoolFile->Remove(false);
65   }
66 }
67 
NS_IMPL_ISUPPORTS(nsDeviceContextSpecGTK,nsIDeviceContextSpec)68 NS_IMPL_ISUPPORTS(nsDeviceContextSpecGTK, nsIDeviceContextSpec)
69 
70 already_AddRefed<PrintTarget> nsDeviceContextSpecGTK::MakePrintTarget() {
71   double width, height;
72   mPrintSettings->GetEffectiveSheetSize(&width, &height);
73 
74   // convert twips to points
75   width /= TWIPS_PER_POINT_FLOAT;
76   height /= TWIPS_PER_POINT_FLOAT;
77 
78   nsresult rv;
79 
80   // We shouldn't be attempting to get a surface if we've already got a spool
81   // file.
82   MOZ_ASSERT(!mSpoolFile);
83 
84   // Spool file. Use Glib's temporary file function since we're
85   // already dependent on the gtk software stack.
86   gchar* buf;
87   gint fd = g_file_open_tmp("XXXXXX.tmp", &buf, nullptr);
88   if (-1 == fd) return nullptr;
89   close(fd);
90 
91   rv = NS_NewNativeLocalFile(nsDependentCString(buf), false,
92                              getter_AddRefs(mSpoolFile));
93   if (NS_FAILED(rv)) {
94     unlink(buf);
95     g_free(buf);
96     return nullptr;
97   }
98 
99   mSpoolName = buf;
100   g_free(buf);
101 
102   mSpoolFile->SetPermissions(0600);
103 
104   nsCOMPtr<nsIFileOutputStream> stream =
105       do_CreateInstance("@mozilla.org/network/file-output-stream;1");
106   rv = stream->Init(mSpoolFile, -1, -1, 0);
107   if (NS_FAILED(rv)) return nullptr;
108 
109   int16_t format;
110   mPrintSettings->GetOutputFormat(&format);
111 
112   // We assume PDF output if asked for native output.
113   if (format == nsIPrintSettings::kOutputFormatNative) {
114     format = nsIPrintSettings::kOutputFormatPDF;
115   }
116 
117   IntSize size = IntSize::Ceil(width, height);
118   if (format == nsIPrintSettings::kOutputFormatPDF) {
119     return PrintTargetPDF::CreateOrNull(stream, size);
120   }
121 
122   int32_t orientation = mPrintSettings->GetSheetOrientation();
123   return PrintTargetPS::CreateOrNull(
124       stream, size,
125       orientation == nsIPrintSettings::kPortraitOrientation
126           ? PrintTargetPS::PORTRAIT
127           : PrintTargetPS::LANDSCAPE);
128 }
129 
130 #define DECLARE_KNOWN_MONOCHROME_SETTING(key_, value_) {"cups-" key_, value_},
131 
132 struct {
133   const char* mKey;
134   const char* mValue;
135 } kKnownMonochromeSettings[] = {
136     CUPS_EACH_MONOCHROME_PRINTER_SETTING(DECLARE_KNOWN_MONOCHROME_SETTING)};
137 
138 #undef DECLARE_KNOWN_MONOCHROME_SETTING
139 
140 // https://developer.gnome.org/gtk3/stable/GtkPaperSize.html#gtk-paper-size-new-from-ipp
GtkPaperSizeFromIpp(const gchar * aIppName,gdouble aWidth,gdouble aHeight)141 static GtkPaperSize* GtkPaperSizeFromIpp(const gchar* aIppName, gdouble aWidth,
142                                          gdouble aHeight) {
143   static auto sPtr = (GtkPaperSize * (*)(const gchar*, gdouble, gdouble))
144       dlsym(RTLD_DEFAULT, "gtk_paper_size_new_from_ipp");
145   if (gtk_check_version(3, 16, 0)) {
146     return nullptr;
147   }
148   return sPtr(aIppName, aWidth, aHeight);
149 }
150 
PaperSizeAlmostEquals(GtkPaperSize * aSize,GtkPaperSize * aOtherSize)151 static bool PaperSizeAlmostEquals(GtkPaperSize* aSize,
152                                   GtkPaperSize* aOtherSize) {
153   const double kEpsilon = 1.0;  // millimetres
154   // GTK stores sizes internally in millimetres so just use that.
155   if (fabs(gtk_paper_size_get_height(aSize, GTK_UNIT_MM) -
156            gtk_paper_size_get_height(aOtherSize, GTK_UNIT_MM)) > kEpsilon) {
157     return false;
158   }
159   if (fabs(gtk_paper_size_get_width(aSize, GTK_UNIT_MM) -
160            gtk_paper_size_get_width(aOtherSize, GTK_UNIT_MM)) > kEpsilon) {
161     return false;
162   }
163   return true;
164 }
165 
166 // This is a horrible workaround for some printer driver bugs that treat
167 // custom page sizes different to standard ones. If our paper object matches
168 // one of a standard one, use a standard paper size object instead.
169 //
170 // See bug 414314 and bug 1691798 for more info.
GetStandardGtkPaperSize(GtkPaperSize * aGeckoPaperSize)171 static GtkPaperSize* GetStandardGtkPaperSize(GtkPaperSize* aGeckoPaperSize) {
172   const gchar* geckoName = gtk_paper_size_get_name(aGeckoPaperSize);
173 
174   // We try ipp size first because that's the names we get from CUPS, and
175   // because even though gtk_paper_size_new deals with ipp, it has rounding
176   // issues, see https://gitlab.gnome.org/GNOME/gtk/-/issues/3685.
177   GtkPaperSize* size = GtkPaperSizeFromIpp(
178       geckoName, gtk_paper_size_get_width(aGeckoPaperSize, GTK_UNIT_POINTS),
179       gtk_paper_size_get_height(aGeckoPaperSize, GTK_UNIT_POINTS));
180   if (size && !gtk_paper_size_is_custom(size)) {
181     return size;
182   }
183 
184   if (size) {
185     gtk_paper_size_free(size);
186   }
187 
188   size = gtk_paper_size_new(geckoName);
189   if (gtk_paper_size_is_equal(size, aGeckoPaperSize)) {
190     return size;
191   }
192 
193   // gtk_paper_size_is_equal compares just paper names. The name in Gecko
194   // might come from CUPS, which is an ipp size, and gets normalized by gtk.
195   //
196   // So check also for the same actual paper size.
197   if (PaperSizeAlmostEquals(aGeckoPaperSize, size)) {
198     return size;
199   }
200 
201   // Not the same after all, so use our custom paper size.
202   gtk_paper_size_free(size);
203   return nullptr;
204 }
205 
206 /** -------------------------------------------------------
207  *  Initialize the nsDeviceContextSpecGTK
208  *  @update   dc 2/15/98
209  *  @update   syd 3/2/99
210  */
Init(nsIWidget * aWidget,nsIPrintSettings * aPS,bool aIsPrintPreview)211 NS_IMETHODIMP nsDeviceContextSpecGTK::Init(nsIWidget* aWidget,
212                                            nsIPrintSettings* aPS,
213                                            bool aIsPrintPreview) {
214   mPrintSettings = do_QueryInterface(aPS);
215   if (!mPrintSettings) {
216     return NS_ERROR_NO_INTERFACE;
217   }
218 
219   // This is only set by embedders
220   bool toFile;
221   aPS->GetPrintToFile(&toFile);
222 
223   mToPrinter = !toFile && !aIsPrintPreview;
224 
225   mGtkPrintSettings = mPrintSettings->GetGtkPrintSettings();
226   mGtkPageSetup = mPrintSettings->GetGtkPageSetup();
227 
228   GtkPaperSize* geckoPaperSize = gtk_page_setup_get_paper_size(mGtkPageSetup);
229   GtkPaperSize* gtkPaperSize = GetStandardGtkPaperSize(geckoPaperSize);
230 
231   mGtkPageSetup = gtk_page_setup_copy(mGtkPageSetup);
232   mGtkPrintSettings = gtk_print_settings_copy(mGtkPrintSettings);
233 
234   if (!aPS->GetPrintInColor() && StaticPrefs::print_cups_monochrome_enabled()) {
235     for (const auto& setting : kKnownMonochromeSettings) {
236       gtk_print_settings_set(mGtkPrintSettings, setting.mKey, setting.mValue);
237     }
238     auto applySetting = [&](const nsACString& aKey, const nsACString& aVal) {
239       nsAutoCString extra;
240       extra.AppendASCII("cups-");
241       extra.Append(aKey);
242       gtk_print_settings_set(mGtkPrintSettings, extra.get(),
243                              nsAutoCString(aVal).get());
244     };
245     nsPrinterCUPS::ForEachExtraMonochromeSetting(applySetting);
246   }
247 
248   GtkPaperSize* properPaperSize = gtkPaperSize ? gtkPaperSize : geckoPaperSize;
249   gtk_print_settings_set_paper_size(mGtkPrintSettings, properPaperSize);
250   gtk_page_setup_set_paper_size_and_default_margins(mGtkPageSetup,
251                                                     properPaperSize);
252   if (gtkPaperSize) {
253     gtk_paper_size_free(gtkPaperSize);
254   }
255 
256   return NS_OK;
257 }
258 
print_callback(GtkPrintJob * aJob,gpointer aData,const GError * aError)259 static void print_callback(GtkPrintJob* aJob, gpointer aData,
260                            const GError* aError) {
261   g_object_unref(aJob);
262   ((nsIFile*)aData)->Remove(false);
263 }
264 
265 /* static */
PrinterEnumerator(GtkPrinter * aPrinter,gpointer aData)266 gboolean nsDeviceContextSpecGTK::PrinterEnumerator(GtkPrinter* aPrinter,
267                                                    gpointer aData) {
268   nsDeviceContextSpecGTK* spec = (nsDeviceContextSpecGTK*)aData;
269 
270   // Find the printer whose name matches the one inside the settings.
271   nsString printerName;
272   nsresult rv = spec->mPrintSettings->GetPrinterName(printerName);
273   if (NS_SUCCEEDED(rv) && !printerName.IsVoid()) {
274     NS_ConvertUTF16toUTF8 requestedName(printerName);
275     const char* currentName = gtk_printer_get_name(aPrinter);
276     if (requestedName.Equals(currentName)) {
277       spec->mPrintSettings->SetGtkPrinter(aPrinter);
278 
279       // Bug 1145916 - attempting to kick off a print job for this printer
280       // during this tick of the event loop will result in the printer backend
281       // misunderstanding what the capabilities of the printer are due to a
282       // GTK bug (https://bugzilla.gnome.org/show_bug.cgi?id=753041). We
283       // sidestep this by deferring the print to the next tick.
284       NS_DispatchToCurrentThread(
285           NewRunnableMethod("nsDeviceContextSpecGTK::StartPrintJob", spec,
286                             &nsDeviceContextSpecGTK::StartPrintJob));
287       return TRUE;
288     }
289   }
290 
291   // We haven't found it yet - keep searching...
292   return FALSE;
293 }
294 
StartPrintJob()295 void nsDeviceContextSpecGTK::StartPrintJob() {
296   // When using flatpak, we have to call the Print method of the portal
297   nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
298   bool shouldUsePortal;
299   giovfs->ShouldUseFlatpakPortal(&shouldUsePortal);
300   if (shouldUsePortal) {
301     GError* error = nullptr;
302     GDBusProxy* dbusProxy = g_dbus_proxy_new_for_bus_sync(
303         G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, nullptr,
304         "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop",
305         "org.freedesktop.portal.Print", nullptr, &error);
306     if (dbusProxy == nullptr) {
307       NS_WARNING(
308           nsPrintfCString("Unable to create dbus proxy: %s", error->message)
309               .get());
310       g_error_free(error);
311       return;
312     }
313     int fd = open(mSpoolName.get(), O_RDONLY | O_CLOEXEC);
314     if (fd == -1) {
315       NS_WARNING("Failed to open spool file.");
316       return;
317     }
318     static auto s_g_unix_fd_list_new = reinterpret_cast<GUnixFDList* (*)(void)>(
319         dlsym(RTLD_DEFAULT, "g_unix_fd_list_new"));
320     NS_ASSERTION(s_g_unix_fd_list_new,
321                  "Cannot find g_unix_fd_list_new function.");
322 
323     GUnixFDList* fd_list = s_g_unix_fd_list_new();
324     static auto s_g_unix_fd_list_append =
325         reinterpret_cast<gint (*)(GUnixFDList*, gint, GError**)>(
326             dlsym(RTLD_DEFAULT, "g_unix_fd_list_append"));
327     int idx = s_g_unix_fd_list_append(fd_list, fd, NULL);
328     close(fd);
329 
330     // We'll pass empty options as long as we don't have token from PreparePrint
331     // dbus call (which we don't use). This unfortunatelly lead to showing
332     // gtk print dialog and also the duplex or printer specific settings
333     // is not honored, so this needs to be fixed when the portal provides
334     // more options.
335     GVariantBuilder opt_builder;
336     g_variant_builder_init(&opt_builder, G_VARIANT_TYPE_VARDICT);
337 
338     g_dbus_proxy_call_with_unix_fd_list(
339         dbusProxy, "Print",
340         g_variant_new("(ssh@a{sv})", "", /* window */
341                       "Print",           /* title */
342                       idx, g_variant_builder_end(&opt_builder)),
343         G_DBUS_CALL_FLAGS_NONE, -1, fd_list, NULL,
344         NULL,      // portal result cb function
345         nullptr);  // userdata
346     g_object_unref(fd_list);
347     g_object_unref(dbusProxy);
348   } else {
349     GtkPrintJob* job =
350         gtk_print_job_new(mTitle.get(), mPrintSettings->GetGtkPrinter(),
351                           mGtkPrintSettings, mGtkPageSetup);
352 
353     if (!gtk_print_job_set_source_file(job, mSpoolName.get(), nullptr)) return;
354 
355     // Now gtk owns the print job, and will be released via our callback.
356     gtk_print_job_send(job, print_callback, mSpoolFile.forget().take(),
357                        [](gpointer aData) {
358                          auto* spoolFile = static_cast<nsIFile*>(aData);
359                          NS_RELEASE(spoolFile);
360                        });
361   }
362 }
363 
EnumeratePrinters()364 void nsDeviceContextSpecGTK::EnumeratePrinters() {
365   gtk_enumerate_printers(&nsDeviceContextSpecGTK::PrinterEnumerator, this,
366                          nullptr, TRUE);
367 }
368 
369 NS_IMETHODIMP
BeginDocument(const nsAString & aTitle,const nsAString & aPrintToFileName,int32_t aStartPage,int32_t aEndPage)370 nsDeviceContextSpecGTK::BeginDocument(const nsAString& aTitle,
371                                       const nsAString& aPrintToFileName,
372                                       int32_t aStartPage, int32_t aEndPage) {
373   // Print job names exceeding 255 bytes are safe with GTK version 3.18.2 or
374   // newer. This is a workaround for old GTK.
375   if (gtk_check_version(3, 18, 2) != nullptr) {
376     PrintTarget::AdjustPrintJobNameForIPP(aTitle, mTitle);
377   } else {
378     CopyUTF16toUTF8(aTitle, mTitle);
379   }
380 
381   return NS_OK;
382 }
383 
EndDocument()384 NS_IMETHODIMP nsDeviceContextSpecGTK::EndDocument() {
385   if (mToPrinter) {
386     // At this point, we might have a GtkPrinter set up in nsPrintSettingsGTK,
387     // or we might not. In the single-process case, we probably will, as this
388     // is populated by the print settings dialog, or set to the default
389     // printer.
390     // In the multi-process case, we proxy the print settings dialog over to
391     // the parent process, and only get the name of the printer back on the
392     // content process side. In that case, we need to enumerate the printers
393     // on the content side, and find a printer with a matching name.
394 
395     if (mPrintSettings->GetGtkPrinter()) {
396       // We have a printer, so we can print right away.
397       StartPrintJob();
398     } else {
399       // We don't have a printer. We have to enumerate the printers and find
400       // one with a matching name.
401       NS_DispatchToCurrentThread(
402           NewRunnableMethod("nsDeviceContextSpecGTK::EnumeratePrinters", this,
403                             &nsDeviceContextSpecGTK::EnumeratePrinters));
404     }
405   } else {
406     // Handle print-to-file ourselves for the benefit of embedders
407     nsString targetPath;
408     nsCOMPtr<nsIFile> destFile;
409     mPrintSettings->GetToFileName(targetPath);
410 
411     nsresult rv = NS_NewLocalFile(targetPath, false, getter_AddRefs(destFile));
412     NS_ENSURE_SUCCESS(rv, rv);
413 
414     nsAutoString destLeafName;
415     rv = destFile->GetLeafName(destLeafName);
416     NS_ENSURE_SUCCESS(rv, rv);
417 
418     nsCOMPtr<nsIFile> destDir;
419     rv = destFile->GetParent(getter_AddRefs(destDir));
420     NS_ENSURE_SUCCESS(rv, rv);
421 
422     rv = mSpoolFile->MoveTo(destDir, destLeafName);
423     NS_ENSURE_SUCCESS(rv, rv);
424 
425     mSpoolFile = nullptr;
426 
427     // This is the standard way to get the UNIX umask. Ugh.
428     mode_t mask = umask(0);
429     umask(mask);
430     // If you're not familiar with umasks, they contain the bits of what NOT
431     // to set in the permissions (thats because files and directories have
432     // different numbers of bits for their permissions)
433     destFile->SetPermissions(0666 & ~(mask));
434 
435     // Notify flatpak printing portal that file is completely written
436     nsCOMPtr<nsIGIOService> giovfs = do_GetService(NS_GIOSERVICE_CONTRACTID);
437     bool shouldUsePortal;
438     if (giovfs) {
439       giovfs->ShouldUseFlatpakPortal(&shouldUsePortal);
440       if (shouldUsePortal) {
441         // Use the name of the file for printing to match with
442         // nsFlatpakPrintPortal
443         nsCOMPtr<nsIObserverService> os =
444             mozilla::services::GetObserverService();
445         // Pass filename to be sure that observer process the right data
446         os->NotifyObservers(nullptr, "print-to-file-finished",
447                             targetPath.get());
448       }
449     }
450   }
451   return NS_OK;
452 }
453