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