/* -*- 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" #include "plstr.h" using namespace mozilla; using MarginDouble = mozilla::gfx::MarginDouble; // Requested attributes for IPP requests, just the CUPS version now. static constexpr Array 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() { auto printerInfoLock = mPrinterInfoMutex.Lock(); if (printerInfoLock->mPrinterInfo) { mShim.cupsFreeDestInfo(printerInfoLock->mPrinterInfo); } if (mPrinter) { mShim.cupsFreeDests(1, mPrinter); mPrinter = nullptr; } } NS_IMETHODIMP nsPrinterCUPS::GetName(nsAString& aName) { GetPrinterName(aName); return NS_OK; } NS_IMETHODIMP nsPrinterCUPS::GetSystemName(nsAString& aName) { CopyUTF8toUTF16(MakeStringSpan(mPrinter->name), aName); return NS_OK; } void nsPrinterCUPS::GetPrinterName(nsAString& aName) const { if (mDisplayName.IsEmpty()) { aName.Truncate(); CopyUTF8toUTF16(MakeStringSpan(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; } auto printerInfoLock = TryEnsurePrinterInfo(); cups_dinfo_t* const printerInfo = printerInfoLock->mPrinterInfo; return mShim.cupsLocalizeDestMedia(&aConnection, mPrinter, printerInfo, 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. const char* const value = FindCUPSOption("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 { auto printerInfoLock = TryEnsurePrinterInfo(); cups_dinfo_t* const printerInfo = printerInfoLock->mPrinterInfo; return mShim.cupsCheckDestSupported(CUPS_HTTP_DEFAULT, mPrinter, printerInfo, aOption, aValue); } bool nsPrinterCUPS::IsCUPSVersionAtLeast(uint64_t aCUPSMajor, uint64_t aCUPSMinor, uint64_t aCUPSPatch) const { auto printerInfoLock = TryEnsurePrinterInfo(); // Compare major version. if (printerInfoLock->mCUPSMajor > aCUPSMajor) { return true; } if (printerInfoLock->mCUPSMajor < aCUPSMajor) { return false; } // Compare minor version. if (printerInfoLock->mCUPSMinor > aCUPSMinor) { return true; } if (printerInfoLock->mCUPSMinor < aCUPSMinor) { return false; } // Compare patch. return aCUPSPatch <= printerInfoLock->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); auto printerInfoLock = TryEnsurePrinterInfo(); cups_dinfo_t* const printerInfo = printerInfoLock->mPrinterInfo; 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, mPrinter, printerInfo, CUPS_MEDIA_FLAGS_DEFAULT, &media); #else { ipp_attribute_t* defaultMediaIPP = mShim.cupsFindDestDefault ? mShim.cupsFindDestDefault(CUPS_HTTP_DEFAULT, mPrinter, printerInfo, "media") : nullptr; const char* defaultMediaName = defaultMediaIPP ? mShim.ippGetString(defaultMediaIPP, 0, nullptr) : nullptr; hasDefaultMedia = defaultMediaName && mShim.cupsGetDestMediaByName( CUPS_HTTP_DEFAULT, mPrinter, printerInfo, 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(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 nsPrinterCUPS::PaperList( Connection& aConnection) const { http_t* const connection = aConnection.GetConnection(mPrinter); auto printerInfoLock = TryEnsurePrinterInfo(connection); cups_dinfo_t* const printerInfo = printerInfoLock->mPrinterInfo; if (!printerInfo) { return {}; } const int paperCount = mShim.cupsGetDestMediaCount ? mShim.cupsGetDestMediaCount(connection, mPrinter, printerInfo, CUPS_MEDIA_FLAGS_DEFAULT) : 0; nsTArray paperList; nsTHashtable 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, mPrinter, printerInfo, 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; } nsPrinterCUPS::PrinterInfoLock nsPrinterCUPS::TryEnsurePrinterInfo( http_t* const aConnection) const { PrinterInfoLock lock = mPrinterInfoMutex.Lock(); if (lock->mPrinterInfo || (aConnection == CUPS_HTTP_DEFAULT ? lock->mTriedInitWithDefault : lock->mTriedInitWithConnection)) { return lock; } if (aConnection == CUPS_HTTP_DEFAULT) { lock->mTriedInitWithDefault = true; } else { lock->mTriedInitWithConnection = true; } MOZ_ASSERT(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(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("printer-uri-supported")) && !FindCUPSOption("device-uri")) { return lock; } } } } // 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. lock->mPrinterInfo = mShim.cupsCopyDestInfo(aConnection, 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, mPrinter, lock->mCUPSMajor, lock->mCUPSMinor, lock->mCUPSPatch); return lock; } void nsPrinterCUPS::ForEachExtraMonochromeSetting( FunctionRef 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); } }