Bug 1857361 [wpt PR 42378] - Update wpt metadata, a=testonly
[gecko.git] / widget / nsPrinterCUPS.cpp
bloba8656de23fdeaf97c2695d922fe601aac87f02c6
1 /* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 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/. */
6 #include "nsPrinterCUPS.h"
8 #include "mozilla/gfx/2D.h"
9 #include "mozilla/GkRustUtils.h"
10 #include "mozilla/Preferences.h"
11 #include "mozilla/StaticPrefs_print.h"
12 #include "nsTHashtable.h"
13 #include "nsPaper.h"
14 #include "nsPrinterBase.h"
15 #include "nsPrintSettingsImpl.h"
17 using namespace mozilla;
18 using MarginDouble = mozilla::gfx::MarginDouble;
20 // Requested attributes for IPP requests, just the CUPS version now.
21 static constexpr Array<const char* const, 1> requestedAttributes{
22 "cups-version"};
24 static constexpr double kPointsPerHundredthMillimeter = 72.0 / 2540.0;
26 static PaperInfo MakePaperInfo(const nsAString& aName,
27 const cups_size_t& aMedia) {
28 // XXX Do we actually have the guarantee that this is utf-8?
29 NS_ConvertUTF8toUTF16 paperId(aMedia.media); // internal paper name/ID
30 return PaperInfo(
31 paperId, aName,
32 {aMedia.width * kPointsPerHundredthMillimeter,
33 aMedia.length * kPointsPerHundredthMillimeter},
34 Some(gfx::MarginDouble{aMedia.top * kPointsPerHundredthMillimeter,
35 aMedia.right * kPointsPerHundredthMillimeter,
36 aMedia.bottom * kPointsPerHundredthMillimeter,
37 aMedia.left * kPointsPerHundredthMillimeter}));
40 // Fetches the CUPS version for the print server controlling the printer. This
41 // will only modify the output arguments if the fetch succeeds.
42 static void FetchCUPSVersionForPrinter(const nsCUPSShim& aShim,
43 const cups_dest_t* const aDest,
44 uint64_t& aOutMajor, uint64_t& aOutMinor,
45 uint64_t& aOutPatch) {
46 // Make an IPP request to the server for the printer.
47 const char* const uri = aShim.cupsGetOption(
48 "printer-uri-supported", aDest->num_options, aDest->options);
49 if (!uri) {
50 return;
53 ipp_t* const ippRequest = aShim.ippNewRequest(IPP_OP_GET_PRINTER_ATTRIBUTES);
55 // Set the URI we want to use.
56 aShim.ippAddString(ippRequest, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri",
57 nullptr, uri);
59 // Set the attributes to request.
60 aShim.ippAddStrings(ippRequest, IPP_TAG_OPERATION, IPP_TAG_KEYWORD,
61 "requested-attributes", requestedAttributes.Length,
62 nullptr, &(requestedAttributes[0]));
64 // Use the default HTTP connection to query the CUPS server itself to get
65 // the CUPS version.
66 // Note that cupsDoRequest will delete the request whether it succeeds or
67 // fails, so we should not use ippDelete on it.
68 if (ipp_t* const ippResponse =
69 aShim.cupsDoRequest(CUPS_HTTP_DEFAULT, ippRequest, "/")) {
70 ipp_attribute_t* const versionAttrib =
71 aShim.ippFindAttribute(ippResponse, "cups-version", IPP_TAG_TEXT);
72 if (versionAttrib && aShim.ippGetCount(versionAttrib) == 1) {
73 const char* versionString = aShim.ippGetString(versionAttrib, 0, nullptr);
74 MOZ_ASSERT(versionString);
75 // On error, GkRustUtils::ParseSemVer will not modify its arguments.
76 GkRustUtils::ParseSemVer(
77 nsDependentCSubstring{MakeStringSpan(versionString)}, aOutMajor,
78 aOutMinor, aOutPatch);
80 aShim.ippDelete(ippResponse);
84 nsPrinterCUPS::~nsPrinterCUPS() {
85 PrinterInfoLock lock = mPrinterInfoMutex.Lock();
86 if (lock->mPrinterInfo) {
87 mShim.cupsFreeDestInfo(lock->mPrinterInfo);
89 if (lock->mPrinter) {
90 mShim.cupsFreeDests(1, lock->mPrinter);
94 NS_IMETHODIMP
95 nsPrinterCUPS::GetName(nsAString& aName) {
96 GetPrinterName(aName);
97 return NS_OK;
100 NS_IMETHODIMP
101 nsPrinterCUPS::GetSystemName(nsAString& aName) {
102 PrinterInfoLock lock = mPrinterInfoMutex.Lock();
103 CopyUTF8toUTF16(MakeStringSpan(lock->mPrinter->name), aName);
104 return NS_OK;
107 void nsPrinterCUPS::GetPrinterName(nsAString& aName) const {
108 if (mDisplayName.IsEmpty()) {
109 aName.Truncate();
110 PrinterInfoLock lock = mPrinterInfoMutex.Lock();
111 CopyUTF8toUTF16(MakeStringSpan(lock->mPrinter->name), aName);
112 } else {
113 aName = mDisplayName;
117 const char* nsPrinterCUPS::LocalizeMediaName(http_t& aConnection,
118 cups_size_t& aMedia) const {
119 // The returned string is owned by mPrinterInfo.
120 // https://www.cups.org/doc/cupspm.html#cupsLocalizeDestMedia
121 if (!mShim.cupsLocalizeDestMedia) {
122 return aMedia.media;
124 PrinterInfoLock lock = TryEnsurePrinterInfo();
125 return mShim.cupsLocalizeDestMedia(&aConnection, lock->mPrinter,
126 lock->mPrinterInfo,
127 CUPS_MEDIA_FLAGS_DEFAULT, &aMedia);
130 bool nsPrinterCUPS::SupportsDuplex() const {
131 return Supports(CUPS_SIDES, CUPS_SIDES_TWO_SIDED_PORTRAIT);
134 bool nsPrinterCUPS::SupportsMonochrome() const {
135 if (!SupportsColor()) {
136 return true;
138 return StaticPrefs::print_cups_monochrome_enabled();
141 bool nsPrinterCUPS::SupportsColor() const {
142 // CUPS 2.1 (particularly as used in Ubuntu 16) is known to have inaccurate
143 // results for CUPS_PRINT_COLOR_MODE.
144 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1660658#c15
145 if (!IsCUPSVersionAtLeast(2, 2, 0)) {
146 return true; // See comment for PrintSettingsInitializer.mPrintInColor
148 return Supports(CUPS_PRINT_COLOR_MODE, CUPS_PRINT_COLOR_MODE_AUTO) ||
149 Supports(CUPS_PRINT_COLOR_MODE, CUPS_PRINT_COLOR_MODE_COLOR) ||
150 !Supports(CUPS_PRINT_COLOR_MODE, CUPS_PRINT_COLOR_MODE_MONOCHROME);
153 bool nsPrinterCUPS::SupportsCollation() const {
154 // We can't depend on cupsGetIntegerOption existing.
155 PrinterInfoLock lock = mPrinterInfoMutex.Lock();
156 const char* const value = FindCUPSOption(lock, "printer-type");
157 if (!value) {
158 return false;
160 // If the value is non-numeric, then atoi will return 0, which will still
161 // cause this function to return false.
162 const int type = atoi(value);
163 return type & CUPS_PRINTER_COLLATE;
166 nsPrinterBase::PrinterInfo nsPrinterCUPS::CreatePrinterInfo() const {
167 Connection connection{mShim};
168 return PrinterInfo{PaperList(connection), DefaultSettings(connection)};
171 bool nsPrinterCUPS::Supports(const char* aOption, const char* aValue) const {
172 PrinterInfoLock lock = TryEnsurePrinterInfo();
173 return mShim.cupsCheckDestSupported(CUPS_HTTP_DEFAULT, lock->mPrinter,
174 lock->mPrinterInfo, aOption, aValue);
177 bool nsPrinterCUPS::IsCUPSVersionAtLeast(uint64_t aCUPSMajor,
178 uint64_t aCUPSMinor,
179 uint64_t aCUPSPatch) const {
180 PrinterInfoLock lock = TryEnsurePrinterInfo();
181 // Compare major version.
182 if (lock->mCUPSMajor > aCUPSMajor) {
183 return true;
185 if (lock->mCUPSMajor < aCUPSMajor) {
186 return false;
189 // Compare minor version.
190 if (lock->mCUPSMinor > aCUPSMinor) {
191 return true;
193 if (lock->mCUPSMinor < aCUPSMinor) {
194 return false;
197 // Compare patch.
198 return aCUPSPatch <= lock->mCUPSPatch;
201 http_t* nsPrinterCUPS::Connection::GetConnection(cups_dest_t* aDest) {
202 if (mWasInited) {
203 return mConnection;
205 mWasInited = true;
207 // blocking call
208 http_t* const connection = mShim.cupsConnectDest(aDest, CUPS_DEST_FLAGS_NONE,
209 /* timeout(ms) */ 5000,
210 /* cancel */ nullptr,
211 /* resource */ nullptr,
212 /* resourcesize */ 0,
213 /* callback */ nullptr,
214 /* user_data */ nullptr);
215 if (connection) {
216 mConnection = connection;
218 return mConnection;
221 nsPrinterCUPS::Connection::~Connection() {
222 if (mWasInited && mConnection) {
223 mShim.httpClose(mConnection);
227 PrintSettingsInitializer nsPrinterCUPS::DefaultSettings(
228 Connection& aConnection) const {
229 nsString printerName;
230 GetPrinterName(printerName);
231 PrinterInfoLock lock = TryEnsurePrinterInfo();
233 cups_size_t media;
235 bool hasDefaultMedia = false;
236 // cupsGetDestMediaDefault appears to return more accurate defaults on macOS,
237 // and the IPP attribute appears to return more accurate defaults on Linux.
238 #ifdef XP_MACOSX
239 hasDefaultMedia = mShim.cupsGetDestMediaDefault(
240 CUPS_HTTP_DEFAULT, lock->mPrinter, lock->mPrinterInfo,
241 CUPS_MEDIA_FLAGS_DEFAULT, &media);
242 #else
244 ipp_attribute_t* defaultMediaIPP =
245 mShim.cupsFindDestDefault
246 ? mShim.cupsFindDestDefault(CUPS_HTTP_DEFAULT, lock->mPrinter,
247 lock->mPrinterInfo, "media")
248 : nullptr;
250 const char* defaultMediaName =
251 defaultMediaIPP ? mShim.ippGetString(defaultMediaIPP, 0, nullptr)
252 : nullptr;
254 hasDefaultMedia = defaultMediaName &&
255 mShim.cupsGetDestMediaByName(
256 CUPS_HTTP_DEFAULT, lock->mPrinter, lock->mPrinterInfo,
257 defaultMediaName, CUPS_MEDIA_FLAGS_DEFAULT, &media);
259 #endif
261 if (!hasDefaultMedia) {
262 return PrintSettingsInitializer{
263 std::move(printerName),
264 PaperInfo(),
265 SupportsColor(),
269 // Check if this is a localized fallback paper size, in which case we can
270 // avoid using the CUPS localization methods.
271 const gfx::SizeDouble sizeDouble{
272 media.width * kPointsPerHundredthMillimeter,
273 media.length * kPointsPerHundredthMillimeter};
274 if (const PaperInfo* const paperInfo = FindCommonPaperSize(sizeDouble)) {
275 return PrintSettingsInitializer{
276 std::move(printerName),
277 MakePaperInfo(paperInfo->mName, media),
278 SupportsColor(),
282 http_t* const connection = aConnection.GetConnection(lock->mPrinter);
283 // XXX Do we actually have the guarantee that this is utf-8?
284 NS_ConvertUTF8toUTF16 localizedName{
285 connection ? LocalizeMediaName(*connection, media) : ""};
287 return PrintSettingsInitializer{
288 std::move(printerName),
289 MakePaperInfo(localizedName, media),
290 SupportsColor(),
294 nsTArray<mozilla::PaperInfo> nsPrinterCUPS::PaperList(
295 Connection& aConnection) const {
296 PrinterInfoLock lock = mPrinterInfoMutex.Lock();
297 http_t* const connection = aConnection.GetConnection(lock->mPrinter);
298 TryEnsurePrinterInfo(lock, connection);
300 if (!lock->mPrinterInfo) {
301 return {};
304 const int paperCount = mShim.cupsGetDestMediaCount
305 ? mShim.cupsGetDestMediaCount(
306 connection, lock->mPrinter,
307 lock->mPrinterInfo, CUPS_MEDIA_FLAGS_DEFAULT)
308 : 0;
309 nsTArray<PaperInfo> paperList;
310 nsTHashtable<nsCharPtrHashKey> paperSet(std::max(paperCount, 0));
312 paperList.SetCapacity(paperCount);
313 for (int i = 0; i < paperCount; ++i) {
314 cups_size_t media;
315 const int getInfoSucceeded = mShim.cupsGetDestMediaByIndex(
316 connection, lock->mPrinter, lock->mPrinterInfo, i,
317 CUPS_MEDIA_FLAGS_DEFAULT, &media);
319 if (!getInfoSucceeded || !paperSet.EnsureInserted(media.media)) {
320 continue;
322 // Check if this is a PWG paper size, in which case we can avoid using the
323 // CUPS localization methods.
324 const gfx::SizeDouble sizeDouble{
325 media.width * kPointsPerHundredthMillimeter,
326 media.length * kPointsPerHundredthMillimeter};
327 if (const PaperInfo* const paperInfo = FindCommonPaperSize(sizeDouble)) {
328 paperList.AppendElement(MakePaperInfo(paperInfo->mName, media));
329 } else {
330 const char* const mediaName =
331 connection ? LocalizeMediaName(*connection, media) : media.media;
332 paperList.AppendElement(
333 MakePaperInfo(NS_ConvertUTF8toUTF16(mediaName), media));
337 return paperList;
340 void nsPrinterCUPS::TryEnsurePrinterInfo(PrinterInfoLock& aLock,
341 http_t* const aConnection) const {
342 if (aLock->mPrinterInfo ||
343 (aConnection == CUPS_HTTP_DEFAULT ? aLock->mTriedInitWithDefault
344 : aLock->mTriedInitWithConnection)) {
345 return;
348 if (aConnection == CUPS_HTTP_DEFAULT) {
349 aLock->mTriedInitWithDefault = true;
350 } else {
351 aLock->mTriedInitWithConnection = true;
354 MOZ_ASSERT(aLock->mPrinter);
356 // httpGetAddress was only added in CUPS 2.0, and some systems still use
357 // CUPS 1.7.
358 if (aConnection && MOZ_LIKELY(mShim.httpGetAddress && mShim.httpAddrPort)) {
359 // This is a workaround for the CUPS Bug seen in bug 1691347.
360 // This is to avoid a null string being passed to strstr in CUPS. The path
361 // in CUPS that leads to this is as follows:
363 // In cupsCopyDestInfo, CUPS_DEST_FLAG_DEVICE is set when the connection is
364 // not null (same as CUPS_HTTP_DEFAULT), the print server is not the same
365 // as our hostname and is not path-based (starts with a '/'), or the IPP
366 // port is different than the global server IPP port.
368 // https://github.com/apple/cups/blob/c9da6f63b263faef5d50592fe8cf8056e0a58aa2/cups/dest-options.c#L718-L722
370 // In _cupsGetDestResource, CUPS fetches the IPP options "device-uri" and
371 // "printer-uri-supported". Note that IPP options are returned as null when
372 // missing.
374 // https://github.com/apple/cups/blob/23c45db76a8520fd6c3b1d9164dbe312f1ab1481/cups/dest.c#L1138-L1141
376 // If the CUPS_DEST_FLAG_DEVICE is set or the "printer-uri-supported"
377 // option is not set, CUPS checks for "._tcp" in the "device-uri" option
378 // without doing a NULL-check first.
380 // https://github.com/apple/cups/blob/23c45db76a8520fd6c3b1d9164dbe312f1ab1481/cups/dest.c#L1144
382 // If we find that those branches will be taken, don't actually fetch the
383 // CUPS data and instead just return an empty printer info.
385 const char* const serverNameBytes = mShim.cupsServer();
387 if (MOZ_LIKELY(serverNameBytes)) {
388 const nsDependentCString serverName{serverNameBytes};
390 // We only need enough characters to determine equality with serverName.
391 // + 2 because we need one byte for the null-character, and we also want
392 // to get more characters of the host name than the server name if
393 // possible. Otherwise, if the hostname starts with the same text as the
394 // entire server name, it would compare equal when it's not.
395 const size_t hostnameMemLength = serverName.Length() + 2;
396 auto hostnameMem = MakeUnique<char[]>(hostnameMemLength);
398 // We don't expect httpGetHostname to return null when a connection is
399 // passed, but it's better not to make assumptions.
400 const char* const hostnameBytes = mShim.httpGetHostname(
401 aConnection, hostnameMem.get(), hostnameMemLength);
403 if (MOZ_LIKELY(hostnameBytes)) {
404 const nsDependentCString hostname{hostnameBytes};
406 // Attempt to match the condional at
407 // https://github.com/apple/cups/blob/c9da6f63b263faef5d50592fe8cf8056e0a58aa2/cups/dest-options.c#L718
409 // To find the result of the comparison CUPS performs of
410 // `strcmp(http->hostname, cg->server)`, we use httpGetHostname to try
411 // to get the value of `http->hostname`, but this isn't quite the same.
412 // For local addresses, httpGetHostName will normalize the result to be
413 // localhost", rather than the actual value of `http->hostname`.
415 // https://github.com/apple/cups/blob/2201569857f225c9874bfae19713ffb2f4bdfdeb/cups/http-addr.c#L794-L818
417 // Because of this, if both serverName and hostname equal "localhost",
418 // then the actual hostname might be a different local address that CUPS
419 // normalized in httpGetHostName, and `http->hostname` won't be equal to
420 // `cg->server` in CUPS.
421 const bool namesMightNotMatch =
422 hostname != serverName || hostname == "localhost";
423 const bool portsDiffer =
424 mShim.httpAddrPort(mShim.httpGetAddress(aConnection)) !=
425 mShim.ippPort();
426 const bool cupsDestDeviceFlag =
427 (namesMightNotMatch && serverName[0] != '/') || portsDiffer;
429 // Match the conditional at
430 // https://github.com/apple/cups/blob/23c45db76a8520fd6c3b1d9164dbe312f1ab1481/cups/dest.c#L1144
431 // but if device-uri is null do not call into CUPS.
432 if ((cupsDestDeviceFlag ||
433 !FindCUPSOption(aLock, "printer-uri-supported")) &&
434 !FindCUPSOption(aLock, "device-uri")) {
435 return;
441 // All CUPS calls that take the printer info do null-checks internally, so we
442 // can fetch this info and only worry about the result of the later CUPS
443 // functions.
444 aLock->mPrinterInfo = mShim.cupsCopyDestInfo(aConnection, aLock->mPrinter);
446 // Even if we failed to fetch printer info, it is still possible we can talk
447 // to the print server and get its CUPS version.
448 FetchCUPSVersionForPrinter(mShim, aLock->mPrinter, aLock->mCUPSMajor,
449 aLock->mCUPSMinor, aLock->mCUPSPatch);
452 void nsPrinterCUPS::ForEachExtraMonochromeSetting(
453 FunctionRef<void(const nsACString&, const nsACString&)> aCallback) {
454 nsAutoCString pref;
455 Preferences::GetCString("print.cups.monochrome.extra_settings", pref);
456 if (pref.IsEmpty()) {
457 return;
460 for (const auto& pair : pref.Split(',')) {
461 auto splitter = pair.Split(':');
462 auto end = splitter.end();
464 auto key = splitter.begin();
465 if (key == end) {
466 continue;
469 auto value = ++splitter.begin();
470 if (value == end) {
471 continue;
474 aCallback(*key, *value);