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