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

source code of kio/src/gui/previewjob.cpp