File indexing completed on 2025-01-05 05:09:28

0001 /*
0002     SPDX-FileCopyrightText: 2010-2018 Daniel Nicoletti <dantti12@gmail.com>
0003     SPDX-FileCopyrightText: 2023 Mike Noe <noeerover@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "DevicesModel.h"
0009 
0010 #include <KCupsPrinter.h>
0011 #include <kcupslib_log.h>
0012 
0013 #include <KLocalizedString>
0014 
0015 #include <QDBusConnection>
0016 #include <QDBusMetaType>
0017 #include <QHostInfo>
0018 
0019 using namespace Qt::StringLiterals;
0020 
0021 DevicesModel::DevicesModel(QObject *parent)
0022     : QStandardItemModel(parent)
0023     , m_request(nullptr)
0024     , m_rx(QLatin1String("[a-z]+://.*"))
0025     , m_blacklistedURIs({QLatin1String("hp"),
0026                          QLatin1String("hpfax"),
0027                          QLatin1String("hal"),
0028                          QLatin1String("beh"),
0029                          QLatin1String("scsi"),
0030                          QLatin1String("http"),
0031                          QLatin1String("delete")})
0032 {
0033     m_roles = QStandardItemModel::roleNames();
0034     m_roles[DeviceClass] = "deviceClass";
0035     m_roles[DeviceId] = "deviceId";
0036     m_roles[DeviceInfo] = "deviceInfo";
0037     m_roles[DeviceMakeAndModel] = "deviceMakeModel";
0038     m_roles[DeviceUri] = "deviceUri";
0039     m_roles[DeviceUris] = "deviceUris";
0040     m_roles[DeviceLocation] = "deviceLocation";
0041     m_roles[DeviceDescription] = "deviceDescription";
0042     m_roles[DeviceCategory] = "deviceCategory";
0043 
0044     qDBusRegisterMetaType<MapSS>();
0045     qDBusRegisterMetaType<MapSMapSS>();
0046 
0047     // Adds the other device which is meant for manual URI input
0048     insertDevice(QLatin1String("other"), QString(), i18nc("@item", "Manual URI"), QString(), QLatin1String("other"), QString());
0049 }
0050 
0051 QHash<int, QByteArray> DevicesModel::roleNames() const
0052 {
0053     return m_roles;
0054 }
0055 
0056 QString DevicesModel::uriDevice(const QString &uri) const
0057 {
0058     QString ret;
0059     if (uri.startsWith(QLatin1String("parallel"))) {
0060         ret = i18n("Parallel Port");
0061     } else if (uri.startsWith(QLatin1String("serial"))) {
0062         ret = i18n("Serial Port");
0063     } else if (uri.startsWith(QLatin1String("usb"))) {
0064         ret = i18n("USB");
0065     } else if (uri.startsWith(QLatin1String("bluetooth"))) {
0066         ret = i18n("Bluetooth");
0067     } else if (uri.startsWith(QLatin1String("hpfax"))) {
0068         ret = i18n("Fax - HP Linux Imaging and Printing (HPLIP)");
0069     } else if (uri.startsWith(QLatin1String("hp"))) {
0070         ret = i18n("HP Linux Imaging and Printing (HPLIP)");
0071     } else if (uri.startsWith(QLatin1String("hal"))) {
0072         ret = i18n("Hardware Abstraction Layer (HAL)");
0073     } else if (uri.startsWith(QLatin1String("socket"))) {
0074         ret = i18n("AppSocket/HP JetDirect");
0075     } else if (uri.startsWith(QLatin1String("lpd"))) {
0076         // Check if the queue name is defined
0077         const QString queue = uri.section(QLatin1Char('/'), -1, -1);
0078         if (queue.isEmpty()) {
0079             ret = i18n("LPD/LPR queue");
0080         } else {
0081             ret = i18n("LPD/LPR queue %1", queue);
0082         }
0083     } else if (uri.startsWith(QLatin1String("smb"))) {
0084         ret = i18n("Windows Printer via SAMBA");
0085     } else if (uri.startsWith(QLatin1String("ipp"))) {
0086         // Check if the queue name (fileName) is defined
0087         const QString queue = uri.section(QLatin1Char('/'), -1, -1);
0088         if (queue.isEmpty()) {
0089             ret = i18n("IPP");
0090         } else {
0091             ret = i18n("IPP %1", queue);
0092         }
0093     } else if (uri.startsWith(QLatin1String("https"))) {
0094         ret = i18n("HTTP");
0095     } else if (uri.startsWith(QLatin1String("dnssd")) || uri.startsWith(QLatin1String("mdns"))) {
0096         // TODO this needs testing...
0097         QString text;
0098         if (uri.contains(QLatin1String("cups"))) {
0099             text = i18n("Remote CUPS printer via DNS-SD");
0100         } else {
0101             if (uri.contains(QLatin1String("._ipp"))) {
0102                 ret = i18n("IPP network printer via DNS-SD");
0103             } else if (uri.contains(QLatin1String("._printer"))) {
0104                 ret = i18n("LPD network printer via DNS-SD");
0105             } else if (uri.contains(QLatin1String("._pdl-datastream"))) {
0106                 ret = i18n("AppSocket/JetDirect network printer via DNS-SD");
0107             } else {
0108                 ret = i18n("Network printer via DNS-SD");
0109             }
0110         }
0111     } else {
0112         ret = uri;
0113     }
0114     return ret;
0115 }
0116 
0117 QString DevicesModel::deviceDescription(const QString &uri) const
0118 {
0119     static QMap<QString, QString> descriptions(
0120         {{u"parallel"_s, i18nc("@info:tooltip", "A printer connected to the parallel port")},
0121          {u"bluetooth"_s, i18nc("@info:tooltip", "A printer connected via Bluetooth")},
0122          {u"hal"_s, i18nc("@info:tooltip", "Local printer detected by the Hardware Abstraction Layer (HAL)")},
0123          {u"hpfax"_s, i18nc("@info:tooltip", "HPLIP software driving a fax machine,\nor the fax function of a multi-function device")},
0124          {u"hp"_s, i18nc("@info:tooltip", "HPLIP software driving a printer,\nor the printer function of a multi-function device")},
0125          {u"ipp"_s, i18nc("@info:tooltip", "IPP Network printer via IPP")},
0126          {u"usb"_s, i18nc("@info:tooltip", "A printer connected to a USB port")}});
0127 
0128     QString ret;
0129 
0130     if (uri.startsWith(u"dnssd"_s) || uri.startsWith(u"mdns"_s)) {
0131         if (uri.contains(u"cups"_s)) {
0132             ret = i18nc("@info:tooltip", "Remote CUPS printer via DNS-SD");
0133         } else {
0134             QString protocol;
0135             if (uri.contains(u"._ipp"_s)) {
0136                 protocol = u"IPP"_s;
0137             } else if (uri.contains(u"._printer"_s)) {
0138                 protocol = u"LPD"_s;
0139             } else if (uri.contains(u"._pdl-datastream"_s)) {
0140                 protocol = u"AppSocket/JetDirect"_s;
0141             }
0142 
0143             if (protocol.isEmpty()) {
0144                 ret = i18nc("@info:tooltip", "Network printer via DNS-SD");
0145             } else {
0146                 ret = i18nc("@info:tooltip", "%1 network printer via DNS-SD", protocol);
0147             }
0148         }
0149     } else {
0150         for (auto i = descriptions.cbegin(), end = descriptions.cend(); i != end; ++i) {
0151             if (uri.startsWith(i.key())) {
0152                 return i.value();
0153             }
0154         }
0155     }
0156 
0157     return ret.isEmpty() ? uri : ret;
0158 }
0159 
0160 void DevicesModel::update()
0161 {
0162     if (m_request) {
0163         return;
0164     }
0165 
0166     // clear the model to don't duplicate items
0167     if (rowCount()) {
0168         removeRows(1, rowCount() - 1);
0169     }
0170     m_request = new KCupsRequest;
0171     connect(m_request, &KCupsRequest::device, this, &DevicesModel::gotDevice);
0172     connect(m_request, &KCupsRequest::finished, this, &DevicesModel::finished);
0173 
0174     // Get devices with 5 seconds of timeout
0175     m_request->getDevices(10);
0176 }
0177 
0178 void DevicesModel::gotDevice(const QString &device_class,
0179                              const QString &device_id,
0180                              const QString &device_info,
0181                              const QString &device_make_and_model,
0182                              const QString &device_uri,
0183                              const QString &device_location)
0184 {
0185     // "direct"
0186     qCDebug(LIBKCUPS) << device_class;
0187     // "MFG:Samsung;CMD:GDI;MDL:SCX-4200 Series;CLS:PRINTER;MODE:PCL;STATUS:IDLE;"
0188     qCDebug(LIBKCUPS) << device_id;
0189     // "Samsung SCX-4200 Series"
0190     qCDebug(LIBKCUPS) << device_info;
0191     // "Samsung SCX-4200 Series"
0192     qCDebug(LIBKCUPS) << device_make_and_model;
0193     // "usb://Samsung/SCX-4200%20Series"
0194     qCDebug(LIBKCUPS) << device_uri;
0195     // ""
0196     qCDebug(LIBKCUPS) << device_location;
0197 
0198     if (m_blacklistedURIs.contains(device_uri)) {
0199         // ignore black listed uri's
0200         return;
0201     }
0202 
0203     // For the protocols, not real devices
0204     if (device_id.isEmpty() && device_make_and_model == QLatin1String("Unknown")) {
0205         insertDevice(device_class, device_id, device_info, device_make_and_model, device_uri, device_location);
0206     } else {
0207         // Map the devices so later we try to group them
0208         const MapSS mapSS({{KCUPS_DEVICE_CLASS, device_class},
0209                            {KCUPS_DEVICE_ID, device_id},
0210                            {KCUPS_DEVICE_INFO, device_info},
0211                            {KCUPS_DEVICE_MAKE_AND_MODEL, device_make_and_model},
0212                            {KCUPS_DEVICE_LOCATION, device_location}});
0213         m_mappedDevices[device_uri] = mapSS;
0214     }
0215 }
0216 
0217 void DevicesModel::finished()
0218 {
0219     bool hasError = m_request->hasError();
0220     if (hasError) {
0221         Q_EMIT errorMessage(i18n("Failed to get a list of devices: '%1'", m_request->errorMsg()));
0222     }
0223     m_request->deleteLater();
0224     m_request = nullptr;
0225 
0226     if (hasError || m_mappedDevices.isEmpty()) {
0227         Q_EMIT loaded();
0228         return;
0229     }
0230 
0231     QDBusMessage message;
0232     message = QDBusMessage::createMethodCall(QLatin1String("org.fedoraproject.Config.Printing"),
0233                                              QLatin1String("/org/fedoraproject/Config/Printing"),
0234                                              QLatin1String("org.fedoraproject.Config.Printing"),
0235                                              QLatin1String("GroupPhysicalDevices"));
0236     message << QVariant::fromValue(m_mappedDevices);
0237     QDBusConnection::sessionBus().callWithCallback(message,
0238                                                    this,
0239                                                    SLOT(getGroupedDevicesSuccess(QDBusMessage)),
0240                                                    SLOT(getGroupedDevicesFailed(QDBusError, QDBusMessage)));
0241 }
0242 
0243 void DevicesModel::insertDevice(const QString &device_class,
0244                                 const QString &device_id,
0245                                 const QString &device_info,
0246                                 const QString &device_make_and_model,
0247                                 const QString &device_uri,
0248                                 const QString &device_location,
0249                                 const QStringList &grouped_uris)
0250 {
0251     QStandardItem *stdItem;
0252     stdItem = createItem(device_class, device_id, device_info, device_make_and_model, device_uri, device_location, !grouped_uris.isEmpty());
0253     if (!grouped_uris.isEmpty()) {
0254         stdItem->setData(grouped_uris, DeviceUris);
0255     }
0256 }
0257 
0258 void DevicesModel::insertDevice(const QString &device_class,
0259                                 const QString &device_id,
0260                                 const QString &device_info,
0261                                 const QString &device_make_and_model,
0262                                 const QString &device_uri,
0263                                 const QString &device_location,
0264                                 const KCupsPrinters &grouped_printers)
0265 {
0266     QStandardItem *stdItem;
0267     stdItem = createItem(device_class, device_id, device_info, device_make_and_model, device_uri, device_location, !grouped_printers.isEmpty());
0268     if (!grouped_printers.isEmpty()) {
0269         stdItem->setData(QVariant::fromValue(grouped_printers), DeviceUris);
0270     }
0271 }
0272 
0273 QStandardItem *DevicesModel::createItem(const QString &device_class,
0274                                         const QString &device_id,
0275                                         const QString &device_info,
0276                                         const QString &device_make_and_model,
0277                                         const QString &device_uri,
0278                                         const QString &device_location,
0279                                         bool grouped)
0280 {
0281     // "direct"
0282     qCDebug(LIBKCUPS) << device_class;
0283     // "MFG:Samsung;CMD:GDI;MDL:SCX-4200 Series;CLS:PRINTER;MODE:PCL;STATUS:IDLE;"
0284     qCDebug(LIBKCUPS) << device_id;
0285     // "Samsung SCX-4200 Series"
0286     qCDebug(LIBKCUPS) << device_info;
0287     // "Samsung SCX-4200 Series"
0288     qCDebug(LIBKCUPS) << device_make_and_model;
0289     // "usb://Samsung/SCX-4200%20Series"
0290     qCDebug(LIBKCUPS) << device_uri;
0291     // ""
0292     qCDebug(LIBKCUPS) << device_location;
0293 
0294     Kind kind;
0295     // Store the kind of the device
0296     if (device_class == QLatin1String("network")) {
0297         const auto match = m_rx.match(device_uri);
0298         if (match.hasMatch()) {
0299             kind = Networked;
0300         } else {
0301             // other network devices looks like
0302             // just "http"
0303             kind = OtherNetworked;
0304         }
0305     } else if (device_class == QLatin1String("other") && device_uri == QLatin1String("other")) {
0306         kind = Other;
0307     } else {
0308         // If device class is not network assume local
0309         kind = Local;
0310     }
0311 
0312     QString location;
0313     if (device_location.isEmpty() && kind == Local) {
0314         location = QHostInfo::localHostName();
0315     } else {
0316         location = device_location;
0317     }
0318 
0319     QString text;
0320     if (!device_make_and_model.isEmpty() && !grouped && device_make_and_model.compare(QLatin1String("unknown"), Qt::CaseInsensitive)) {
0321         text = device_info + QLatin1String(" (") + device_make_and_model + QLatin1Char(')');
0322     } else {
0323         text = device_info;
0324     }
0325 
0326     QString toolTip;
0327     if (!grouped) {
0328         toolTip = deviceDescription(device_uri);
0329     }
0330 
0331     auto stdItem = new QStandardItem;
0332     stdItem->setText(text);
0333     stdItem->setToolTip(toolTip);
0334     stdItem->setData(device_class, DeviceClass);
0335     stdItem->setData(device_id, DeviceId);
0336     stdItem->setData(device_info, DeviceInfo);
0337     stdItem->setData(device_uri, DeviceUri);
0338     stdItem->setData(device_make_and_model, DeviceMakeAndModel);
0339     stdItem->setData(device_location, DeviceLocation);
0340     stdItem->setData(deviceDescription(device_uri), DeviceDescription);
0341 
0342     // Find the proper category to our item
0343     QStandardItem *catItem;
0344     QString cat;
0345     switch (kind) {
0346     case Networked:
0347         cat = i18nc("@item", "Discovered Network Printers");
0348         catItem = findCreateCategory(cat, kind);
0349         stdItem->setData(u"Network"_s, DeviceCategory);
0350         catItem->appendRow(stdItem);
0351         break;
0352     case OtherNetworked:
0353         cat = i18nc("@item", "Other Network Printers");
0354         catItem = findCreateCategory(cat, kind);
0355         stdItem->setData(u"Manual"_s, DeviceCategory);
0356         catItem->appendRow(stdItem);
0357         break;
0358     case Local:
0359         cat = i18nc("@item", "Local Printers");
0360         catItem = findCreateCategory(cat, kind);
0361         stdItem->setData(u"Local"_s, DeviceCategory);
0362         catItem->appendRow(stdItem);
0363         break;
0364     default:
0365         stdItem->setData(kind, Qt::UserRole);
0366         stdItem->setData(u"Manual"_s, DeviceCategory);
0367         appendRow(stdItem);
0368     }
0369 
0370     return stdItem;
0371 }
0372 
0373 void DevicesModel::getGroupedDevicesSuccess(const QDBusMessage &message)
0374 {
0375     if (message.type() == QDBusMessage::ReplyMessage && message.arguments().size() == 1) {
0376         const auto argument = message.arguments().first().value<QDBusArgument>();
0377         const auto groupedDevices = qdbus_cast<QList<QStringList>>(argument);
0378         for (const QStringList &list : groupedDevices) {
0379             if (list.isEmpty()) {
0380                 continue;
0381             }
0382 
0383             const QString uri = list.first();
0384             const MapSS device = m_mappedDevices[uri];
0385             insertDevice(device[KCUPS_DEVICE_CLASS],
0386                          device[KCUPS_DEVICE_ID],
0387                          device[KCUPS_DEVICE_INFO],
0388                          device[KCUPS_DEVICE_MAKE_AND_MODEL],
0389                          uri,
0390                          device[KCUPS_DEVICE_LOCATION],
0391                          list.size() > 1 ? list : QStringList());
0392         }
0393     } else {
0394         qWarning() << "Unexpected message" << message;
0395         groupedDevicesFallback();
0396     }
0397     Q_EMIT loaded();
0398 }
0399 
0400 void DevicesModel::getGroupedDevicesFailed(const QDBusError &error, const QDBusMessage &message)
0401 {
0402     qWarning() << error << message;
0403     groupedDevicesFallback();
0404     Q_EMIT errorMessage(i18n("Failed to group devices: '%1'", error.message()));
0405     Q_EMIT loaded();
0406 }
0407 
0408 void DevicesModel::groupedDevicesFallback()
0409 {
0410     MapSMapSS::const_iterator i = m_mappedDevices.constBegin();
0411     while (i != m_mappedDevices.constEnd()) {
0412         const MapSS device = i.value();
0413         insertDevice(device[KCUPS_DEVICE_CLASS],
0414                      device[KCUPS_DEVICE_ID],
0415                      device[KCUPS_DEVICE_INFO],
0416                      device[KCUPS_DEVICE_MAKE_AND_MODEL],
0417                      i.key(),
0418                      device[KCUPS_DEVICE_LOCATION]);
0419         ++i;
0420     }
0421 }
0422 
0423 QStandardItem *DevicesModel::findCreateCategory(const QString &category, Kind kind)
0424 {
0425     for (int i = 0; i < rowCount(); ++i) {
0426         QStandardItem *catItem = item(i);
0427         if (catItem->data(Qt::UserRole).toInt() == kind) {
0428             return catItem;
0429         }
0430     }
0431 
0432     int pos = 0;
0433     for (int i = 0; i < rowCount(); ++i, ++pos) {
0434         QStandardItem *catItem = item(i);
0435         if (catItem->data(Qt::UserRole).toInt() > kind) {
0436             pos = i;
0437             break;
0438         }
0439     }
0440 
0441     auto catItem = new QStandardItem(category);
0442     QFont font = catItem->font();
0443     font.setBold(true);
0444     catItem->setFont(font);
0445     catItem->setData(kind, Qt::UserRole);
0446     catItem->setFlags(Qt::ItemIsEnabled);
0447     insertRow(pos, catItem);
0448 
0449     // Emit the parent so the view expand the item
0450     Q_EMIT parentAdded(indexFromItem(catItem));
0451 
0452     return catItem;
0453 }
0454 
0455 #include "moc_DevicesModel.cpp"