File indexing completed on 2024-11-24 04:55:02

0001 /*
0002  *   SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include "SnapResource.h"
0008 #include "SnapBackend.h"
0009 #include <KLocalizedString>
0010 #include <QBuffer>
0011 #include <QDebug>
0012 #include <QImageReader>
0013 #include <QProcess>
0014 #include <QStandardItemModel>
0015 
0016 #ifdef SNAP_MARKDOWN
0017 #include <Snapd/MarkdownParser>
0018 #endif
0019 
0020 #include <appstream/AppStreamUtils.h>
0021 #include <utils.h>
0022 
0023 using namespace Qt::StringLiterals;
0024 
0025 QDebug operator<<(QDebug debug, const QSnapdPlug &plug)
0026 {
0027     QDebugStateSaver saver(debug);
0028     debug.nospace() << "QSnapdPlug(";
0029     debug.nospace() << "name:" << plug.name() << ',';
0030     debug.nospace() << "snap:" << plug.snap() << ',';
0031     debug.nospace() << "label:" << plug.label() << ',';
0032     debug.nospace() << "interface:" << plug.interface() << ',';
0033     // debug.nospace() << "connectionCount:" << plug.connectionSlotCount();
0034     debug.nospace() << ')';
0035     return debug;
0036 }
0037 
0038 QDebug operator<<(QDebug debug, const QSnapdSlot &slot)
0039 {
0040     QDebugStateSaver saver(debug);
0041     debug.nospace() << "QSnapdSlot(";
0042     debug.nospace() << "name:" << slot.name() << ',';
0043     debug.nospace() << "label:" << slot.label() << ',';
0044     debug.nospace() << "snap:" << slot.snap() << ',';
0045     debug.nospace() << "interface:" << slot.interface() << ',';
0046     // debug.nospace() << "connectionCount:" << slot.connectionSlotCount();
0047     debug.nospace() << ')';
0048     return debug;
0049 }
0050 
0051 QDebug operator<<(QDebug debug, const QSnapdPlug *plug)
0052 {
0053     QDebugStateSaver saver(debug);
0054     debug.nospace() << "*" << *plug;
0055     return debug;
0056 }
0057 
0058 QDebug operator<<(QDebug debug, const QSnapdSlot *slot)
0059 {
0060     QDebugStateSaver saver(debug);
0061     debug.nospace() << "*" << *slot;
0062     return debug;
0063 }
0064 
0065 const QStringList SnapResource::s_topObjects({QStringLiteral("qrc:/qml/PermissionsButton.qml")
0066 #ifdef SNAP_CHANNELS
0067                                                   ,
0068                                               QStringLiteral("qrc:/qml/ChannelsButton.qml")
0069 #endif
0070 });
0071 
0072 SnapResource::SnapResource(QSharedPointer<QSnapdSnap> snap, AbstractResource::State state, SnapBackend *backend)
0073     : AbstractResource(backend)
0074     , m_state(state)
0075     , m_snap(snap)
0076 {
0077     setObjectName(snap->name());
0078 }
0079 
0080 QSnapdClient *SnapResource::client() const
0081 {
0082     auto backend = qobject_cast<SnapBackend *>(parent());
0083     return backend->client();
0084 }
0085 
0086 QString SnapResource::availableVersion() const
0087 {
0088     return installedVersion();
0089 }
0090 
0091 QStringList SnapResource::categories()
0092 {
0093     return {QStringLiteral("Application")};
0094 }
0095 
0096 QString SnapResource::comment()
0097 {
0098     return m_snap->summary();
0099 }
0100 
0101 quint64 SnapResource::size()
0102 {
0103     // return isInstalled() ? m_snap->installedSize() : m_snap->downloadSize();
0104     return m_snap->downloadSize();
0105 }
0106 
0107 QVariant SnapResource::icon() const
0108 {
0109     if (m_icon.isNull()) {
0110         m_icon = [this]() -> QVariant {
0111             const auto iconPath = m_snap->icon();
0112             if (iconPath.isEmpty())
0113                 return QStringLiteral("package-x-generic");
0114 
0115             if (!iconPath.startsWith(QLatin1Char('/')))
0116                 return QUrl(iconPath);
0117 
0118             auto req = client()->getIcon(packageName());
0119             connect(req, &QSnapdGetIconRequest::complete, this, &SnapResource::gotIcon);
0120             req->runAsync();
0121             return {};
0122         }();
0123     }
0124     return m_icon;
0125 }
0126 
0127 void SnapResource::gotIcon()
0128 {
0129     auto req = qobject_cast<QSnapdGetIconRequest *>(sender());
0130     if (req->error()) {
0131         qWarning() << "icon error" << req->errorString();
0132         return;
0133     }
0134 
0135     auto icon = req->icon();
0136 
0137     QBuffer buffer;
0138     buffer.setData(icon->data());
0139     QImageReader reader(&buffer);
0140 
0141     auto theIcon = QVariant::fromValue<QImage>(reader.read());
0142     if (theIcon != m_icon) {
0143         m_icon = theIcon;
0144         Q_EMIT iconChanged();
0145     }
0146 }
0147 
0148 QString SnapResource::installedVersion() const
0149 {
0150     return m_snap->version();
0151 }
0152 
0153 QJsonArray SnapResource::licenses()
0154 {
0155     return AppStreamUtils::licenses(m_snap->license());
0156 }
0157 
0158 #ifdef SNAP_MARKDOWN
0159 static QString serialize_node(QSnapdMarkdownNode &node);
0160 
0161 static QString serialize_children(QSnapdMarkdownNode &node)
0162 {
0163     QString result;
0164     for (int i = 0; i < node.childCount(); i++) {
0165         QScopedPointer<QSnapdMarkdownNode> child(node.child(i));
0166         result += serialize_node(*child);
0167     }
0168     return result;
0169 }
0170 
0171 static QString serialize_node(QSnapdMarkdownNode &node)
0172 {
0173     switch (node.type()) {
0174     case QSnapdMarkdownNode::NodeTypeText:
0175         return node.text().toHtmlEscaped();
0176 
0177     case QSnapdMarkdownNode::NodeTypeParagraph:
0178         return QLatin1String("<p>") + serialize_children(node) + QLatin1String("</p>\n");
0179 
0180     case QSnapdMarkdownNode::NodeTypeUnorderedList:
0181         return QLatin1String("<ul>\n") + serialize_children(node) + QLatin1String("</ul>\n");
0182 
0183     case QSnapdMarkdownNode::NodeTypeListItem:
0184         if (node.childCount() == 0)
0185             return QLatin1String("<li></li>\n");
0186         if (node.childCount() == 1) {
0187             QScopedPointer<QSnapdMarkdownNode> child(node.child(0));
0188             if (child->type() == QSnapdMarkdownNode::NodeTypeParagraph)
0189                 return QLatin1String("<li>") + serialize_children(*child) + QLatin1String("</li>\n");
0190         }
0191         return QLatin1String("<li>\n") + serialize_children(node) + QLatin1String("</li>\n");
0192 
0193     case QSnapdMarkdownNode::NodeTypeCodeBlock:
0194         return QLatin1String("<pre><code>") + serialize_children(node) + QLatin1String("</code></pre>\n");
0195 
0196     case QSnapdMarkdownNode::NodeTypeCodeSpan:
0197         return QLatin1String("<code>") + serialize_children(node) + QLatin1String("</code>");
0198 
0199     case QSnapdMarkdownNode::NodeTypeEmphasis:
0200         return QLatin1String("<em>") + serialize_children(node) + QLatin1String("</em>");
0201 
0202     case QSnapdMarkdownNode::NodeTypeStrongEmphasis:
0203         return QLatin1String("<strong>") + serialize_children(node) + QLatin1String("</strong>");
0204 
0205     case QSnapdMarkdownNode::NodeTypeUrl:
0206         return serialize_children(node);
0207 
0208     default:
0209         return QString();
0210     }
0211 }
0212 #endif
0213 
0214 QString SnapResource::longDescription()
0215 {
0216 #ifdef SNAP_MARKDOWN
0217     QSnapdMarkdownParser parser(QSnapdMarkdownParser::MarkdownVersion0);
0218     QList<QSnapdMarkdownNode> nodes = parser.parse(m_snap->description());
0219     QString result;
0220     for (int i = 0; i < nodes.size(); i++)
0221         result += serialize_node(nodes[i]);
0222     return result;
0223 #else
0224     return m_snap->description();
0225 #endif
0226 }
0227 
0228 QString SnapResource::name() const
0229 {
0230     return m_snap->title().isEmpty() ? m_snap->name() : m_snap->title();
0231 }
0232 
0233 QString SnapResource::origin() const
0234 {
0235     return QStringLiteral("Snap");
0236 }
0237 
0238 QString SnapResource::packageName() const
0239 {
0240     return m_snap->name();
0241 }
0242 
0243 QString SnapResource::section()
0244 {
0245     return QStringLiteral("snap");
0246 }
0247 
0248 AbstractResource::State SnapResource::state()
0249 {
0250     return m_state;
0251 }
0252 
0253 void SnapResource::setState(AbstractResource::State state)
0254 {
0255     if (m_state != state) {
0256         m_state = state;
0257         Q_EMIT stateChanged();
0258     }
0259 }
0260 
0261 void SnapResource::fetchChangelog()
0262 {
0263     QString log;
0264     Q_EMIT changelogFetched(log);
0265 }
0266 
0267 void SnapResource::fetchScreenshots()
0268 {
0269     Screenshots screenshots;
0270 #ifdef SNAP_MEDIA
0271     for (int i = 0, c = m_snap->mediaCount(); i < c; ++i) {
0272         QScopedPointer<QSnapdMedia> media(m_snap->media(i));
0273         if (media->type() == QLatin1String("screenshot"))
0274             screenshots << QUrl(media->url());
0275     }
0276 #else
0277     for (int i = 0, c = m_snap->screenshotCount(); i < c; ++i) {
0278         QScopedPointer<QSnapdScreenshot> screenshot(m_snap->screenshot(i));
0279         screenshots << QUrl(screenshot->url());
0280     }
0281 #endif
0282     Q_EMIT screenshotsFetched(screenshots);
0283 }
0284 
0285 void SnapResource::invokeApplication() const
0286 {
0287     QProcess::startDetached(QStringLiteral("snap"), {QStringLiteral("run"), packageName()});
0288 }
0289 
0290 AbstractResource::Type SnapResource::type() const
0291 {
0292     return m_snap->snapType() != QSnapdEnums::SnapTypeApp ? Application : Technical;
0293 }
0294 
0295 void SnapResource::setSnap(const QSharedPointer<QSnapdSnap> &snap)
0296 {
0297     Q_ASSERT(snap->name() == m_snap->name());
0298     if (m_snap == snap)
0299         return;
0300 
0301     const bool newSize = m_snap->installedSize() != snap->installedSize() || m_snap->downloadSize() != snap->downloadSize();
0302     m_snap = snap;
0303     if (newSize)
0304         Q_EMIT sizeChanged();
0305 
0306     Q_EMIT newSnap();
0307 }
0308 
0309 QDate SnapResource::releaseDate() const
0310 {
0311     return {};
0312 }
0313 
0314 class PlugsModel : public QStandardItemModel
0315 {
0316     Q_OBJECT
0317 public:
0318     enum Roles {
0319         PlugNameRole = Qt::UserRole + 1,
0320         SlotSnapRole,
0321         SlotNameRole,
0322     };
0323 
0324     PlugsModel(SnapResource *res, SnapBackend *backend, QObject *parent)
0325         : QStandardItemModel(parent)
0326         , m_res(res)
0327         , m_backend(backend)
0328     {
0329         auto roles = roleNames();
0330         roles.insert(Qt::CheckStateRole, "checked");
0331         setItemRoleNames(roles);
0332 
0333         auto req = backend->client()->getInterfaces();
0334         req->runSync();
0335 
0336         QHash<QString, QVector<QSnapdSlot *>> slotsForInterface;
0337         for (int i = 0; i < req->slotCount(); ++i) {
0338             const auto slot = req->slot(i);
0339             slot->setParent(this);
0340             slotsForInterface[slot->interface()].append(slot);
0341         }
0342 
0343         const auto snap = m_res->snap();
0344         for (int i = 0; i < req->plugCount(); ++i) {
0345             const QScopedPointer<QSnapdPlug> plug(req->plug(i));
0346             if (plug->snap() == snap->name()) {
0347                 if (plug->interface() == QLatin1String("content"))
0348                     continue;
0349 
0350                 const auto theSlots = slotsForInterface.value(plug->interface());
0351                 for (auto slot : theSlots) {
0352                     auto item = new QStandardItem;
0353                     if (plug->label().isEmpty())
0354                         item->setText(plug->name());
0355                     else
0356                         item->setText(i18n("%1 - %2", plug->name(), plug->label()));
0357 
0358                     // qDebug() << "xxx" << plug->name() << plug->label() << plug->interface() << slot->snap() << "slot:" << slot->name() <<
0359                     // slot->snap() << slot->interface() << slot->label();
0360                     item->setCheckable(true);
0361                     item->setCheckState(plug->connectionCount() > 0 ? Qt::Checked : Qt::Unchecked);
0362                     item->setData(plug->name(), PlugNameRole);
0363                     item->setData(slot->snap(), SlotSnapRole);
0364                     item->setData(slot->name(), SlotNameRole);
0365                     appendRow(item);
0366                 }
0367             }
0368         }
0369     }
0370 
0371 Q_SIGNALS:
0372     void error(InlineMessage *message);
0373 
0374 private:
0375     bool setData(const QModelIndex &index, const QVariant &value, int role) override
0376     {
0377         if (role != Qt::CheckStateRole)
0378             return QStandardItemModel::setData(index, value, role);
0379 
0380         auto item = itemFromIndex(index);
0381         Q_ASSERT(item);
0382         const QString plugName = item->data(PlugNameRole).toString();
0383         const QString slotSnap = item->data(SlotSnapRole).toString();
0384         const QString slotName = item->data(SlotNameRole).toString();
0385 
0386         QSnapdRequest *req;
0387 
0388         const auto snap = m_res->snap();
0389         if (item->checkState() == Qt::Checked) {
0390             req = m_backend->client()->disconnectInterface(snap->name(), plugName, slotSnap, slotName);
0391         } else {
0392             req = m_backend->client()->connectInterface(snap->name(), plugName, slotSnap, slotName);
0393         }
0394         req->runSync();
0395         if (req->error()) {
0396             qWarning() << "snapd error" << req->errorString();
0397             Q_EMIT error(new InlineMessage(InlineMessage::Error, u"error"_s, req->errorString()));
0398         }
0399         return req->error() == QSnapdRequest::NoError;
0400     }
0401 
0402     SnapResource *const m_res;
0403     SnapBackend *const m_backend;
0404 };
0405 
0406 QAbstractItemModel *SnapResource::plugs(QObject *p)
0407 {
0408     if (!isInstalled())
0409         return nullptr;
0410 
0411     return new PlugsModel(this, qobject_cast<SnapBackend *>(parent()), p);
0412 }
0413 
0414 QString SnapResource::appstreamId() const
0415 {
0416     const QStringList ids
0417 #if defined(SNAP_COMMON_IDS)
0418         = m_snap->commonIds()
0419 #endif
0420         ;
0421     return ids.isEmpty() ? QLatin1String("io.snapcraft.") + m_snap->name() + QLatin1Char('-') + m_snap->id() : ids.first();
0422 }
0423 
0424 QStringList SnapResource::topObjects() const
0425 {
0426     return s_topObjects;
0427 }
0428 
0429 QString SnapResource::channel() const
0430 {
0431 #ifdef SNAP_PUBLISHER
0432     auto req = client()->getSnap(packageName());
0433 #else
0434     auto req = client()->listOne(packageName());
0435 #endif
0436     req->runSync();
0437     return req->error() ? QString() : req->snap()->trackingChannel();
0438 }
0439 
0440 QString SnapResource::author() const
0441 {
0442 #ifdef SNAP_PUBLISHER
0443     QString author = m_snap->publisherDisplayName();
0444     if (m_snap->publisherValidation() == QSnapdEnums::PublisherValidationVerified) {
0445         author += QStringLiteral(" ✅");
0446     }
0447 #else
0448     QString author;
0449 #endif
0450 
0451     return author;
0452 }
0453 
0454 void SnapResource::setChannel(const QString &channelName)
0455 {
0456 #ifdef SNAP_CHANNELS
0457     Q_ASSERT(isInstalled());
0458     auto request = client()->switchChannel(m_snap->name(), channelName);
0459 
0460     const auto currentChannel = channel();
0461     request->runAsync();
0462     connect(request, &QSnapdRequest::complete, this, [this, currentChannel]() {
0463         const auto newChannel = channel();
0464         if (newChannel != currentChannel) {
0465             Q_EMIT channelChanged(newChannel);
0466         }
0467     });
0468 #endif
0469 }
0470 
0471 void SnapResource::refreshSnap()
0472 {
0473     auto request = client()->find(QSnapdClient::FindFlag::MatchName, m_snap->name());
0474     connect(request, &QSnapdRequest::complete, this, [this, request]() {
0475         if (request->error()) {
0476             qWarning() << "error" << request->error() << ": " << request->errorString();
0477             return;
0478         }
0479         Q_ASSERT(request->snapCount() == 1);
0480         setSnap(QSharedPointer<QSnapdSnap>(request->snap(0)));
0481     });
0482     request->runAsync();
0483 }
0484 
0485 #ifdef SNAP_CHANNELS
0486 class Channels : public QObject
0487 {
0488     Q_OBJECT
0489     Q_PROPERTY(QList<QObject *> channels READ channels NOTIFY channelsChanged)
0490 
0491 public:
0492     Channels(SnapResource *res, QObject *parent)
0493         : QObject(parent)
0494         , m_res(res)
0495     {
0496         if (res->snap()->channelCount() == 0)
0497             res->refreshSnap();
0498         else
0499             refreshChannels();
0500 
0501         connect(res, &SnapResource::newSnap, this, &Channels::refreshChannels);
0502     }
0503 
0504     void refreshChannels()
0505     {
0506         qDeleteAll(m_channels);
0507         m_channels.clear();
0508 
0509         auto s = m_res->snap();
0510         for (int i = 0, c = s->channelCount(); i < c; ++i) {
0511             auto channel = s->channel(i);
0512             channel->setParent(this);
0513             m_channels << channel;
0514         }
0515         Q_EMIT channelsChanged();
0516     }
0517 
0518     QList<QObject *> channels() const
0519     {
0520         return m_channels;
0521     }
0522 
0523 Q_SIGNALS:
0524     void channelsChanged();
0525 
0526 private:
0527     QList<QObject *> m_channels;
0528     SnapResource *const m_res;
0529 };
0530 
0531 #endif
0532 
0533 QObject *SnapResource::channels(QObject *parent)
0534 {
0535 #ifdef SNAP_CHANNELS
0536     return new Channels(this, parent);
0537 #else
0538     return nullptr;
0539 #endif
0540 }
0541 
0542 #include "SnapResource.moc"