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