1 /*
2  *   SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
3  *
4  *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
5  */
6 
7 #include "SnapResource.h"
8 #include "SnapBackend.h"
9 #include <KLocalizedString>
10 #include <QBuffer>
11 #include <QDebug>
12 #include <QImageReader>
13 #include <QProcess>
14 #include <QStandardItemModel>
15 
16 #ifdef SNAP_MARKDOWN
17 #include <Snapd/MarkdownParser>
18 #endif
19 
20 #include <utils.h>
21 
operator <<(QDebug debug,const QSnapdPlug & plug)22 QDebug operator<<(QDebug debug, const QSnapdPlug &plug)
23 {
24     QDebugStateSaver saver(debug);
25     debug.nospace() << "QSnapdPlug(";
26     debug.nospace() << "name:" << plug.name() << ',';
27     debug.nospace() << "snap:" << plug.snap() << ',';
28     debug.nospace() << "label:" << plug.label() << ',';
29     debug.nospace() << "interface:" << plug.interface() << ',';
30     // debug.nospace() << "connectionCount:" << plug.connectionSlotCount();
31     debug.nospace() << ')';
32     return debug;
33 }
34 
operator <<(QDebug debug,const QSnapdSlot & slot)35 QDebug operator<<(QDebug debug, const QSnapdSlot &slot)
36 {
37     QDebugStateSaver saver(debug);
38     debug.nospace() << "QSnapdSlot(";
39     debug.nospace() << "name:" << slot.name() << ',';
40     debug.nospace() << "label:" << slot.label() << ',';
41     debug.nospace() << "snap:" << slot.snap() << ',';
42     debug.nospace() << "interface:" << slot.interface() << ',';
43     // debug.nospace() << "connectionCount:" << slot.connectionSlotCount();
44     debug.nospace() << ')';
45     return debug;
46 }
47 
operator <<(QDebug debug,const QSnapdPlug * plug)48 QDebug operator<<(QDebug debug, const QSnapdPlug *plug)
49 {
50     QDebugStateSaver saver(debug);
51     debug.nospace() << "*" << *plug;
52     return debug;
53 }
54 
operator <<(QDebug debug,const QSnapdSlot * slot)55 QDebug operator<<(QDebug debug, const QSnapdSlot *slot)
56 {
57     QDebugStateSaver saver(debug);
58     debug.nospace() << "*" << *slot;
59     return debug;
60 }
61 
62 const QStringList SnapResource::m_objects({QStringLiteral("qrc:/qml/PermissionsButton.qml")
63 #ifdef SNAP_CHANNELS
64                                                ,
65                                            QStringLiteral("qrc:/qml/ChannelsButton.qml")
66 #endif
67 });
68 
SnapResource(QSharedPointer<QSnapdSnap> snap,AbstractResource::State state,SnapBackend * backend)69 SnapResource::SnapResource(QSharedPointer<QSnapdSnap> snap, AbstractResource::State state, SnapBackend *backend)
70     : AbstractResource(backend)
71     , m_state(state)
72     , m_snap(snap)
73 {
74     setObjectName(snap->name());
75 }
76 
client() const77 QSnapdClient *SnapResource::client() const
78 {
79     auto backend = qobject_cast<SnapBackend *>(parent());
80     return backend->client();
81 }
82 
availableVersion() const83 QString SnapResource::availableVersion() const
84 {
85     return installedVersion();
86 }
87 
categories()88 QStringList SnapResource::categories()
89 {
90     return {QStringLiteral("Application")};
91 }
92 
comment()93 QString SnapResource::comment()
94 {
95     return m_snap->summary();
96 }
97 
size()98 int SnapResource::size()
99 {
100     // return isInstalled() ? m_snap->installedSize() : m_snap->downloadSize();
101     return m_snap->downloadSize();
102 }
103 
icon() const104 QVariant SnapResource::icon() const
105 {
106     if (m_icon.isNull()) {
107         m_icon = [this]() -> QVariant {
108             const auto iconPath = m_snap->icon();
109             if (iconPath.isEmpty())
110                 return QStringLiteral("package-x-generic");
111 
112             if (!iconPath.startsWith(QLatin1Char('/')))
113                 return QUrl(iconPath);
114 
115             auto req = client()->getIcon(packageName());
116             connect(req, &QSnapdGetIconRequest::complete, this, &SnapResource::gotIcon);
117             req->runAsync();
118             return {};
119         }();
120     }
121     return m_icon;
122 }
123 
gotIcon()124 void SnapResource::gotIcon()
125 {
126     auto req = qobject_cast<QSnapdGetIconRequest *>(sender());
127     if (req->error()) {
128         qWarning() << "icon error" << req->errorString();
129         return;
130     }
131 
132     auto icon = req->icon();
133 
134     QBuffer buffer;
135     buffer.setData(icon->data());
136     QImageReader reader(&buffer);
137 
138     auto theIcon = QVariant::fromValue<QImage>(reader.read());
139     if (theIcon != m_icon) {
140         m_icon = theIcon;
141         Q_EMIT iconChanged();
142     }
143 }
144 
installedVersion() const145 QString SnapResource::installedVersion() const
146 {
147     return m_snap->version();
148 }
149 
licenses()150 QJsonArray SnapResource::licenses()
151 {
152     return {QJsonObject{{QStringLiteral("name"), m_snap->license()}}};
153 }
154 
155 #ifdef SNAP_MARKDOWN
156 static QString serialize_node(QSnapdMarkdownNode &node);
157 
serialize_children(QSnapdMarkdownNode & node)158 static QString serialize_children(QSnapdMarkdownNode &node)
159 {
160     QString result;
161     for (int i = 0; i < node.childCount(); i++) {
162         QScopedPointer<QSnapdMarkdownNode> child(node.child(i));
163         result += serialize_node(*child);
164     }
165     return result;
166 }
167 
serialize_node(QSnapdMarkdownNode & node)168 static QString serialize_node(QSnapdMarkdownNode &node)
169 {
170     switch (node.type()) {
171     case QSnapdMarkdownNode::NodeTypeText:
172         return node.text().toHtmlEscaped();
173 
174     case QSnapdMarkdownNode::NodeTypeParagraph:
175         return QLatin1String("<p>") + serialize_children(node) + QLatin1String("</p>\n");
176 
177     case QSnapdMarkdownNode::NodeTypeUnorderedList:
178         return QLatin1String("<ul>\n") + serialize_children(node) + QLatin1String("</ul>\n");
179 
180     case QSnapdMarkdownNode::NodeTypeListItem:
181         if (node.childCount() == 0)
182             return QLatin1String("<li></li>\n");
183         if (node.childCount() == 1) {
184             QScopedPointer<QSnapdMarkdownNode> child(node.child(0));
185             if (child->type() == QSnapdMarkdownNode::NodeTypeParagraph)
186                 return QLatin1String("<li>") + serialize_children(*child) + QLatin1String("</li>\n");
187         }
188         return QLatin1String("<li>\n") + serialize_children(node) + QLatin1String("</li>\n");
189 
190     case QSnapdMarkdownNode::NodeTypeCodeBlock:
191         return QLatin1String("<pre><code>") + serialize_children(node) + QLatin1String("</code></pre>\n");
192 
193     case QSnapdMarkdownNode::NodeTypeCodeSpan:
194         return QLatin1String("<code>") + serialize_children(node) + QLatin1String("</code>");
195 
196     case QSnapdMarkdownNode::NodeTypeEmphasis:
197         return QLatin1String("<em>") + serialize_children(node) + QLatin1String("</em>");
198 
199     case QSnapdMarkdownNode::NodeTypeStrongEmphasis:
200         return QLatin1String("<strong>") + serialize_children(node) + QLatin1String("</strong>");
201 
202     case QSnapdMarkdownNode::NodeTypeUrl:
203         return serialize_children(node);
204 
205     default:
206         return QString();
207     }
208 }
209 #endif
210 
longDescription()211 QString SnapResource::longDescription()
212 {
213 #ifdef SNAP_MARKDOWN
214     QSnapdMarkdownParser parser(QSnapdMarkdownParser::MarkdownVersion0);
215     QList<QSnapdMarkdownNode> nodes = parser.parse(m_snap->description());
216     QString result;
217     for (int i = 0; i < nodes.size(); i++)
218         result += serialize_node(nodes[i]);
219     return result;
220 #else
221     return m_snap->description();
222 #endif
223 }
224 
name() const225 QString SnapResource::name() const
226 {
227     return m_snap->title().isEmpty() ? m_snap->name() : m_snap->title();
228 }
229 
origin() const230 QString SnapResource::origin() const
231 {
232     return QStringLiteral("Snap");
233 }
234 
packageName() const235 QString SnapResource::packageName() const
236 {
237     return m_snap->name();
238 }
239 
section()240 QString SnapResource::section()
241 {
242     return QStringLiteral("snap");
243 }
244 
state()245 AbstractResource::State SnapResource::state()
246 {
247     return m_state;
248 }
249 
setState(AbstractResource::State state)250 void SnapResource::setState(AbstractResource::State state)
251 {
252     if (m_state != state) {
253         m_state = state;
254         Q_EMIT stateChanged();
255     }
256 }
257 
fetchChangelog()258 void SnapResource::fetchChangelog()
259 {
260     QString log;
261     Q_EMIT changelogFetched(log);
262 }
263 
fetchScreenshots()264 void SnapResource::fetchScreenshots()
265 {
266     QList<QUrl> screenshots;
267 #ifdef SNAP_MEDIA
268     for (int i = 0, c = m_snap->mediaCount(); i < c; ++i) {
269         QScopedPointer<QSnapdMedia> media(m_snap->media(i));
270         if (media->type() == QLatin1String("screenshot"))
271             screenshots << QUrl(media->url());
272     }
273 #else
274     for (int i = 0, c = m_snap->screenshotCount(); i < c; ++i) {
275         QScopedPointer<QSnapdScreenshot> screenshot(m_snap->screenshot(i));
276         screenshots << QUrl(screenshot->url());
277     }
278 #endif
279     Q_EMIT screenshotsFetched(screenshots, screenshots);
280 }
281 
invokeApplication() const282 void SnapResource::invokeApplication() const
283 {
284     QProcess::startDetached(QStringLiteral("snap"), {QStringLiteral("run"), packageName()});
285 }
286 
type() const287 AbstractResource::Type SnapResource::type() const
288 {
289     return m_snap->snapType() != QLatin1String("app") ? Application : Technical;
290 }
291 
setSnap(const QSharedPointer<QSnapdSnap> & snap)292 void SnapResource::setSnap(const QSharedPointer<QSnapdSnap> &snap)
293 {
294     Q_ASSERT(snap->name() == m_snap->name());
295     if (m_snap == snap)
296         return;
297 
298     const bool newSize = m_snap->installedSize() != snap->installedSize() || m_snap->downloadSize() != snap->downloadSize();
299     m_snap = snap;
300     if (newSize)
301         Q_EMIT sizeChanged();
302 
303     Q_EMIT newSnap();
304 }
305 
releaseDate() const306 QDate SnapResource::releaseDate() const
307 {
308     return {};
309 }
310 
311 class PlugsModel : public QStandardItemModel
312 {
313 public:
314     enum Roles {
315         PlugNameRole = Qt::UserRole + 1,
316         SlotSnapRole,
317         SlotNameRole,
318     };
319 
PlugsModel(SnapResource * res,SnapBackend * backend,QObject * parent)320     PlugsModel(SnapResource *res, SnapBackend *backend, QObject *parent)
321         : QStandardItemModel(parent)
322         , m_res(res)
323         , m_backend(backend)
324     {
325         auto roles = roleNames();
326         roles.insert(Qt::CheckStateRole, "checked");
327         setItemRoleNames(roles);
328 
329         auto req = backend->client()->getInterfaces();
330         req->runSync();
331 
332         QHash<QString, QVector<QSnapdSlot *>> slotsForInterface;
333         for (int i = 0; i < req->slotCount(); ++i) {
334             const auto slot = req->slot(i);
335             slot->setParent(this);
336             slotsForInterface[slot->interface()].append(slot);
337         }
338 
339         const auto snap = m_res->snap();
340         for (int i = 0; i < req->plugCount(); ++i) {
341             const QScopedPointer<QSnapdPlug> plug(req->plug(i));
342             if (plug->snap() == snap->name()) {
343                 if (plug->interface() == QLatin1String("content"))
344                     continue;
345 
346                 const auto theSlots = slotsForInterface.value(plug->interface());
347                 for (auto slot : theSlots) {
348                     auto item = new QStandardItem;
349                     if (plug->label().isEmpty())
350                         item->setText(plug->name());
351                     else
352                         item->setText(i18n("%1 - %2", plug->name(), plug->label()));
353 
354                     // qDebug() << "xxx" << plug->name() << plug->label() << plug->interface() << slot->snap() << "slot:" << slot->name() <<
355                     // slot->snap() << slot->interface() << slot->label();
356                     item->setCheckable(true);
357                     item->setCheckState(plug->connectionCount() > 0 ? Qt::Checked : Qt::Unchecked);
358                     item->setData(plug->name(), PlugNameRole);
359                     item->setData(slot->snap(), SlotSnapRole);
360                     item->setData(slot->name(), SlotNameRole);
361                     appendRow(item);
362                 }
363             }
364         }
365     }
366 
367 private:
setData(const QModelIndex & index,const QVariant & value,int role)368     bool setData(const QModelIndex &index, const QVariant &value, int role) override
369     {
370         if (role != Qt::CheckStateRole)
371             return QStandardItemModel::setData(index, value, role);
372 
373         auto item = itemFromIndex(index);
374         Q_ASSERT(item);
375         const QString plugName = item->data(PlugNameRole).toString();
376         const QString slotSnap = item->data(SlotSnapRole).toString();
377         const QString slotName = item->data(SlotNameRole).toString();
378 
379         QSnapdRequest *req;
380 
381         const auto snap = m_res->snap();
382         if (item->checkState() == Qt::Checked) {
383             req = m_backend->client()->disconnectInterface(snap->name(), plugName, slotSnap, slotName);
384         } else {
385             req = m_backend->client()->connectInterface(snap->name(), plugName, slotSnap, slotName);
386         }
387         req->runSync();
388         if (req->error()) {
389             qWarning() << "snapd error" << req->errorString();
390             Q_EMIT m_res->backend()->passiveMessage(req->errorString());
391         }
392         return req->error() == QSnapdRequest::NoError;
393     }
394 
395     SnapResource *const m_res;
396     SnapBackend *const m_backend;
397 };
398 
plugs(QObject * p)399 QAbstractItemModel *SnapResource::plugs(QObject *p)
400 {
401     if (!isInstalled())
402         return new QStandardItemModel(p);
403 
404     return new PlugsModel(this, qobject_cast<SnapBackend *>(parent()), p);
405 }
406 
appstreamId() const407 QString SnapResource::appstreamId() const
408 {
409     const QStringList ids
410 #if defined(SNAP_COMMON_IDS)
411         = m_snap->commonIds()
412 #endif
413         ;
414     return ids.isEmpty() ? QLatin1String("io.snapcraft.") + m_snap->name() + QLatin1Char('-') + m_snap->id() : ids.first();
415 }
416 
channel() const417 QString SnapResource::channel() const
418 {
419 #ifdef SNAP_PUBLISHER
420     auto req = client()->getSnap(packageName());
421 #else
422     auto req = client()->listOne(packageName());
423 #endif
424     req->runSync();
425     return req->error() ? QString() : req->snap()->trackingChannel();
426 }
427 
author() const428 QString SnapResource::author() const
429 {
430 #ifdef SNAP_PUBLISHER
431     QString author = m_snap->publisherDisplayName();
432     if (m_snap->publisherValidation() == QSnapdEnums::PublisherValidationVerified) {
433         author += QStringLiteral(" ✅");
434     }
435 #else
436     QString author;
437 #endif
438 
439     return author;
440 }
441 
setChannel(const QString & channelName)442 void SnapResource::setChannel(const QString &channelName)
443 {
444 #ifdef SNAP_CHANNELS
445     Q_ASSERT(isInstalled());
446     auto request = client()->switchChannel(m_snap->name(), channelName);
447 
448     const auto currentChannel = channel();
449     request->runAsync();
450     connect(request, &QSnapdRequest::complete, this, [this, currentChannel]() {
451         const auto newChannel = channel();
452         if (newChannel != currentChannel) {
453             Q_EMIT channelChanged(newChannel);
454         }
455     });
456 #endif
457 }
458 
refreshSnap()459 void SnapResource::refreshSnap()
460 {
461     auto request = client()->find(QSnapdClient::FindFlag::MatchName, m_snap->name());
462     connect(request, &QSnapdRequest::complete, this, [this, request]() {
463         if (request->error()) {
464             qWarning() << "error" << request->error() << ": " << request->errorString();
465             return;
466         }
467         Q_ASSERT(request->snapCount() == 1);
468         setSnap(QSharedPointer<QSnapdSnap>(request->snap(0)));
469     });
470     request->runAsync();
471 }
472 
473 #ifdef SNAP_CHANNELS
474 class Channels : public QObject
475 {
476     Q_OBJECT
477     Q_PROPERTY(QList<QObject *> channels READ channels NOTIFY channelsChanged)
478 
479 public:
Channels(SnapResource * res,QObject * parent)480     Channels(SnapResource *res, QObject *parent)
481         : QObject(parent)
482         , m_res(res)
483     {
484         if (res->snap()->channelCount() == 0)
485             res->refreshSnap();
486         else
487             refreshChannels();
488 
489         connect(res, &SnapResource::newSnap, this, &Channels::refreshChannels);
490     }
491 
refreshChannels()492     void refreshChannels()
493     {
494         qDeleteAll(m_channels);
495         m_channels.clear();
496 
497         auto s = m_res->snap();
498         for (int i = 0, c = s->channelCount(); i < c; ++i) {
499             auto channel = s->channel(i);
500             channel->setParent(this);
501             m_channels << channel;
502         }
503         Q_EMIT channelsChanged();
504     }
505 
channels() const506     QList<QObject *> channels() const
507     {
508         return m_channels;
509     }
510 
511 Q_SIGNALS:
512     void channelsChanged();
513 
514 private:
515     QList<QObject *> m_channels;
516     SnapResource *const m_res;
517 };
518 
519 #endif
520 
channels(QObject * parent)521 QObject *SnapResource::channels(QObject *parent)
522 {
523 #ifdef SNAP_CHANNELS
524     return new Channels(this, parent);
525 #else
526     return nullptr;
527 #endif
528 }
529 
530 #include "SnapResource.moc"
531