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"
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
{
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
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
);
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",
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
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
);
90 mShim
.cupsFreeDests(1, lock
->mPrinter
);
95 nsPrinterCUPS::GetName(nsAString
& aName
) {
96 GetPrinterName(aName
);
101 nsPrinterCUPS::GetSystemName(nsAString
& aName
) {
102 PrinterInfoLock lock
= mPrinterInfoMutex
.Lock();
103 CopyUTF8toUTF16(MakeStringSpan(lock
->mPrinter
->name
), aName
);
107 void nsPrinterCUPS::GetPrinterName(nsAString
& aName
) const {
108 if (mDisplayName
.IsEmpty()) {
110 PrinterInfoLock lock
= mPrinterInfoMutex
.Lock();
111 CopyUTF8toUTF16(MakeStringSpan(lock
->mPrinter
->name
), aName
);
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
) {
124 PrinterInfoLock lock
= TryEnsurePrinterInfo();
125 return mShim
.cupsLocalizeDestMedia(&aConnection
, lock
->mPrinter
,
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()) {
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");
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
,
179 uint64_t aCUPSPatch
) const {
180 PrinterInfoLock lock
= TryEnsurePrinterInfo();
181 // Compare major version.
182 if (lock
->mCUPSMajor
> aCUPSMajor
) {
185 if (lock
->mCUPSMajor
< aCUPSMajor
) {
189 // Compare minor version.
190 if (lock
->mCUPSMinor
> aCUPSMinor
) {
193 if (lock
->mCUPSMinor
< aCUPSMinor
) {
198 return aCUPSPatch
<= lock
->mCUPSPatch
;
201 http_t
* nsPrinterCUPS::Connection::GetConnection(cups_dest_t
* aDest
) {
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);
216 mConnection
= connection
;
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();
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.
239 hasDefaultMedia
= mShim
.cupsGetDestMediaDefault(
240 CUPS_HTTP_DEFAULT
, lock
->mPrinter
, lock
->mPrinterInfo
,
241 CUPS_MEDIA_FLAGS_DEFAULT
, &media
);
244 ipp_attribute_t
* defaultMediaIPP
=
245 mShim
.cupsFindDestDefault
246 ? mShim
.cupsFindDestDefault(CUPS_HTTP_DEFAULT
, lock
->mPrinter
,
247 lock
->mPrinterInfo
, "media")
250 const char* defaultMediaName
=
251 defaultMediaIPP
? mShim
.ippGetString(defaultMediaIPP
, 0, nullptr)
254 hasDefaultMedia
= defaultMediaName
&&
255 mShim
.cupsGetDestMediaByName(
256 CUPS_HTTP_DEFAULT
, lock
->mPrinter
, lock
->mPrinterInfo
,
257 defaultMediaName
, CUPS_MEDIA_FLAGS_DEFAULT
, &media
);
261 if (!hasDefaultMedia
) {
262 return PrintSettingsInitializer
{
263 std::move(printerName
),
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
),
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
),
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
) {
304 const int paperCount
= mShim
.cupsGetDestMediaCount
305 ? mShim
.cupsGetDestMediaCount(
306 connection
, lock
->mPrinter
,
307 lock
->mPrinterInfo
, CUPS_MEDIA_FLAGS_DEFAULT
)
309 nsTArray
<PaperInfo
> paperList
;
310 nsTHashtable
<nsCharPtrHashKey
> paperSet(std::max(paperCount
, 0));
312 paperList
.SetCapacity(paperCount
);
313 for (int i
= 0; i
< paperCount
; ++i
) {
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
)) {
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
));
330 const char* const mediaName
=
331 connection
? LocalizeMediaName(*connection
, media
) : media
.media
;
332 paperList
.AppendElement(
333 MakePaperInfo(NS_ConvertUTF8toUTF16(mediaName
), media
));
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
)) {
348 if (aConnection
== CUPS_HTTP_DEFAULT
) {
349 aLock
->mTriedInitWithDefault
= true;
351 aLock
->mTriedInitWithConnection
= true;
354 MOZ_ASSERT(aLock
->mPrinter
);
356 // httpGetAddress was only added in CUPS 2.0, and some systems still use
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
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
)) !=
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")) {
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
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
) {
455 Preferences::GetCString("print.cups.monochrome.extra_settings", pref
);
456 if (pref
.IsEmpty()) {
460 for (const auto& pair
: pref
.Split(',')) {
461 auto splitter
= pair
.Split(':');
462 auto end
= splitter
.end();
464 auto key
= splitter
.begin();
469 auto value
= ++splitter
.begin();
474 aCallback(*key
, *value
);