1 // -*- c++ -*-
2 /*
3     This file is part of the KDE libraries
4     SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
5     SPDX-FileCopyrightText: 2000 Carsten Pfeiffer <pfeiffer@kde.org>
6     SPDX-FileCopyrightText: 2001 Malte Starostik <malte.starostik@t-online.de>
7 
8     SPDX-License-Identifier: LGPL-2.0-or-later
9 */
10 
11 #include "previewjob.h"
12 #include "kio_widgets_debug.h"
13 
14 #if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID)
15 #define WITH_SHM 1
16 #else
17 #define WITH_SHM 0
18 #endif
19 
20 #if WITH_SHM
21 #include <sys/ipc.h>
22 #include <sys/shm.h>
23 #endif
24 
25 #include <limits>
26 #include <set>
27 
28 #include <QDir>
29 #include <QFile>
30 #include <QImage>
31 #include <QPixmap>
32 #include <QRegularExpression>
33 #include <QSaveFile>
34 #include <QTemporaryFile>
35 #include <QTimer>
36 
37 #include <QCryptographicHash>
38 
39 #include <KConfigGroup>
40 #include <KMountPoint>
41 #include <KPluginInfo>
42 #include <KService>
43 #include <KServiceTypeTrader>
44 #include <KSharedConfig>
45 #include <QMimeDatabase>
46 #include <QStandardPaths>
47 #include <Solid/Device>
48 #include <Solid/StorageAccess>
49 #include <kprotocolinfo.h>
50 
51 #include <algorithm>
52 #include <cmath>
53 
54 #include "job_p.h"
55 
56 namespace  {
57     static int s_defaultDevicePixelRatio = 1;
58 }
59 
60 namespace KIO
61 {
62 struct PreviewItem;
63 }
64 using namespace KIO;
65 
66 struct KIO::PreviewItem {
67     KFileItem item;
68     KPluginMetaData plugin;
69 };
70 
71 class KIO::PreviewJobPrivate : public KIO::JobPrivate
72 {
73 public:
PreviewJobPrivate(const KFileItemList & items,const QSize & size)74     PreviewJobPrivate(const KFileItemList &items, const QSize &size)
75         : initialItems(items)
76         , width(size.width())
77         , height(size.height())
78         , cacheSize(0)
79         , bScale(true)
80         , bSave(true)
81         , ignoreMaximumSize(false)
82         , sequenceIndex(0)
83         , succeeded(false)
84         , maximumLocalSize(0)
85         , maximumRemoteSize(0)
86         , iconSize(0)
87         , iconAlpha(70)
88         , shmid(-1)
89         , shmaddr(nullptr)
90     {
91         // http://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#DIRECTORY
92         thumbRoot = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/thumbnails/");
93     }
94 
95     enum {
96         STATE_STATORIG, // if the thumbnail exists
97         STATE_GETORIG, // if we create it
98         STATE_CREATETHUMB, // thumbnail:/ slave
99         STATE_DEVICE_INFO, // additional state check to get needed device ids
100     } state;
101 
102     KFileItemList initialItems;
103     QStringList enabledPlugins;
104     // Some plugins support remote URLs, <protocol, mimetypes>
105     QHash<QString, QStringList> m_remoteProtocolPlugins;
106     // Our todo list :)
107     // We remove the first item at every step, so use std::list
108     std::list<PreviewItem> items;
109     // The current item
110     PreviewItem currentItem;
111     // The modification time of that URL
112     QDateTime tOrig;
113     // Path to thumbnail cache for the current size
114     QString thumbPath;
115     // Original URL of current item in RFC2396 format
116     // (file:///path/to/a%20file instead of file:/path/to/a file)
117     QByteArray origName;
118     // Thumbnail file name for current item
119     QString thumbName;
120     // Size of thumbnail
121     int width;
122     int height;
123     // Unscaled size of thumbnail (128, 256 or 512 if cache is enabled)
124     short cacheSize;
125     // Whether the thumbnail should be scaled
126     bool bScale;
127     // Whether we should save the thumbnail
128     bool bSave;
129     bool ignoreMaximumSize;
130     int sequenceIndex;
131     bool succeeded;
132     // If the file to create a thumb for was a temp file, this is its name
133     QString tempName;
134     KIO::filesize_t maximumLocalSize;
135     KIO::filesize_t maximumRemoteSize;
136     // the size for the icon overlay
137     int iconSize;
138     // the transparency of the blended MIME type icon
139     int iconAlpha;
140     // Shared memory segment Id. The segment is allocated to a size
141     // of extent x extent x 4 (32 bit image) on first need.
142     int shmid;
143     // And the data area
144     uchar *shmaddr;
145     // Size of the shm segment
146     size_t shmsize;
147     // Root of thumbnail cache
148     QString thumbRoot;
149     // Metadata returned from the KIO thumbnail slave
150     QMap<QString, QString> thumbnailSlaveMetaData;
151     int devicePixelRatio = s_defaultDevicePixelRatio;
152     static const int idUnknown = -1;
153     // Id of a device storing currently processed file
154     int currentDeviceId = 0;
155     // Device ID for each file. Stored while in STATE_DEVICE_INFO state, used later on.
156     QMap<QString, int> deviceIdMap;
157     enum CachePolicy { Prevent, Allow, Unknown } currentDeviceCachePolicy = Unknown;
158 
159     void getOrCreateThumbnail();
160     bool statResultThumbnail();
161     void createThumbnail(const QString &);
162     void cleanupTempFile();
163     void determineNextFile();
164     void emitPreview(const QImage &thumb);
165 
166     void startPreview();
167     void slotThumbData(KIO::Job *, const QByteArray &);
168     // Checks if thumbnail is on encrypted partition different than thumbRoot
169     CachePolicy canBeCached(const QString &path);
170     int getDeviceId(const QString &path);
171 
Q_DECLARE_PUBLIC(PreviewJob)172     Q_DECLARE_PUBLIC(PreviewJob)
173 
174     static QVector<KPluginMetaData> loadAvailablePlugins()
175     {
176         static QVector<KPluginMetaData> jsonMetaDataPlugins;
177         if (!jsonMetaDataPlugins.isEmpty()) {
178             return jsonMetaDataPlugins;
179         }
180         jsonMetaDataPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf5/thumbcreator"));
181         std::set<QString> pluginIds;
182         for (const KPluginMetaData &data : std::as_const(jsonMetaDataPlugins)) {
183             pluginIds.insert(data.pluginId());
184         }
185         QT_WARNING_PUSH
186         QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
187         QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
188         const KService::List plugins = KServiceTypeTrader::self()->query(QStringLiteral("ThumbCreator"));
189         for (const auto &plugin : plugins) {
190             if (KPluginInfo info(plugin); info.isValid()) {
191                 if (auto [it, inserted] = pluginIds.insert(info.pluginName()); inserted) {
192                     jsonMetaDataPlugins << info.toMetaData();
193                 }
194             } else {
195                 // Hack for directory thumbnailer: It has a hardcoded plugin id in the kio-slave and not any C++ plugin
196                 // Consequently we just use the base name as the plugin file for our KPluginMetaData object
197                 const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kservices5/") + plugin->entryPath());
198                 KPluginMetaData tmpData = KPluginMetaData::fromDesktopFile(path);
199                 jsonMetaDataPlugins << KPluginMetaData(tmpData.rawData(), QFileInfo(path).baseName(), path);
200             }
201         }
202         QT_WARNING_POP
203         return jsonMetaDataPlugins;
204     }
205 };
206 
207 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 86)
setDefaultDevicePixelRatio(int defaultDevicePixelRatio)208 void PreviewJob::setDefaultDevicePixelRatio(int defaultDevicePixelRatio)
209 {
210     s_defaultDevicePixelRatio = defaultDevicePixelRatio;
211 }
212 #endif
213 
setDefaultDevicePixelRatio(qreal defaultDevicePixelRatio)214 void PreviewJob::setDefaultDevicePixelRatio(qreal defaultDevicePixelRatio)
215 {
216     s_defaultDevicePixelRatio = std::ceil(defaultDevicePixelRatio);
217 }
218 
219 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 7)
PreviewJob(const KFileItemList & items,int width,int height,int iconSize,int iconAlpha,bool scale,bool save,const QStringList * enabledPlugins)220 PreviewJob::PreviewJob(const KFileItemList &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins)
221     : KIO::Job(*new PreviewJobPrivate(items, QSize(width, height ? height : width)))
222 {
223     Q_D(PreviewJob);
224     d->enabledPlugins = enabledPlugins ? *enabledPlugins : availablePlugins();
225     d->iconSize = iconSize;
226     d->iconAlpha = iconAlpha;
227     d->bScale = scale;
228     d->bSave = save && scale;
229 
230     // Return to event loop first, determineNextFile() might delete this;
231     QTimer::singleShot(0, this, SLOT(startPreview()));
232 }
233 #endif
234 
PreviewJob(const KFileItemList & items,const QSize & size,const QStringList * enabledPlugins)235 PreviewJob::PreviewJob(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins)
236     : KIO::Job(*new PreviewJobPrivate(items, size))
237 {
238     Q_D(PreviewJob);
239 
240     if (enabledPlugins) {
241         d->enabledPlugins = *enabledPlugins;
242     } else {
243         const KConfigGroup globalConfig(KSharedConfig::openConfig(), "PreviewSettings");
244         d->enabledPlugins =
245             globalConfig.readEntry("Plugins",
246                                    QStringList{QStringLiteral("directorythumbnail"), QStringLiteral("imagethumbnail"), QStringLiteral("jpegthumbnail")});
247     }
248 
249     // Return to event loop first, determineNextFile() might delete this;
250     QTimer::singleShot(0, this, SLOT(startPreview()));
251 }
252 
~PreviewJob()253 PreviewJob::~PreviewJob()
254 {
255 #if WITH_SHM
256     Q_D(PreviewJob);
257     if (d->shmaddr) {
258         shmdt((char *)d->shmaddr);
259         shmctl(d->shmid, IPC_RMID, nullptr);
260     }
261 #endif
262 }
263 
setOverlayIconSize(int size)264 void PreviewJob::setOverlayIconSize(int size)
265 {
266     Q_D(PreviewJob);
267     d->iconSize = size;
268 }
269 
overlayIconSize() const270 int PreviewJob::overlayIconSize() const
271 {
272     Q_D(const PreviewJob);
273     return d->iconSize;
274 }
275 
setOverlayIconAlpha(int alpha)276 void PreviewJob::setOverlayIconAlpha(int alpha)
277 {
278     Q_D(PreviewJob);
279     d->iconAlpha = qBound(0, alpha, 255);
280 }
281 
overlayIconAlpha() const282 int PreviewJob::overlayIconAlpha() const
283 {
284     Q_D(const PreviewJob);
285     return d->iconAlpha;
286 }
287 
setScaleType(ScaleType type)288 void PreviewJob::setScaleType(ScaleType type)
289 {
290     Q_D(PreviewJob);
291     switch (type) {
292     case Unscaled:
293         d->bScale = false;
294         d->bSave = false;
295         break;
296     case Scaled:
297         d->bScale = true;
298         d->bSave = false;
299         break;
300     case ScaledAndCached:
301         d->bScale = true;
302         d->bSave = true;
303         break;
304     default:
305         break;
306     }
307 }
308 
scaleType() const309 PreviewJob::ScaleType PreviewJob::scaleType() const
310 {
311     Q_D(const PreviewJob);
312     if (d->bScale) {
313         return d->bSave ? ScaledAndCached : Scaled;
314     }
315     return Unscaled;
316 }
317 
startPreview()318 void PreviewJobPrivate::startPreview()
319 {
320     Q_Q(PreviewJob);
321     // Load the list of plugins to determine which MIME types are supported
322     const QVector<KPluginMetaData> plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
323     QMap<QString, KPluginMetaData> mimeMap;
324     QHash<QString, QHash<QString, KPluginMetaData>> protocolMap;
325 
326     for (const KPluginMetaData &plugin : plugins) {
327         QStringList protocols = plugin.value(QStringLiteral("X-KDE-Protocols"), QStringList());
328         const QString p = plugin.value(QStringLiteral("X-KDE-Protocol"));
329         if (!p.isEmpty()) {
330             protocols.append(p);
331         }
332         for (const QString &protocol : std::as_const(protocols)) {
333             // Add supported MIME type for this protocol
334             QStringList &_ms = m_remoteProtocolPlugins[protocol];
335             const auto mimeTypes = plugin.mimeTypes();
336             for (const QString &_m : mimeTypes) {
337                 protocolMap[protocol].insert(_m, plugin);
338                 if (!_ms.contains(_m)) {
339                     _ms.append(_m);
340                 }
341             }
342         }
343         if (enabledPlugins.contains(plugin.pluginId())) {
344             const auto mimeTypes = plugin.mimeTypes();
345             for (const QString &mimeType : mimeTypes) {
346                 mimeMap.insert(mimeType, plugin);
347             }
348         }
349     }
350 
351     // Look for images and store the items in our todo list :)
352     bool bNeedCache = false;
353     for (const auto &fileItem : std::as_const(initialItems)) {
354         PreviewItem item;
355         item.item = fileItem;
356 
357         const QString mimeType = item.item.mimetype();
358         KPluginMetaData plugin;
359 
360         // look for protocol-specific thumbnail plugins first
361         auto it = protocolMap.constFind(item.item.url().scheme());
362         if (it != protocolMap.constEnd()) {
363             plugin = it.value().value(mimeType);
364         }
365 
366         if (!plugin.isValid()) {
367             auto pluginIt = mimeMap.constFind(mimeType);
368             if (pluginIt == mimeMap.constEnd()) {
369                 QString groupMimeType = mimeType;
370                 groupMimeType.replace(QRegularExpression(QStringLiteral("/.*")), QStringLiteral("/*"));
371                 pluginIt = mimeMap.constFind(groupMimeType);
372 
373                 if (pluginIt == mimeMap.constEnd()) {
374                     QMimeDatabase db;
375                     // check MIME type inheritance, resolve aliases
376                     const QMimeType mimeInfo = db.mimeTypeForName(mimeType);
377                     if (mimeInfo.isValid()) {
378                         const QStringList parentMimeTypes = mimeInfo.allAncestors();
379                         for (const QString &parentMimeType : parentMimeTypes) {
380                             pluginIt = mimeMap.constFind(parentMimeType);
381                             if (pluginIt != mimeMap.constEnd()) {
382                                 break;
383                             }
384                         }
385                     }
386                 }
387             }
388 
389             if (pluginIt != mimeMap.constEnd()) {
390                 plugin = *pluginIt;
391             }
392         }
393 
394         if (plugin.isValid()) {
395             item.plugin = plugin;
396             items.push_back(item);
397             if (!bNeedCache && bSave && plugin.value(QStringLiteral("CacheThumbnail"), true)) {
398                 const QUrl url = fileItem.url();
399                 if (!url.isLocalFile() || !url.adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot)) {
400                     bNeedCache = true;
401                 }
402             }
403         } else {
404             Q_EMIT q->failed(fileItem);
405         }
406     }
407 
408     KConfigGroup cg(KSharedConfig::openConfig(), "PreviewSettings");
409     maximumLocalSize = cg.readEntry("MaximumSize", std::numeric_limits<KIO::filesize_t>::max());
410     maximumRemoteSize = cg.readEntry("MaximumRemoteSize", 0);
411 
412     if (bNeedCache) {
413 
414         if (width <= 128 && height <= 128) {
415             cacheSize = 128;
416         } else if (width <= 256 && height <= 256) {
417             cacheSize = 256;
418         } else {
419             cacheSize = 512;
420         }
421 
422         struct CachePool {
423             QString path;
424             int minSize;
425         };
426 
427         const static auto pools = {
428             CachePool{QStringLiteral("/normal/"), 128},
429             CachePool{QStringLiteral("/large/"), 256},
430             CachePool{QStringLiteral("/x-large/"), 512},
431             CachePool{QStringLiteral("/xx-large/"), 1024},
432         };
433 
434         QString thumbDir;
435         int wants = devicePixelRatio * cacheSize;
436         for (const auto &p : pools) {
437             if (p.minSize < wants) {
438                 continue;
439             } else {
440                 thumbDir = p.path;
441                 break;
442             }
443         }
444         thumbPath = thumbRoot + thumbDir;
445 
446         if (!QDir(thumbPath).exists()) {
447             if (QDir().mkpath(thumbPath)) { // Qt5 TODO: mkpath(dirPath, permissions)
448                 QFile f(thumbPath);
449                 f.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser); // 0700
450             }
451         }
452     } else {
453         bSave = false;
454     }
455 
456     initialItems.clear();
457     determineNextFile();
458 }
459 
removeItem(const QUrl & url)460 void PreviewJob::removeItem(const QUrl &url)
461 {
462     Q_D(PreviewJob);
463 
464     auto it = std::find_if(d->items.cbegin(), d->items.cend(), [&url](const PreviewItem &pItem) {
465         return url == pItem.item.url();
466     });
467     if (it != d->items.cend()) {
468         d->items.erase(it);
469     }
470 
471     if (d->currentItem.item.url() == url) {
472         KJob *job = subjobs().first();
473         job->kill();
474         removeSubjob(job);
475         d->determineNextFile();
476     }
477 }
478 
setSequenceIndex(int index)479 void KIO::PreviewJob::setSequenceIndex(int index)
480 {
481     d_func()->sequenceIndex = index;
482 }
483 
sequenceIndex() const484 int KIO::PreviewJob::sequenceIndex() const
485 {
486     return d_func()->sequenceIndex;
487 }
488 
sequenceIndexWraparoundPoint() const489 float KIO::PreviewJob::sequenceIndexWraparoundPoint() const
490 {
491     return d_func()->thumbnailSlaveMetaData.value(QStringLiteral("sequenceIndexWraparoundPoint"), QStringLiteral("-1.0")).toFloat();
492 }
493 
handlesSequences() const494 bool KIO::PreviewJob::handlesSequences() const
495 {
496     return d_func()->thumbnailSlaveMetaData.value(QStringLiteral("handlesSequences")) == QStringLiteral("1");
497 }
498 
499 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 86)
setDevicePixelRatio(int dpr)500 void KIO::PreviewJob::setDevicePixelRatio(int dpr)
501 {
502     d_func()->devicePixelRatio = dpr;
503 }
504 #endif
505 
setDevicePixelRatio(qreal dpr)506 void KIO::PreviewJob::setDevicePixelRatio(qreal dpr)
507 {
508     d_func()->devicePixelRatio = std::ceil(dpr);
509 }
510 
setIgnoreMaximumSize(bool ignoreSize)511 void PreviewJob::setIgnoreMaximumSize(bool ignoreSize)
512 {
513     d_func()->ignoreMaximumSize = ignoreSize;
514 }
515 
cleanupTempFile()516 void PreviewJobPrivate::cleanupTempFile()
517 {
518     if (!tempName.isEmpty()) {
519         Q_ASSERT((!QFileInfo(tempName).isDir() && QFileInfo(tempName).isFile()) || QFileInfo(tempName).isSymLink());
520         QFile::remove(tempName);
521         tempName.clear();
522     }
523 }
524 
determineNextFile()525 void PreviewJobPrivate::determineNextFile()
526 {
527     Q_Q(PreviewJob);
528     if (!currentItem.item.isNull()) {
529         if (!succeeded) {
530             Q_EMIT q->failed(currentItem.item);
531         }
532     }
533     // No more items ?
534     if (items.empty()) {
535         q->emitResult();
536         return;
537     } else {
538         // First, stat the orig file
539         state = PreviewJobPrivate::STATE_STATORIG;
540         currentItem = items.front();
541         items.pop_front();
542         succeeded = false;
543         KIO::Job *job = KIO::statDetails(currentItem.item.url(), StatJob::SourceSide, KIO::StatDefaultDetails | KIO::StatInode, KIO::HideProgressInfo);
544         job->addMetaData(QStringLiteral("thumbnail"), QStringLiteral("1"));
545         job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
546         q->addSubjob(job);
547     }
548 }
549 
slotResult(KJob * job)550 void PreviewJob::slotResult(KJob *job)
551 {
552     Q_D(PreviewJob);
553 
554     removeSubjob(job);
555     Q_ASSERT(!hasSubjobs()); // We should have only one job at a time ...
556     switch (d->state) {
557     case PreviewJobPrivate::STATE_STATORIG: {
558         if (job->error()) { // that's no good news...
559             // Drop this one and move on to the next one
560             d->determineNextFile();
561             return;
562         }
563         const KIO::UDSEntry statResult = static_cast<KIO::StatJob *>(job)->statResult();
564         d->currentDeviceId = statResult.numberValue(KIO::UDSEntry::UDS_DEVICE_ID, 0);
565         d->tOrig = QDateTime::fromSecsSinceEpoch(statResult.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, 0));
566 
567         bool skipCurrentItem = false;
568         const KIO::filesize_t size = (KIO::filesize_t)statResult.numberValue(KIO::UDSEntry::UDS_SIZE, 0);
569         const QUrl itemUrl = d->currentItem.item.mostLocalUrl();
570 
571         if (itemUrl.isLocalFile() || KProtocolInfo::protocolClass(itemUrl.scheme()) == QLatin1String(":local")) {
572             skipCurrentItem = !d->ignoreMaximumSize && size > d->maximumLocalSize && !d->currentItem.plugin.value(QStringLiteral("IgnoreMaximumSize"), false);
573         } else {
574             // For remote items the "IgnoreMaximumSize" plugin property is not respected
575             skipCurrentItem = !d->ignoreMaximumSize && size > d->maximumRemoteSize;
576 
577             // Remote directories are not supported, don't try to do a file_copy on them
578             if (!skipCurrentItem) {
579                 // TODO update item.mimeType from the UDS entry, in case it wasn't set initially
580                 // But we don't use the MIME type anymore, we just use isDir().
581                 if (d->currentItem.item.isDir()) {
582                     skipCurrentItem = true;
583                 }
584             }
585         }
586         if (skipCurrentItem) {
587             d->determineNextFile();
588             return;
589         }
590 
591         bool pluginHandlesSequences = d->currentItem.plugin.value(QStringLiteral("HandleSequences"), false);
592         if (!d->currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true) || (d->sequenceIndex && pluginHandlesSequences)) {
593             // This preview will not be cached, no need to look for a saved thumbnail
594             // Just create it, and be done
595             d->getOrCreateThumbnail();
596             return;
597         }
598 
599         if (d->statResultThumbnail()) {
600             return;
601         }
602 
603         d->getOrCreateThumbnail();
604         return;
605     }
606     case PreviewJobPrivate::STATE_DEVICE_INFO: {
607         KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job);
608         int id;
609         QString path = statJob->url().toLocalFile();
610         if (job->error()) {
611             // We set id to 0 to know we tried getting it
612             qCWarning(KIO_WIDGETS) << "Cannot read information about filesystem under path" << path;
613             id = 0;
614         } else {
615             id = statJob->statResult().numberValue(KIO::UDSEntry::UDS_DEVICE_ID, 0);
616         }
617         d->deviceIdMap[path] = id;
618         d->createThumbnail(d->currentItem.item.localPath());
619         return;
620     }
621     case PreviewJobPrivate::STATE_GETORIG: {
622         if (job->error()) {
623             d->cleanupTempFile();
624             d->determineNextFile();
625             return;
626         }
627 
628         d->createThumbnail(static_cast<KIO::FileCopyJob *>(job)->destUrl().toLocalFile());
629         return;
630     }
631     case PreviewJobPrivate::STATE_CREATETHUMB: {
632         d->cleanupTempFile();
633         d->determineNextFile();
634         return;
635     }
636     }
637 }
638 
statResultThumbnail()639 bool PreviewJobPrivate::statResultThumbnail()
640 {
641     if (thumbPath.isEmpty()) {
642         return false;
643     }
644 
645     bool isLocal;
646     const QUrl url = currentItem.item.mostLocalUrl(&isLocal);
647     if (isLocal) {
648         const QFileInfo localFile(url.toLocalFile());
649         const QString canonicalPath = localFile.canonicalFilePath();
650         origName = QUrl::fromLocalFile(canonicalPath).toEncoded(QUrl::RemovePassword | QUrl::FullyEncoded);
651         if (origName.isEmpty()) {
652             qCWarning(KIO_WIDGETS) << "Failed to convert" << url << "to canonical path";
653             return false;
654         }
655     } else {
656         // Don't include the password if any
657         origName = url.toEncoded(QUrl::RemovePassword);
658     }
659 
660     QCryptographicHash md5(QCryptographicHash::Md5);
661     md5.addData(origName);
662     thumbName = QString::fromLatin1(md5.result().toHex()) + QLatin1String(".png");
663 
664     QImage thumb;
665     QFile thumbFile(thumbPath + thumbName);
666     if (!thumbFile.open(QIODevice::ReadOnly) || !thumb.load(&thumbFile, "png")) {
667         return false;
668     }
669 
670     if (thumb.text(QStringLiteral("Thumb::URI")) != QString::fromUtf8(origName)
671         || thumb.text(QStringLiteral("Thumb::MTime")).toLongLong() != tOrig.toSecsSinceEpoch()) {
672         return false;
673     }
674 
675     const QString origSize = thumb.text(QStringLiteral("Thumb::Size"));
676     if (!origSize.isEmpty() && origSize.toULongLong() != currentItem.item.size()) {
677         // Thumb::Size is not required, but if it is set it should match
678         return false;
679     }
680 
681     // The DPR of the loaded thumbnail is unspecified (and typically irrelevant).
682     // When a thumbnail is DPR-invariant, use the DPR passed in the request.
683     thumb.setDevicePixelRatio(devicePixelRatio);
684 
685     QString thumbnailerVersion = currentItem.plugin.value(QStringLiteral("ThumbnailerVersion"));
686 
687     if (!thumbnailerVersion.isEmpty() && thumb.text(QStringLiteral("Software")).startsWith(QLatin1String("KDE Thumbnail Generator"))) {
688         // Check if the version matches
689         // The software string should read "KDE Thumbnail Generator pluginName (vX)"
690         QString softwareString = thumb.text(QStringLiteral("Software")).remove(QStringLiteral("KDE Thumbnail Generator")).trimmed();
691         if (softwareString.isEmpty()) {
692             // The thumbnail has been created with an older version, recreating
693             return false;
694         }
695         int versionIndex = softwareString.lastIndexOf(QLatin1String("(v"));
696         if (versionIndex < 0) {
697             return false;
698         }
699 
700         QString cachedVersion = softwareString.remove(0, versionIndex + 2);
701         cachedVersion.chop(1);
702         uint thumbnailerMajor = thumbnailerVersion.toInt();
703         uint cachedMajor = cachedVersion.toInt();
704         if (thumbnailerMajor > cachedMajor) {
705             return false;
706         }
707     }
708 
709     // Found it, use it
710     emitPreview(thumb);
711     succeeded = true;
712     determineNextFile();
713     return true;
714 }
715 
getOrCreateThumbnail()716 void PreviewJobPrivate::getOrCreateThumbnail()
717 {
718     Q_Q(PreviewJob);
719     // We still need to load the orig file ! (This is getting tedious) :)
720     const KFileItem &item = currentItem.item;
721     const QString localPath = item.localPath();
722     if (!localPath.isEmpty()) {
723         createThumbnail(localPath);
724     } else {
725         const QUrl fileUrl = item.url();
726         // heuristics for remote URL support
727         bool supportsProtocol = false;
728         if (m_remoteProtocolPlugins.value(fileUrl.scheme()).contains(item.mimetype())) {
729             // There's a plugin supporting this protocol and MIME type
730             supportsProtocol = true;
731         } else if (m_remoteProtocolPlugins.value(QStringLiteral("KIO")).contains(item.mimetype())) {
732             // Assume KIO understands any URL, ThumbCreator slaves who have
733             // X-KDE-Protocols=KIO will get fed the remote URL directly.
734             supportsProtocol = true;
735         }
736 
737         if (supportsProtocol) {
738             createThumbnail(fileUrl.toString());
739             return;
740         }
741         if (item.isDir()) {
742             // Skip remote dirs (bug 208625)
743             cleanupTempFile();
744             determineNextFile();
745             return;
746         }
747         // No plugin support access to this remote content, copy the file
748         // to the local machine, then create the thumbnail
749         state = PreviewJobPrivate::STATE_GETORIG;
750         QTemporaryFile localFile;
751         localFile.setAutoRemove(false);
752         localFile.open();
753         tempName = localFile.fileName();
754         const QUrl currentURL = item.mostLocalUrl();
755         KIO::Job *job = KIO::file_copy(currentURL, QUrl::fromLocalFile(tempName), -1, KIO::Overwrite | KIO::HideProgressInfo /* No GUI */);
756         job->addMetaData(QStringLiteral("thumbnail"), QStringLiteral("1"));
757         q->addSubjob(job);
758     }
759 }
760 
canBeCached(const QString & path)761 PreviewJobPrivate::CachePolicy PreviewJobPrivate::canBeCached(const QString &path)
762 {
763     // If checked file is directory on a different filesystem than its parent, we need to check it separately
764     int separatorIndex = path.lastIndexOf(QLatin1Char('/'));
765     QString parentDirPath = path.left(separatorIndex);
766 
767     int parentId = getDeviceId(parentDirPath);
768     if (parentId == idUnknown) {
769         return CachePolicy::Unknown;
770     }
771 
772     bool isDifferentSystem = !parentId || parentId != currentDeviceId;
773     if (!isDifferentSystem && currentDeviceCachePolicy != CachePolicy::Unknown) {
774         return currentDeviceCachePolicy;
775     }
776     int checkedId;
777     QString checkedPath;
778     if (isDifferentSystem) {
779         checkedId = currentDeviceId;
780         checkedPath = path;
781     } else {
782         checkedId = getDeviceId(parentDirPath);
783         checkedPath = parentDirPath;
784         if (checkedId == idUnknown) {
785             return CachePolicy::Unknown;
786         }
787     }
788     // If we're checking different filesystem or haven't checked yet see if filesystem matches thumbRoot
789     int thumbRootId = getDeviceId(thumbRoot);
790     if (thumbRootId == idUnknown) {
791         return CachePolicy::Unknown;
792     }
793     bool shouldAllow = checkedId && checkedId == thumbRootId;
794     if (!shouldAllow) {
795         Solid::Device device = Solid::Device::storageAccessFromPath(checkedPath);
796         if (device.isValid()) {
797             shouldAllow = !device.as<Solid::StorageAccess>()->isEncrypted();
798         }
799     }
800     if (!isDifferentSystem) {
801         currentDeviceCachePolicy = shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent;
802     }
803     return shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent;
804 }
805 
getDeviceId(const QString & path)806 int PreviewJobPrivate::getDeviceId(const QString &path)
807 {
808     Q_Q(PreviewJob);
809     auto iter = deviceIdMap.find(path);
810     if (iter != deviceIdMap.end()) {
811         return iter.value();
812     }
813     QUrl url = QUrl::fromLocalFile(path);
814     if (!url.isValid()) {
815         qCWarning(KIO_WIDGETS) << "Invalid url" << path;
816         return 0;
817     }
818     state = PreviewJobPrivate::STATE_DEVICE_INFO;
819     KIO::Job *job = KIO::statDetails(url, StatJob::SourceSide, KIO::StatDefaultDetails | KIO::StatInode, KIO::HideProgressInfo);
820     job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
821     q->addSubjob(job);
822 
823     return idUnknown;
824 }
825 
createThumbnail(const QString & pixPath)826 void PreviewJobPrivate::createThumbnail(const QString &pixPath)
827 {
828     Q_Q(PreviewJob);
829     state = PreviewJobPrivate::STATE_CREATETHUMB;
830     QUrl thumbURL;
831     thumbURL.setScheme(QStringLiteral("thumbnail"));
832     thumbURL.setPath(pixPath);
833 
834     bool save = bSave && currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true) && !sequenceIndex;
835 
836     bool isRemoteProtocol = currentItem.item.localPath().isEmpty();
837     CachePolicy cachePolicy = isRemoteProtocol ? CachePolicy::Prevent : canBeCached(pixPath);
838 
839     if (cachePolicy == CachePolicy::Unknown) {
840         // If Unknown is returned, creating thumbnail should be called again by slotResult
841         return;
842     }
843 
844     KIO::TransferJob *job = KIO::get(thumbURL, NoReload, HideProgressInfo);
845     q->addSubjob(job);
846     q->connect(job, &KIO::TransferJob::data, q, [this](KIO::Job *job, const QByteArray &data) {
847         slotThumbData(job, data);
848     });
849 
850     int thumb_width = width;
851     int thumb_height = height;
852     int thumb_iconSize = iconSize;
853     if (save) {
854         thumb_width = thumb_height = cacheSize;
855         thumb_iconSize = 64;
856     }
857 
858     job->addMetaData(QStringLiteral("mimeType"), currentItem.item.mimetype());
859     job->addMetaData(QStringLiteral("width"), QString::number(thumb_width));
860     job->addMetaData(QStringLiteral("height"), QString::number(thumb_height));
861     job->addMetaData(QStringLiteral("iconSize"), QString::number(thumb_iconSize));
862     job->addMetaData(QStringLiteral("iconAlpha"), QString::number(iconAlpha));
863     job->addMetaData(QStringLiteral("plugin"), currentItem.plugin.fileName());
864     job->addMetaData(QStringLiteral("enabledPlugins"), enabledPlugins.join(QLatin1Char(',')));
865     job->addMetaData(QStringLiteral("devicePixelRatio"), QString::number(devicePixelRatio));
866     job->addMetaData(QStringLiteral("cache"), QString::number(cachePolicy == CachePolicy::Allow));
867     if (sequenceIndex) {
868         job->addMetaData(QStringLiteral("sequence-index"), QString::number(sequenceIndex));
869     }
870 
871 #if WITH_SHM
872     size_t requiredSize = thumb_width * devicePixelRatio * thumb_height * devicePixelRatio * 4;
873     if (shmid == -1 || shmsize < requiredSize) {
874         if (shmaddr) {
875             // clean previous shared memory segment
876             shmdt((char *)shmaddr);
877             shmaddr = nullptr;
878             shmctl(shmid, IPC_RMID, nullptr);
879             shmid = -1;
880         }
881         if (requiredSize > 0) {
882             shmid = shmget(IPC_PRIVATE, requiredSize, IPC_CREAT | 0600);
883             if (shmid != -1) {
884                 shmsize = requiredSize;
885                 shmaddr = (uchar *)(shmat(shmid, nullptr, SHM_RDONLY));
886                 if (shmaddr == (uchar *)-1) {
887                     shmctl(shmid, IPC_RMID, nullptr);
888                     shmaddr = nullptr;
889                     shmid = -1;
890                 }
891             }
892         }
893     }
894     if (shmid != -1) {
895         job->addMetaData(QStringLiteral("shmid"), QString::number(shmid));
896     }
897 #endif
898 }
899 
slotThumbData(KIO::Job * job,const QByteArray & data)900 void PreviewJobPrivate::slotThumbData(KIO::Job *job, const QByteArray &data)
901 {
902     thumbnailSlaveMetaData = job->metaData();
903     /* clang-format off */
904     const bool save = bSave
905                       && !sequenceIndex
906                       && currentDeviceCachePolicy == CachePolicy::Allow
907                       && currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true)
908                       && (!currentItem.item.url().isLocalFile()
909                           || !currentItem.item.url().adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot));
910     /* clang-format on */
911 
912     QImage thumb;
913 #if WITH_SHM
914     if (shmaddr) {
915         // Keep this in sync with kdebase/kioslave/thumbnail.cpp
916         QDataStream str(data);
917         int width;
918         int height;
919         quint8 iFormat;
920         int imgDevicePixelRatio = 1;
921         // TODO KF6: add a version number as first parameter
922         str >> width >> height >> iFormat;
923         if (iFormat & 0x80) {
924             // HACK to deduce if imgDevicePixelRatio is present
925             iFormat &= 0x7f;
926             str >> imgDevicePixelRatio;
927         }
928         QImage::Format format = static_cast<QImage::Format>(iFormat);
929         thumb = QImage(shmaddr, width, height, format).copy();
930         thumb.setDevicePixelRatio(imgDevicePixelRatio);
931     } else {
932         thumb.loadFromData(data);
933     }
934 #else
935     thumb.loadFromData(data);
936 #endif
937 
938     if (thumb.isNull()) {
939         QDataStream s(data);
940         s >> thumb;
941     }
942 
943     if (save) {
944         thumb.setText(QStringLiteral("Thumb::URI"), QString::fromUtf8(origName));
945         thumb.setText(QStringLiteral("Thumb::MTime"), QString::number(tOrig.toSecsSinceEpoch()));
946         thumb.setText(QStringLiteral("Thumb::Size"), number(currentItem.item.size()));
947         thumb.setText(QStringLiteral("Thumb::Mimetype"), currentItem.item.mimetype());
948         QString thumbnailerVersion = currentItem.plugin.value(QStringLiteral("ThumbnailerVersion"));
949         QString signature = QLatin1String("KDE Thumbnail Generator ") + currentItem.plugin.name();
950         if (!thumbnailerVersion.isEmpty()) {
951             signature.append(QLatin1String(" (v") + thumbnailerVersion + QLatin1Char(')'));
952         }
953         thumb.setText(QStringLiteral("Software"), signature);
954         QSaveFile saveFile(thumbPath + thumbName);
955         if (saveFile.open(QIODevice::WriteOnly)) {
956             if (thumb.save(&saveFile, "PNG")) {
957                 saveFile.commit();
958             }
959         }
960     }
961     emitPreview(thumb);
962     succeeded = true;
963 }
964 
emitPreview(const QImage & thumb)965 void PreviewJobPrivate::emitPreview(const QImage &thumb)
966 {
967     Q_Q(PreviewJob);
968     QPixmap pix;
969     if (thumb.width() > width * thumb.devicePixelRatio() || thumb.height() > height * thumb.devicePixelRatio()) {
970         pix = QPixmap::fromImage(thumb.scaled(QSize(width * thumb.devicePixelRatio(), height * thumb.devicePixelRatio()), Qt::KeepAspectRatio, Qt::SmoothTransformation));
971     } else {
972         pix = QPixmap::fromImage(thumb);
973     }
974     pix.setDevicePixelRatio(thumb.devicePixelRatio());
975     Q_EMIT q->gotPreview(currentItem.item, pix);
976 }
977 
availablePlugins()978 QStringList PreviewJob::availablePlugins()
979 {
980     QStringList result;
981     const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
982     for (const KPluginMetaData &plugin : plugins) {
983         result << plugin.pluginId();
984     }
985     return result;
986 }
987 
defaultPlugins()988 QStringList PreviewJob::defaultPlugins()
989 {
990     const QStringList blacklist = QStringList() << QStringLiteral("textthumbnail");
991 
992     QStringList defaultPlugins = availablePlugins();
993     for (const QString &plugin : blacklist) {
994         defaultPlugins.removeAll(plugin);
995     }
996 
997     return defaultPlugins;
998 }
999 
supportedMimeTypes()1000 QStringList PreviewJob::supportedMimeTypes()
1001 {
1002     QStringList result;
1003     const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
1004     for (const KPluginMetaData &plugin : plugins) {
1005         result += plugin.mimeTypes();
1006     }
1007     return result;
1008 }
1009 
1010 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 7)
1011 PreviewJob *
filePreview(const KFileItemList & items,int width,int height,int iconSize,int iconAlpha,bool scale,bool save,const QStringList * enabledPlugins)1012 KIO::filePreview(const KFileItemList &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins)
1013 {
1014     return new PreviewJob(items, width, height, iconSize, iconAlpha, scale, save, enabledPlugins);
1015 }
1016 #endif
1017 
1018 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 7)
1019 PreviewJob *
filePreview(const QList<QUrl> & items,int width,int height,int iconSize,int iconAlpha,bool scale,bool save,const QStringList * enabledPlugins)1020 KIO::filePreview(const QList<QUrl> &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins)
1021 {
1022     KFileItemList fileItems;
1023     fileItems.reserve(items.size());
1024     for (const QUrl &url : items) {
1025         Q_ASSERT(url.isValid()); // please call us with valid urls only
1026         fileItems.append(KFileItem(url));
1027     }
1028     return new PreviewJob(fileItems, width, height, iconSize, iconAlpha, scale, save, enabledPlugins);
1029 }
1030 #endif
1031 
filePreview(const KFileItemList & items,const QSize & size,const QStringList * enabledPlugins)1032 PreviewJob *KIO::filePreview(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins)
1033 {
1034     return new PreviewJob(items, size, enabledPlugins);
1035 }
1036 
1037 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 5)
maximumFileSize()1038 KIO::filesize_t PreviewJob::maximumFileSize()
1039 {
1040     KConfigGroup cg(KSharedConfig::openConfig(), "PreviewSettings");
1041     return cg.readEntry("MaximumSize", 5 * 1024 * 1024LL /* 5MB */);
1042 }
1043 #endif
1044 
1045 #include "moc_previewjob.cpp"
1046