gecko-dev/widget/nsPrinterCUPS.cpp

477 lines
18 KiB
C++

/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 2; -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsPrinterCUPS.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/GkRustUtils.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPrefs_print.h"
#include "nsTHashtable.h"
#include "nsPaper.h"
#include "nsPrinterBase.h"
#include "nsPrintSettingsImpl.h"
using namespace mozilla;
using MarginDouble = mozilla::gfx::MarginDouble;
// Requested attributes for IPP requests, just the CUPS version now.
static constexpr Array<const char* const, 1> requestedAttributes{
"cups-version"};
static constexpr double kPointsPerHundredthMillimeter = 72.0 / 2540.0;
static PaperInfo MakePaperInfo(const nsAString& aName,
const cups_size_t& aMedia) {
// XXX Do we actually have the guarantee that this is utf-8?
NS_ConvertUTF8toUTF16 paperId(aMedia.media); // internal paper name/ID
return PaperInfo(
paperId, aName,
{aMedia.width * kPointsPerHundredthMillimeter,
aMedia.length * kPointsPerHundredthMillimeter},
Some(gfx::MarginDouble{aMedia.top * kPointsPerHundredthMillimeter,
aMedia.right * kPointsPerHundredthMillimeter,
aMedia.bottom * kPointsPerHundredthMillimeter,
aMedia.left * kPointsPerHundredthMillimeter}));
}
// Fetches the CUPS version for the print server controlling the printer. This
// will only modify the output arguments if the fetch succeeds.
static void FetchCUPSVersionForPrinter(const nsCUPSShim& aShim,
const cups_dest_t* const aDest,
uint64_t& aOutMajor, uint64_t& aOutMinor,
uint64_t& aOutPatch) {
// Make an IPP request to the server for the printer.
const char* const uri = aShim.cupsGetOption(
"printer-uri-supported", aDest->num_options, aDest->options);
if (!uri) {
return;
}
ipp_t* const ippRequest = aShim.ippNewRequest(IPP_OP_GET_PRINTER_ATTRIBUTES);
// Set the URI we want to use.
aShim.ippAddString(ippRequest, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri",
nullptr, uri);
// Set the attributes to request.
aShim.ippAddStrings(ippRequest, IPP_TAG_OPERATION, IPP_TAG_KEYWORD,
"requested-attributes", requestedAttributes.Length,
nullptr, &(requestedAttributes[0]));
// Use the default HTTP connection to query the CUPS server itself to get
// the CUPS version.
// Note that cupsDoRequest will delete the request whether it succeeds or
// fails, so we should not use ippDelete on it.
if (ipp_t* const ippResponse =
aShim.cupsDoRequest(CUPS_HTTP_DEFAULT, ippRequest, "/")) {
ipp_attribute_t* const versionAttrib =
aShim.ippFindAttribute(ippResponse, "cups-version", IPP_TAG_TEXT);
if (versionAttrib && aShim.ippGetCount(versionAttrib) == 1) {
const char* versionString = aShim.ippGetString(versionAttrib, 0, nullptr);
MOZ_ASSERT(versionString);
// On error, GkRustUtils::ParseSemVer will not modify its arguments.
GkRustUtils::ParseSemVer(
nsDependentCSubstring{MakeStringSpan(versionString)}, aOutMajor,
aOutMinor, aOutPatch);
}
aShim.ippDelete(ippResponse);
}
}
nsPrinterCUPS::~nsPrinterCUPS() {
PrinterInfoLock lock = mPrinterInfoMutex.Lock();
if (lock->mPrinterInfo) {
mShim.cupsFreeDestInfo(lock->mPrinterInfo);
}
if (lock->mPrinter) {
mShim.cupsFreeDests(1, lock->mPrinter);
}
}
NS_IMETHODIMP
nsPrinterCUPS::GetName(nsAString& aName) {
GetPrinterName(aName);
return NS_OK;
}
NS_IMETHODIMP
nsPrinterCUPS::GetSystemName(nsAString& aName) {
PrinterInfoLock lock = mPrinterInfoMutex.Lock();
CopyUTF8toUTF16(MakeStringSpan(lock->mPrinter->name), aName);
return NS_OK;
}
void nsPrinterCUPS::GetPrinterName(nsAString& aName) const {
if (mDisplayName.IsEmpty()) {
aName.Truncate();
PrinterInfoLock lock = mPrinterInfoMutex.Lock();
CopyUTF8toUTF16(MakeStringSpan(lock->mPrinter->name), aName);
} else {
aName = mDisplayName;
}
}
const char* nsPrinterCUPS::LocalizeMediaName(http_t& aConnection,
cups_size_t& aMedia) const {
// The returned string is owned by mPrinterInfo.
// https://www.cups.org/doc/cupspm.html#cupsLocalizeDestMedia
if (!mShim.cupsLocalizeDestMedia) {
return aMedia.media;
}
PrinterInfoLock lock = TryEnsurePrinterInfo();
return mShim.cupsLocalizeDestMedia(&aConnection, lock->mPrinter,
lock->mPrinterInfo,
CUPS_MEDIA_FLAGS_DEFAULT, &aMedia);
}
bool nsPrinterCUPS::SupportsDuplex() const {
return Supports(CUPS_SIDES, CUPS_SIDES_TWO_SIDED_PORTRAIT);
}
bool nsPrinterCUPS::SupportsMonochrome() const {
if (!SupportsColor()) {
return true;
}
return StaticPrefs::print_cups_monochrome_enabled();
}
bool nsPrinterCUPS::SupportsColor() const {
// CUPS 2.1 (particularly as used in Ubuntu 16) is known to have inaccurate
// results for CUPS_PRINT_COLOR_MODE.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1660658#c15
if (!IsCUPSVersionAtLeast(2, 2, 0)) {
return true; // See comment for PrintSettingsInitializer.mPrintInColor
}
return Supports(CUPS_PRINT_COLOR_MODE, CUPS_PRINT_COLOR_MODE_AUTO) ||
Supports(CUPS_PRINT_COLOR_MODE, CUPS_PRINT_COLOR_MODE_COLOR) ||
!Supports(CUPS_PRINT_COLOR_MODE, CUPS_PRINT_COLOR_MODE_MONOCHROME);
}
bool nsPrinterCUPS::SupportsCollation() const {
// We can't depend on cupsGetIntegerOption existing.
PrinterInfoLock lock = mPrinterInfoMutex.Lock();
const char* const value = FindCUPSOption(lock, "printer-type");
if (!value) {
return false;
}
// If the value is non-numeric, then atoi will return 0, which will still
// cause this function to return false.
const int type = atoi(value);
return type & CUPS_PRINTER_COLLATE;
}
nsPrinterBase::PrinterInfo nsPrinterCUPS::CreatePrinterInfo() const {
Connection connection{mShim};
return PrinterInfo{PaperList(connection), DefaultSettings(connection)};
}
bool nsPrinterCUPS::Supports(const char* aOption, const char* aValue) const {
PrinterInfoLock lock = TryEnsurePrinterInfo();
return mShim.cupsCheckDestSupported(CUPS_HTTP_DEFAULT, lock->mPrinter,
lock->mPrinterInfo, aOption, aValue);
}
bool nsPrinterCUPS::IsCUPSVersionAtLeast(uint64_t aCUPSMajor,
uint64_t aCUPSMinor,
uint64_t aCUPSPatch) const {
PrinterInfoLock lock = TryEnsurePrinterInfo();
// Compare major version.
if (lock->mCUPSMajor > aCUPSMajor) {
return true;
}
if (lock->mCUPSMajor < aCUPSMajor) {
return false;
}
// Compare minor version.
if (lock->mCUPSMinor > aCUPSMinor) {
return true;
}
if (lock->mCUPSMinor < aCUPSMinor) {
return false;
}
// Compare patch.
return aCUPSPatch <= lock->mCUPSPatch;
}
http_t* nsPrinterCUPS::Connection::GetConnection(cups_dest_t* aDest) {
if (mWasInited) {
return mConnection;
}
mWasInited = true;
// blocking call
http_t* const connection = mShim.cupsConnectDest(aDest, CUPS_DEST_FLAGS_NONE,
/* timeout(ms) */ 5000,
/* cancel */ nullptr,
/* resource */ nullptr,
/* resourcesize */ 0,
/* callback */ nullptr,
/* user_data */ nullptr);
if (connection) {
mConnection = connection;
}
return mConnection;
}
nsPrinterCUPS::Connection::~Connection() {
if (mWasInited && mConnection) {
mShim.httpClose(mConnection);
}
}
PrintSettingsInitializer nsPrinterCUPS::DefaultSettings(
Connection& aConnection) const {
nsString printerName;
GetPrinterName(printerName);
PrinterInfoLock lock = TryEnsurePrinterInfo();
cups_size_t media;
bool hasDefaultMedia = false;
// cupsGetDestMediaDefault appears to return more accurate defaults on macOS,
// and the IPP attribute appears to return more accurate defaults on Linux.
#ifdef XP_MACOSX
hasDefaultMedia = mShim.cupsGetDestMediaDefault(
CUPS_HTTP_DEFAULT, lock->mPrinter, lock->mPrinterInfo,
CUPS_MEDIA_FLAGS_DEFAULT, &media);
#else
{
ipp_attribute_t* defaultMediaIPP =
mShim.cupsFindDestDefault
? mShim.cupsFindDestDefault(CUPS_HTTP_DEFAULT, lock->mPrinter,
lock->mPrinterInfo, "media")
: nullptr;
const char* defaultMediaName =
defaultMediaIPP ? mShim.ippGetString(defaultMediaIPP, 0, nullptr)
: nullptr;
hasDefaultMedia = defaultMediaName &&
mShim.cupsGetDestMediaByName(
CUPS_HTTP_DEFAULT, lock->mPrinter, lock->mPrinterInfo,
defaultMediaName, CUPS_MEDIA_FLAGS_DEFAULT, &media);
}
#endif
if (!hasDefaultMedia) {
return PrintSettingsInitializer{
std::move(printerName),
PaperInfo(),
SupportsColor(),
};
}
// Check if this is a localized fallback paper size, in which case we can
// avoid using the CUPS localization methods.
const gfx::SizeDouble sizeDouble{
media.width * kPointsPerHundredthMillimeter,
media.length * kPointsPerHundredthMillimeter};
if (const PaperInfo* const paperInfo = FindCommonPaperSize(sizeDouble)) {
return PrintSettingsInitializer{
std::move(printerName),
MakePaperInfo(paperInfo->mName, media),
SupportsColor(),
};
}
http_t* const connection = aConnection.GetConnection(lock->mPrinter);
// XXX Do we actually have the guarantee that this is utf-8?
NS_ConvertUTF8toUTF16 localizedName{
connection ? LocalizeMediaName(*connection, media) : ""};
return PrintSettingsInitializer{
std::move(printerName),
MakePaperInfo(localizedName, media),
SupportsColor(),
};
}
nsTArray<mozilla::PaperInfo> nsPrinterCUPS::PaperList(
Connection& aConnection) const {
PrinterInfoLock lock = mPrinterInfoMutex.Lock();
http_t* const connection = aConnection.GetConnection(lock->mPrinter);
TryEnsurePrinterInfo(lock, connection);
if (!lock->mPrinterInfo) {
return {};
}
const int paperCount = mShim.cupsGetDestMediaCount
? mShim.cupsGetDestMediaCount(
connection, lock->mPrinter,
lock->mPrinterInfo, CUPS_MEDIA_FLAGS_DEFAULT)
: 0;
nsTArray<PaperInfo> paperList;
nsTHashtable<nsCharPtrHashKey> paperSet(std::max(paperCount, 0));
paperList.SetCapacity(paperCount);
for (int i = 0; i < paperCount; ++i) {
cups_size_t media;
const int getInfoSucceeded = mShim.cupsGetDestMediaByIndex(
connection, lock->mPrinter, lock->mPrinterInfo, i,
CUPS_MEDIA_FLAGS_DEFAULT, &media);
if (!getInfoSucceeded || !paperSet.EnsureInserted(media.media)) {
continue;
}
// Check if this is a PWG paper size, in which case we can avoid using the
// CUPS localization methods.
const gfx::SizeDouble sizeDouble{
media.width * kPointsPerHundredthMillimeter,
media.length * kPointsPerHundredthMillimeter};
if (const PaperInfo* const paperInfo = FindCommonPaperSize(sizeDouble)) {
paperList.AppendElement(MakePaperInfo(paperInfo->mName, media));
} else {
const char* const mediaName =
connection ? LocalizeMediaName(*connection, media) : media.media;
paperList.AppendElement(
MakePaperInfo(NS_ConvertUTF8toUTF16(mediaName), media));
}
}
return paperList;
}
void nsPrinterCUPS::TryEnsurePrinterInfo(PrinterInfoLock& aLock,
http_t* const aConnection) const {
if (aLock->mPrinterInfo ||
(aConnection == CUPS_HTTP_DEFAULT ? aLock->mTriedInitWithDefault
: aLock->mTriedInitWithConnection)) {
return;
}
if (aConnection == CUPS_HTTP_DEFAULT) {
aLock->mTriedInitWithDefault = true;
} else {
aLock->mTriedInitWithConnection = true;
}
MOZ_ASSERT(aLock->mPrinter);
// httpGetAddress was only added in CUPS 2.0, and some systems still use
// CUPS 1.7.
if (aConnection && MOZ_LIKELY(mShim.httpGetAddress && mShim.httpAddrPort)) {
// This is a workaround for the CUPS Bug seen in bug 1691347.
// This is to avoid a null string being passed to strstr in CUPS. The path
// in CUPS that leads to this is as follows:
//
// In cupsCopyDestInfo, CUPS_DEST_FLAG_DEVICE is set when the connection is
// not null (same as CUPS_HTTP_DEFAULT), the print server is not the same
// as our hostname and is not path-based (starts with a '/'), or the IPP
// port is different than the global server IPP port.
//
// https://github.com/apple/cups/blob/c9da6f63b263faef5d50592fe8cf8056e0a58aa2/cups/dest-options.c#L718-L722
//
// In _cupsGetDestResource, CUPS fetches the IPP options "device-uri" and
// "printer-uri-supported". Note that IPP options are returned as null when
// missing.
//
// https://github.com/apple/cups/blob/23c45db76a8520fd6c3b1d9164dbe312f1ab1481/cups/dest.c#L1138-L1141
//
// If the CUPS_DEST_FLAG_DEVICE is set or the "printer-uri-supported"
// option is not set, CUPS checks for "._tcp" in the "device-uri" option
// without doing a NULL-check first.
//
// https://github.com/apple/cups/blob/23c45db76a8520fd6c3b1d9164dbe312f1ab1481/cups/dest.c#L1144
//
// If we find that those branches will be taken, don't actually fetch the
// CUPS data and instead just return an empty printer info.
const char* const serverNameBytes = mShim.cupsServer();
if (MOZ_LIKELY(serverNameBytes)) {
const nsDependentCString serverName{serverNameBytes};
// We only need enough characters to determine equality with serverName.
// + 2 because we need one byte for the null-character, and we also want
// to get more characters of the host name than the server name if
// possible. Otherwise, if the hostname starts with the same text as the
// entire server name, it would compare equal when it's not.
const size_t hostnameMemLength = serverName.Length() + 2;
auto hostnameMem = MakeUnique<char[]>(hostnameMemLength);
// We don't expect httpGetHostname to return null when a connection is
// passed, but it's better not to make assumptions.
const char* const hostnameBytes = mShim.httpGetHostname(
aConnection, hostnameMem.get(), hostnameMemLength);
if (MOZ_LIKELY(hostnameBytes)) {
const nsDependentCString hostname{hostnameBytes};
// Attempt to match the condional at
// https://github.com/apple/cups/blob/c9da6f63b263faef5d50592fe8cf8056e0a58aa2/cups/dest-options.c#L718
//
// To find the result of the comparison CUPS performs of
// `strcmp(http->hostname, cg->server)`, we use httpGetHostname to try
// to get the value of `http->hostname`, but this isn't quite the same.
// For local addresses, httpGetHostName will normalize the result to be
// localhost", rather than the actual value of `http->hostname`.
//
// https://github.com/apple/cups/blob/2201569857f225c9874bfae19713ffb2f4bdfdeb/cups/http-addr.c#L794-L818
//
// Because of this, if both serverName and hostname equal "localhost",
// then the actual hostname might be a different local address that CUPS
// normalized in httpGetHostName, and `http->hostname` won't be equal to
// `cg->server` in CUPS.
const bool namesMightNotMatch =
hostname != serverName || hostname == "localhost";
const bool portsDiffer =
mShim.httpAddrPort(mShim.httpGetAddress(aConnection)) !=
mShim.ippPort();
const bool cupsDestDeviceFlag =
(namesMightNotMatch && serverName[0] != '/') || portsDiffer;
// Match the conditional at
// https://github.com/apple/cups/blob/23c45db76a8520fd6c3b1d9164dbe312f1ab1481/cups/dest.c#L1144
// but if device-uri is null do not call into CUPS.
if ((cupsDestDeviceFlag ||
!FindCUPSOption(aLock, "printer-uri-supported")) &&
!FindCUPSOption(aLock, "device-uri")) {
return;
}
}
}
}
// All CUPS calls that take the printer info do null-checks internally, so we
// can fetch this info and only worry about the result of the later CUPS
// functions.
aLock->mPrinterInfo = mShim.cupsCopyDestInfo(aConnection, aLock->mPrinter);
// Even if we failed to fetch printer info, it is still possible we can talk
// to the print server and get its CUPS version.
FetchCUPSVersionForPrinter(mShim, aLock->mPrinter, aLock->mCUPSMajor,
aLock->mCUPSMinor, aLock->mCUPSPatch);
}
void nsPrinterCUPS::ForEachExtraMonochromeSetting(
FunctionRef<void(const nsACString&, const nsACString&)> aCallback) {
nsAutoCString pref;
Preferences::GetCString("print.cups.monochrome.extra_settings", pref);
if (pref.IsEmpty()) {
return;
}
for (const auto& pair : pref.Split(',')) {
auto splitter = pair.Split(':');
auto end = splitter.end();
auto key = splitter.begin();
if (key == end) {
continue;
}
auto value = ++splitter.begin();
if (value == end) {
continue;
}
aCallback(*key, *value);
}
}