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"
18 using namespace mozilla
;
19 using MarginDouble
= mozilla::gfx::MarginDouble
;
21 // Requested attributes for IPP requests, just the CUPS version now.
22 static constexpr Array
<const char* const, 1> requestedAttributes
{
25 static constexpr double kPointsPerHundredthMillimeter
= 72.0 / 2540.0;
27 static PaperInfo
MakePaperInfo(const nsAString
& aName
,
28 const cups_size_t
& aMedia
) {
29 // XXX Do we actually have the guarantee that this is utf-8?
30 NS_ConvertUTF8toUTF16
paperId(aMedia
.media
); // internal paper name/ID
33 {aMedia
.width
* kPointsPerHundredthMillimeter
,
34 aMedia
.length
* kPointsPerHundredthMillimeter
},
35 Some(gfx::MarginDouble
{aMedia
.top
* kPointsPerHundredthMillimeter
,
36 aMedia
.right
* kPointsPerHundredthMillimeter
,
37 aMedia
.bottom
* kPointsPerHundredthMillimeter
,
38 aMedia
.left
* kPointsPerHundredthMillimeter
}));
41 // Fetches the CUPS version for the print server controlling the printer. This
42 // will only modify the output arguments if the fetch succeeds.
43 static void FetchCUPSVersionForPrinter(const nsCUPSShim
& aShim
,
44 const cups_dest_t
* const aDest
,
45 uint64_t& aOutMajor
, uint64_t& aOutMinor
,
46 uint64_t& aOutPatch
) {
47 // Make an IPP request to the server for the printer.
48 const char* const uri
= aShim
.cupsGetOption(
49 "printer-uri-supported", aDest
->num_options
, aDest
->options
);
54 ipp_t
* const ippRequest
= aShim
.ippNewRequest(IPP_OP_GET_PRINTER_ATTRIBUTES
);
56 // Set the URI we want to use.
57 aShim
.ippAddString(ippRequest
, IPP_TAG_OPERATION
, IPP_TAG_URI
, "printer-uri",
60 // Set the attributes to request.
61 aShim
.ippAddStrings(ippRequest
, IPP_TAG_OPERATION
, IPP_TAG_KEYWORD
,
62 "requested-attributes", requestedAttributes
.Length
,
63 nullptr, &(requestedAttributes
[0]));
65 // Use the default HTTP connection to query the CUPS server itself to get
67 // Note that cupsDoRequest will delete the request whether it succeeds or
68 // fails, so we should not use ippDelete on it.
69 if (ipp_t
* const ippResponse
=
70 aShim
.cupsDoRequest(CUPS_HTTP_DEFAULT
, ippRequest
, "/")) {
71 ipp_attribute_t
* const versionAttrib
=
72 aShim
.ippFindAttribute(ippResponse
, "cups-version", IPP_TAG_TEXT
);
73 if (versionAttrib
&& aShim
.ippGetCount(versionAttrib
) == 1) {
74 const char* versionString
= aShim
.ippGetString(versionAttrib
, 0, nullptr);
75 MOZ_ASSERT(versionString
);
76 // On error, GkRustUtils::ParseSemVer will not modify its arguments.
77 GkRustUtils::ParseSemVer(
78 nsDependentCSubstring
{MakeStringSpan(versionString
)}, aOutMajor
,
79 aOutMinor
, aOutPatch
);
81 aShim
.ippDelete(ippResponse
);
85 nsPrinterCUPS::~nsPrinterCUPS() {
86 auto printerInfoLock
= mPrinterInfoMutex
.Lock();
87 if (printerInfoLock
->mPrinterInfo
) {
88 mShim
.cupsFreeDestInfo(printerInfoLock
->mPrinterInfo
);
91 mShim
.cupsFreeDests(1, mPrinter
);
97 nsPrinterCUPS::GetName(nsAString
& aName
) {
98 GetPrinterName(aName
);
103 nsPrinterCUPS::GetSystemName(nsAString
& aName
) {
104 CopyUTF8toUTF16(MakeStringSpan(mPrinter
->name
), aName
);
108 void nsPrinterCUPS::GetPrinterName(nsAString
& aName
) const {
109 if (mDisplayName
.IsEmpty()) {
111 CopyUTF8toUTF16(MakeStringSpan(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 auto printerInfoLock
= TryEnsurePrinterInfo();
125 cups_dinfo_t
* const printerInfo
= printerInfoLock
->mPrinterInfo
;
126 return mShim
.cupsLocalizeDestMedia(&aConnection
, mPrinter
, printerInfo
,
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 const char* const value
= FindCUPSOption("printer-type");
159 // If the value is non-numeric, then atoi will return 0, which will still
160 // cause this function to return false.
161 const int type
= atoi(value
);
162 return type
& CUPS_PRINTER_COLLATE
;
165 nsPrinterBase::PrinterInfo
nsPrinterCUPS::CreatePrinterInfo() const {
166 Connection connection
{mShim
};
167 return PrinterInfo
{PaperList(connection
), DefaultSettings(connection
)};
170 bool nsPrinterCUPS::Supports(const char* aOption
, const char* aValue
) const {
171 auto printerInfoLock
= TryEnsurePrinterInfo();
172 cups_dinfo_t
* const printerInfo
= printerInfoLock
->mPrinterInfo
;
173 return mShim
.cupsCheckDestSupported(CUPS_HTTP_DEFAULT
, mPrinter
, printerInfo
,
177 bool nsPrinterCUPS::IsCUPSVersionAtLeast(uint64_t aCUPSMajor
,
179 uint64_t aCUPSPatch
) const {
180 auto printerInfoLock
= TryEnsurePrinterInfo();
181 // Compare major version.
182 if (printerInfoLock
->mCUPSMajor
> aCUPSMajor
) {
185 if (printerInfoLock
->mCUPSMajor
< aCUPSMajor
) {
189 // Compare minor version.
190 if (printerInfoLock
->mCUPSMinor
> aCUPSMinor
) {
193 if (printerInfoLock
->mCUPSMinor
< aCUPSMinor
) {
198 return aCUPSPatch
<= printerInfoLock
->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 auto printerInfoLock
= TryEnsurePrinterInfo();
232 cups_dinfo_t
* const printerInfo
= printerInfoLock
->mPrinterInfo
;
236 bool hasDefaultMedia
= false;
237 // cupsGetDestMediaDefault appears to return more accurate defaults on macOS,
238 // and the IPP attribute appears to return more accurate defaults on Linux.
241 mShim
.cupsGetDestMediaDefault(CUPS_HTTP_DEFAULT
, mPrinter
, printerInfo
,
242 CUPS_MEDIA_FLAGS_DEFAULT
, &media
);
245 ipp_attribute_t
* defaultMediaIPP
=
246 mShim
.cupsFindDestDefault
247 ? mShim
.cupsFindDestDefault(CUPS_HTTP_DEFAULT
, mPrinter
,
248 printerInfo
, "media")
251 const char* defaultMediaName
=
252 defaultMediaIPP
? mShim
.ippGetString(defaultMediaIPP
, 0, nullptr)
255 hasDefaultMedia
= defaultMediaName
&&
256 mShim
.cupsGetDestMediaByName(
257 CUPS_HTTP_DEFAULT
, mPrinter
, printerInfo
,
258 defaultMediaName
, CUPS_MEDIA_FLAGS_DEFAULT
, &media
);
262 if (!hasDefaultMedia
) {
263 return PrintSettingsInitializer
{
264 std::move(printerName
),
270 // Check if this is a localized fallback paper size, in which case we can
271 // avoid using the CUPS localization methods.
272 const gfx::SizeDouble sizeDouble
{
273 media
.width
* kPointsPerHundredthMillimeter
,
274 media
.length
* kPointsPerHundredthMillimeter
};
275 if (const PaperInfo
* const paperInfo
= FindCommonPaperSize(sizeDouble
)) {
276 return PrintSettingsInitializer
{
277 std::move(printerName
),
278 MakePaperInfo(paperInfo
->mName
, media
),
283 http_t
* const connection
= aConnection
.GetConnection(mPrinter
);
284 // XXX Do we actually have the guarantee that this is utf-8?
285 NS_ConvertUTF8toUTF16 localizedName
{
286 connection
? LocalizeMediaName(*connection
, media
) : ""};
288 return PrintSettingsInitializer
{
289 std::move(printerName
),
290 MakePaperInfo(localizedName
, media
),
295 nsTArray
<mozilla::PaperInfo
> nsPrinterCUPS::PaperList(
296 Connection
& aConnection
) const {
297 http_t
* const connection
= aConnection
.GetConnection(mPrinter
);
298 auto printerInfoLock
= TryEnsurePrinterInfo(connection
);
299 cups_dinfo_t
* const printerInfo
= printerInfoLock
->mPrinterInfo
;
305 const int paperCount
=
306 mShim
.cupsGetDestMediaCount
307 ? mShim
.cupsGetDestMediaCount(connection
, mPrinter
, printerInfo
,
308 CUPS_MEDIA_FLAGS_DEFAULT
)
310 nsTArray
<PaperInfo
> paperList
;
311 nsTHashtable
<nsCharPtrHashKey
> paperSet(std::max(paperCount
, 0));
313 paperList
.SetCapacity(paperCount
);
314 for (int i
= 0; i
< paperCount
; ++i
) {
316 const int getInfoSucceeded
= mShim
.cupsGetDestMediaByIndex(
317 connection
, mPrinter
, printerInfo
, i
, 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 nsPrinterCUPS::PrinterInfoLock
nsPrinterCUPS::TryEnsurePrinterInfo(
341 http_t
* const aConnection
) const {
342 PrinterInfoLock lock
= mPrinterInfoMutex
.Lock();
343 if (lock
->mPrinterInfo
||
344 (aConnection
== CUPS_HTTP_DEFAULT
? lock
->mTriedInitWithDefault
345 : lock
->mTriedInitWithConnection
)) {
349 if (aConnection
== CUPS_HTTP_DEFAULT
) {
350 lock
->mTriedInitWithDefault
= true;
352 lock
->mTriedInitWithConnection
= true;
355 MOZ_ASSERT(mPrinter
);
357 // httpGetAddress was only added in CUPS 2.0, and some systems still use
359 if (aConnection
&& MOZ_LIKELY(mShim
.httpGetAddress
&& mShim
.httpAddrPort
)) {
360 // This is a workaround for the CUPS Bug seen in bug 1691347.
361 // This is to avoid a null string being passed to strstr in CUPS. The path
362 // in CUPS that leads to this is as follows:
364 // In cupsCopyDestInfo, CUPS_DEST_FLAG_DEVICE is set when the connection is
365 // not null (same as CUPS_HTTP_DEFAULT), the print server is not the same
366 // as our hostname and is not path-based (starts with a '/'), or the IPP
367 // port is different than the global server IPP port.
369 // https://github.com/apple/cups/blob/c9da6f63b263faef5d50592fe8cf8056e0a58aa2/cups/dest-options.c#L718-L722
371 // In _cupsGetDestResource, CUPS fetches the IPP options "device-uri" and
372 // "printer-uri-supported". Note that IPP options are returned as null when
375 // https://github.com/apple/cups/blob/23c45db76a8520fd6c3b1d9164dbe312f1ab1481/cups/dest.c#L1138-L1141
377 // If the CUPS_DEST_FLAG_DEVICE is set or the "printer-uri-supported"
378 // option is not set, CUPS checks for "._tcp" in the "device-uri" option
379 // without doing a NULL-check first.
381 // https://github.com/apple/cups/blob/23c45db76a8520fd6c3b1d9164dbe312f1ab1481/cups/dest.c#L1144
383 // If we find that those branches will be taken, don't actually fetch the
384 // CUPS data and instead just return an empty printer info.
386 const char* const serverNameBytes
= mShim
.cupsServer();
388 if (MOZ_LIKELY(serverNameBytes
)) {
389 const nsDependentCString serverName
{serverNameBytes
};
391 // We only need enough characters to determine equality with serverName.
392 // + 2 because we need one byte for the null-character, and we also want
393 // to get more characters of the host name than the server name if
394 // possible. Otherwise, if the hostname starts with the same text as the
395 // entire server name, it would compare equal when it's not.
396 const size_t hostnameMemLength
= serverName
.Length() + 2;
397 auto hostnameMem
= MakeUnique
<char[]>(hostnameMemLength
);
399 // We don't expect httpGetHostname to return null when a connection is
400 // passed, but it's better not to make assumptions.
401 const char* const hostnameBytes
= mShim
.httpGetHostname(
402 aConnection
, hostnameMem
.get(), hostnameMemLength
);
404 if (MOZ_LIKELY(hostnameBytes
)) {
405 const nsDependentCString hostname
{hostnameBytes
};
407 // Attempt to match the condional at
408 // https://github.com/apple/cups/blob/c9da6f63b263faef5d50592fe8cf8056e0a58aa2/cups/dest-options.c#L718
410 // To find the result of the comparison CUPS performs of
411 // `strcmp(http->hostname, cg->server)`, we use httpGetHostname to try
412 // to get the value of `http->hostname`, but this isn't quite the same.
413 // For local addresses, httpGetHostName will normalize the result to be
414 // localhost", rather than the actual value of `http->hostname`.
416 // https://github.com/apple/cups/blob/2201569857f225c9874bfae19713ffb2f4bdfdeb/cups/http-addr.c#L794-L818
418 // Because of this, if both serverName and hostname equal "localhost",
419 // then the actual hostname might be a different local address that CUPS
420 // normalized in httpGetHostName, and `http->hostname` won't be equal to
421 // `cg->server` in CUPS.
422 const bool namesMightNotMatch
=
423 hostname
!= serverName
|| hostname
== "localhost";
424 const bool portsDiffer
=
425 mShim
.httpAddrPort(mShim
.httpGetAddress(aConnection
)) !=
427 const bool cupsDestDeviceFlag
=
428 (namesMightNotMatch
&& serverName
[0] != '/') || portsDiffer
;
430 // Match the conditional at
431 // https://github.com/apple/cups/blob/23c45db76a8520fd6c3b1d9164dbe312f1ab1481/cups/dest.c#L1144
432 // but if device-uri is null do not call into CUPS.
433 if ((cupsDestDeviceFlag
|| !FindCUPSOption("printer-uri-supported")) &&
434 !FindCUPSOption("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 lock
->mPrinterInfo
= mShim
.cupsCopyDestInfo(aConnection
, 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
, mPrinter
, lock
->mCUPSMajor
,
449 lock
->mCUPSMinor
, lock
->mCUPSPatch
);
453 void nsPrinterCUPS::ForEachExtraMonochromeSetting(
454 FunctionRef
<void(const nsACString
&, const nsACString
&)> aCallback
) {
456 Preferences::GetCString("print.cups.monochrome.extra_settings", pref
);
457 if (pref
.IsEmpty()) {
461 for (const auto& pair
: pref
.Split(',')) {
462 auto splitter
= pair
.Split(':');
463 auto end
= splitter
.end();
465 auto key
= splitter
.begin();
470 auto value
= ++splitter
.begin();
475 aCallback(*key
, *value
);