File indexing completed on 2024-12-01 09:53:46

0001 // -*- c++ -*-
0002 /*
0003     This file is part of the KDE libraries
0004     SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
0005     SPDX-FileCopyrightText: 2000 Carsten Pfeiffer <pfeiffer@kde.org>
0006     SPDX-FileCopyrightText: 2001 Malte Starostik <malte.starostik@t-online.de>
0007 
0008     SPDX-License-Identifier: LGPL-2.0-or-later
0009 */
0010 
0011 #include "previewjob.h"
0012 #include "kio_widgets_debug.h"
0013 
0014 #if defined(Q_OS_UNIX) && !defined(Q_OS_ANDROID)
0015 #define WITH_SHM 1
0016 #else
0017 #define WITH_SHM 0
0018 #endif
0019 
0020 #if WITH_SHM
0021 #include <sys/ipc.h>
0022 #include <sys/shm.h>
0023 #endif
0024 
0025 #include <limits>
0026 #include <set>
0027 
0028 #include <QDir>
0029 #include <QFile>
0030 #include <QImage>
0031 #include <QPixmap>
0032 #include <QRegularExpression>
0033 #include <QSaveFile>
0034 #include <QTemporaryFile>
0035 #include <QTimer>
0036 
0037 #include <QCryptographicHash>
0038 
0039 #include <KConfigGroup>
0040 #include <KMountPoint>
0041 #include <KPluginInfo>
0042 #include <KPluginMetaData>
0043 #include <KService>
0044 #include <KServiceTypeTrader>
0045 #include <KSharedConfig>
0046 #include <QMimeDatabase>
0047 #include <QStandardPaths>
0048 #include <Solid/Device>
0049 #include <Solid/StorageAccess>
0050 #include <kprotocolinfo.h>
0051 
0052 #include <algorithm>
0053 #include <cmath>
0054 
0055 #include "job_p.h"
0056 
0057 namespace
0058 {
0059 static int s_defaultDevicePixelRatio = 1;
0060 }
0061 
0062 namespace KIO
0063 {
0064 struct PreviewItem;
0065 }
0066 using namespace KIO;
0067 
0068 struct KIO::PreviewItem {
0069     KFileItem item;
0070     KPluginMetaData plugin;
0071 };
0072 
0073 class KIO::PreviewJobPrivate : public KIO::JobPrivate
0074 {
0075 public:
0076     PreviewJobPrivate(const KFileItemList &items, const QSize &size)
0077         : initialItems(items)
0078         , width(size.width())
0079         , height(size.height())
0080         , cacheSize(0)
0081         , bScale(true)
0082         , bSave(true)
0083         , ignoreMaximumSize(false)
0084         , sequenceIndex(0)
0085         , succeeded(false)
0086         , maximumLocalSize(0)
0087         , maximumRemoteSize(0)
0088 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102)
0089         , iconSize(0)
0090         , iconAlpha(70)
0091 #endif
0092         , shmid(-1)
0093         , shmaddr(nullptr)
0094     {
0095         // https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html#DIRECTORY
0096         thumbRoot = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/thumbnails/");
0097     }
0098 
0099     enum {
0100         STATE_STATORIG, // if the thumbnail exists
0101         STATE_GETORIG, // if we create it
0102         STATE_CREATETHUMB, // thumbnail:/ worker
0103         STATE_DEVICE_INFO, // additional state check to get needed device ids
0104     } state;
0105 
0106     KFileItemList initialItems;
0107     QStringList enabledPlugins;
0108     // Some plugins support remote URLs, <protocol, mimetypes>
0109     QHash<QString, QStringList> m_remoteProtocolPlugins;
0110     // Our todo list :)
0111     // We remove the first item at every step, so use std::list
0112     std::list<PreviewItem> items;
0113     // The current item
0114     PreviewItem currentItem;
0115     // The modification time of that URL
0116     QDateTime tOrig;
0117     // Path to thumbnail cache for the current size
0118     QString thumbPath;
0119     // Original URL of current item in RFC2396 format
0120     // (file:///path/to/a%20file instead of file:/path/to/a file)
0121     QByteArray origName;
0122     // Thumbnail file name for current item
0123     QString thumbName;
0124     // Size of thumbnail
0125     int width;
0126     int height;
0127     // Unscaled size of thumbnail (128, 256 or 512 if cache is enabled)
0128     short cacheSize;
0129     // Whether the thumbnail should be scaled
0130     bool bScale;
0131     // Whether we should save the thumbnail
0132     bool bSave;
0133     bool ignoreMaximumSize;
0134     int sequenceIndex;
0135     bool succeeded;
0136     // If the file to create a thumb for was a temp file, this is its name
0137     QString tempName;
0138     KIO::filesize_t maximumLocalSize;
0139     KIO::filesize_t maximumRemoteSize;
0140 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102)
0141     // the size for the icon overlay
0142     int iconSize;
0143     // the transparency of the blended MIME type icon
0144     int iconAlpha;
0145 #endif
0146     // Shared memory segment Id. The segment is allocated to a size
0147     // of extent x extent x 4 (32 bit image) on first need.
0148     int shmid;
0149     // And the data area
0150     uchar *shmaddr;
0151     // Size of the shm segment
0152     size_t shmsize;
0153     // Root of thumbnail cache
0154     QString thumbRoot;
0155     // Metadata returned from the KIO thumbnail worker
0156     QMap<QString, QString> thumbnailWorkerMetaData;
0157     int devicePixelRatio = s_defaultDevicePixelRatio;
0158     static const int idUnknown = -1;
0159     // Id of a device storing currently processed file
0160     int currentDeviceId = 0;
0161     // Device ID for each file. Stored while in STATE_DEVICE_INFO state, used later on.
0162     QMap<QString, int> deviceIdMap;
0163     enum CachePolicy { Prevent, Allow, Unknown } currentDeviceCachePolicy = Unknown;
0164 
0165     void getOrCreateThumbnail();
0166     bool statResultThumbnail();
0167     void createThumbnail(const QString &);
0168     void cleanupTempFile();
0169     void determineNextFile();
0170     void emitPreview(const QImage &thumb);
0171 
0172     void startPreview();
0173     void slotThumbData(KIO::Job *, const QByteArray &);
0174     // Checks if thumbnail is on encrypted partition different than thumbRoot
0175     CachePolicy canBeCached(const QString &path);
0176     int getDeviceId(const QString &path);
0177 
0178     Q_DECLARE_PUBLIC(PreviewJob)
0179 
0180     static QVector<KPluginMetaData> loadAvailablePlugins()
0181     {
0182         static QVector<KPluginMetaData> jsonMetaDataPlugins;
0183         if (!jsonMetaDataPlugins.isEmpty()) {
0184             return jsonMetaDataPlugins;
0185         }
0186         jsonMetaDataPlugins = KPluginMetaData::findPlugins(QStringLiteral("kf" QT_STRINGIFY(QT_VERSION_MAJOR) "/thumbcreator"));
0187         std::set<QString> pluginIds;
0188         for (const KPluginMetaData &data : std::as_const(jsonMetaDataPlugins)) {
0189             pluginIds.insert(data.pluginId());
0190         }
0191 #if KSERVICE_ENABLE_DEPRECATED_SINCE(5, 88)
0192         QT_WARNING_PUSH
0193         QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
0194         QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
0195         const KService::List plugins = KServiceTypeTrader::self()->query(QStringLiteral("ThumbCreator"));
0196         for (const auto &plugin : plugins) {
0197             if (KPluginInfo info(plugin); info.isValid()) {
0198                 if (auto [it, inserted] = pluginIds.insert(plugin->desktopEntryName()); inserted) {
0199                     jsonMetaDataPlugins << info.toMetaData();
0200                 }
0201             } else {
0202                 // Hack for directory thumbnailer: It has a hardcoded plugin id in the KIO worker and not any C++ plugin
0203                 // Consequently we just use the base name as the plugin file for our KPluginMetaData object
0204                 const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kservices5/") + plugin->entryPath());
0205                 KPluginMetaData tmpData = KPluginMetaData::fromDesktopFile(path);
0206                 jsonMetaDataPlugins << KPluginMetaData(tmpData.rawData(), QFileInfo(path).baseName(), path);
0207             }
0208         }
0209         QT_WARNING_POP
0210 #else
0211 #pragma message("TODO: directory thumbnailer needs a non-desktop file solution ")
0212 #endif
0213         return jsonMetaDataPlugins;
0214     }
0215 };
0216 
0217 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 86)
0218 void PreviewJob::setDefaultDevicePixelRatio(int defaultDevicePixelRatio)
0219 {
0220     s_defaultDevicePixelRatio = defaultDevicePixelRatio;
0221 }
0222 #endif
0223 
0224 void PreviewJob::setDefaultDevicePixelRatio(qreal defaultDevicePixelRatio)
0225 {
0226     s_defaultDevicePixelRatio = std::ceil(defaultDevicePixelRatio);
0227 }
0228 
0229 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 7)
0230 PreviewJob::PreviewJob(const KFileItemList &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins)
0231     : KIO::Job(*new PreviewJobPrivate(items, QSize(width, height ? height : width)))
0232 {
0233     Q_D(PreviewJob);
0234     d->enabledPlugins = enabledPlugins ? *enabledPlugins : availablePlugins();
0235     d->iconSize = iconSize;
0236     d->iconAlpha = iconAlpha;
0237     d->bScale = scale;
0238     d->bSave = save && scale;
0239 
0240     // Return to event loop first, determineNextFile() might delete this;
0241     QTimer::singleShot(0, this, [d]() {
0242         d->startPreview();
0243     });
0244 }
0245 #endif
0246 
0247 PreviewJob::PreviewJob(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins)
0248     : KIO::Job(*new PreviewJobPrivate(items, size))
0249 {
0250     Q_D(PreviewJob);
0251 
0252     if (enabledPlugins) {
0253         d->enabledPlugins = *enabledPlugins;
0254     } else {
0255         const KConfigGroup globalConfig(KSharedConfig::openConfig(), "PreviewSettings");
0256         d->enabledPlugins =
0257             globalConfig.readEntry("Plugins",
0258                                    QStringList{QStringLiteral("directorythumbnail"), QStringLiteral("imagethumbnail"), QStringLiteral("jpegthumbnail")});
0259     }
0260 
0261     // Return to event loop first, determineNextFile() might delete this;
0262     QTimer::singleShot(0, this, [d]() {
0263         d->startPreview();
0264     });
0265 }
0266 
0267 PreviewJob::~PreviewJob()
0268 {
0269 #if WITH_SHM
0270     Q_D(PreviewJob);
0271     if (d->shmaddr) {
0272         shmdt((char *)d->shmaddr);
0273         shmctl(d->shmid, IPC_RMID, nullptr);
0274     }
0275 #endif
0276 }
0277 
0278 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102)
0279 void PreviewJob::setOverlayIconSize(int size)
0280 {
0281     Q_D(PreviewJob);
0282     d->iconSize = size;
0283 }
0284 #endif
0285 
0286 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102)
0287 int PreviewJob::overlayIconSize() const
0288 {
0289     Q_D(const PreviewJob);
0290     return d->iconSize;
0291 }
0292 #endif
0293 
0294 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102)
0295 void PreviewJob::setOverlayIconAlpha(int alpha)
0296 {
0297     Q_D(PreviewJob);
0298     d->iconAlpha = qBound(0, alpha, 255);
0299 }
0300 #endif
0301 
0302 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102)
0303 int PreviewJob::overlayIconAlpha() const
0304 {
0305     Q_D(const PreviewJob);
0306     return d->iconAlpha;
0307 }
0308 #endif
0309 
0310 void PreviewJob::setScaleType(ScaleType type)
0311 {
0312     Q_D(PreviewJob);
0313     switch (type) {
0314     case Unscaled:
0315         d->bScale = false;
0316         d->bSave = false;
0317         break;
0318     case Scaled:
0319         d->bScale = true;
0320         d->bSave = false;
0321         break;
0322     case ScaledAndCached:
0323         d->bScale = true;
0324         d->bSave = true;
0325         break;
0326     default:
0327         break;
0328     }
0329 }
0330 
0331 PreviewJob::ScaleType PreviewJob::scaleType() const
0332 {
0333     Q_D(const PreviewJob);
0334     if (d->bScale) {
0335         return d->bSave ? ScaledAndCached : Scaled;
0336     }
0337     return Unscaled;
0338 }
0339 
0340 void PreviewJobPrivate::startPreview()
0341 {
0342     Q_Q(PreviewJob);
0343     // Load the list of plugins to determine which MIME types are supported
0344     const QVector<KPluginMetaData> plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
0345     QMap<QString, KPluginMetaData> mimeMap;
0346     QHash<QString, QHash<QString, KPluginMetaData>> protocolMap;
0347 
0348     for (const KPluginMetaData &plugin : plugins) {
0349         QStringList protocols = plugin.value(QStringLiteral("X-KDE-Protocols"), QStringList());
0350         const QString p = plugin.value(QStringLiteral("X-KDE-Protocol"));
0351         if (!p.isEmpty()) {
0352             protocols.append(p);
0353         }
0354         for (const QString &protocol : std::as_const(protocols)) {
0355             // Add supported MIME type for this protocol
0356             QStringList &_ms = m_remoteProtocolPlugins[protocol];
0357             const auto mimeTypes = plugin.mimeTypes();
0358             for (const QString &_m : mimeTypes) {
0359                 protocolMap[protocol].insert(_m, plugin);
0360                 if (!_ms.contains(_m)) {
0361                     _ms.append(_m);
0362                 }
0363             }
0364         }
0365         if (enabledPlugins.contains(plugin.pluginId())) {
0366             const auto mimeTypes = plugin.mimeTypes();
0367             for (const QString &mimeType : mimeTypes) {
0368                 mimeMap.insert(mimeType, plugin);
0369             }
0370         }
0371     }
0372 
0373     // Look for images and store the items in our todo list :)
0374     bool bNeedCache = false;
0375     for (const auto &fileItem : std::as_const(initialItems)) {
0376         PreviewItem item;
0377         item.item = fileItem;
0378 
0379         const QString mimeType = item.item.mimetype();
0380         KPluginMetaData plugin;
0381 
0382         // look for protocol-specific thumbnail plugins first
0383         auto it = protocolMap.constFind(item.item.url().scheme());
0384         if (it != protocolMap.constEnd()) {
0385             plugin = it.value().value(mimeType);
0386         }
0387 
0388         if (!plugin.isValid()) {
0389             auto pluginIt = mimeMap.constFind(mimeType);
0390             if (pluginIt == mimeMap.constEnd()) {
0391                 // check MIME type inheritance, resolve aliases
0392                 QMimeDatabase db;
0393                 const QMimeType mimeInfo = db.mimeTypeForName(mimeType);
0394                 if (mimeInfo.isValid()) {
0395                     const QStringList parentMimeTypes = mimeInfo.allAncestors();
0396                     for (const QString &parentMimeType : parentMimeTypes) {
0397                         pluginIt = mimeMap.constFind(parentMimeType);
0398                         if (pluginIt != mimeMap.constEnd()) {
0399                             break;
0400                         }
0401                     }
0402                 }
0403 
0404                 if (pluginIt == mimeMap.constEnd()) {
0405                     // Check the wildcards last, see BUG 453480
0406                     QString groupMimeType = mimeType;
0407                     const int slashIdx = groupMimeType.indexOf(QLatin1Char('/'));
0408                     if (slashIdx != -1) {
0409                         // Replace everything after '/' with '*'
0410                         groupMimeType.truncate(slashIdx + 1);
0411                         groupMimeType += QLatin1Char('*');
0412                     }
0413                     pluginIt = mimeMap.constFind(groupMimeType);
0414                 }
0415             }
0416 
0417             if (pluginIt != mimeMap.constEnd()) {
0418                 plugin = *pluginIt;
0419             }
0420         }
0421 
0422         if (plugin.isValid()) {
0423             item.plugin = plugin;
0424             items.push_back(item);
0425             if (!bNeedCache && bSave && plugin.value(QStringLiteral("CacheThumbnail"), true)) {
0426                 const QUrl url = fileItem.url();
0427                 if (!url.isLocalFile() || !url.adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot)) {
0428                     bNeedCache = true;
0429                 }
0430             }
0431         } else {
0432             Q_EMIT q->failed(fileItem);
0433         }
0434     }
0435 
0436     KConfigGroup cg(KSharedConfig::openConfig(), "PreviewSettings");
0437     maximumLocalSize = cg.readEntry("MaximumSize", std::numeric_limits<KIO::filesize_t>::max());
0438     maximumRemoteSize = cg.readEntry<KIO::filesize_t>("MaximumRemoteSize", 0);
0439 
0440     if (bNeedCache) {
0441         const int longer = std::max(width, height);
0442         if (longer <= 128) {
0443             cacheSize = 128;
0444         } else if (longer <= 256) {
0445             cacheSize = 256;
0446         } else if (longer <= 512) {
0447             cacheSize = 512;
0448         } else {
0449             cacheSize = 1024;
0450         }
0451 
0452         struct CachePool {
0453             QString path;
0454             int minSize;
0455         };
0456 
0457         const static auto pools = {
0458             CachePool{QStringLiteral("/normal/"), 128},
0459             CachePool{QStringLiteral("/large/"), 256},
0460             CachePool{QStringLiteral("/x-large/"), 512},
0461             CachePool{QStringLiteral("/xx-large/"), 1024},
0462         };
0463 
0464         QString thumbDir;
0465         int wants = devicePixelRatio * cacheSize;
0466         for (const auto &p : pools) {
0467             if (p.minSize < wants) {
0468                 continue;
0469             } else {
0470                 thumbDir = p.path;
0471                 break;
0472             }
0473         }
0474         thumbPath = thumbRoot + thumbDir;
0475 
0476         if (!QDir(thumbPath).exists()) {
0477             if (QDir().mkpath(thumbPath)) { // Qt5 TODO: mkpath(dirPath, permissions)
0478                 QFile f(thumbPath);
0479                 f.setPermissions(QFile::ReadUser | QFile::WriteUser | QFile::ExeUser); // 0700
0480             }
0481         }
0482     } else {
0483         bSave = false;
0484     }
0485 
0486     initialItems.clear();
0487     determineNextFile();
0488 }
0489 
0490 void PreviewJob::removeItem(const QUrl &url)
0491 {
0492     Q_D(PreviewJob);
0493 
0494     auto it = std::find_if(d->items.cbegin(), d->items.cend(), [&url](const PreviewItem &pItem) {
0495         return url == pItem.item.url();
0496     });
0497     if (it != d->items.cend()) {
0498         d->items.erase(it);
0499     }
0500 
0501     if (d->currentItem.item.url() == url) {
0502         KJob *job = subjobs().first();
0503         job->kill();
0504         removeSubjob(job);
0505         d->determineNextFile();
0506     }
0507 }
0508 
0509 void KIO::PreviewJob::setSequenceIndex(int index)
0510 {
0511     d_func()->sequenceIndex = index;
0512 }
0513 
0514 int KIO::PreviewJob::sequenceIndex() const
0515 {
0516     return d_func()->sequenceIndex;
0517 }
0518 
0519 float KIO::PreviewJob::sequenceIndexWraparoundPoint() const
0520 {
0521     return d_func()->thumbnailWorkerMetaData.value(QStringLiteral("sequenceIndexWraparoundPoint"), QStringLiteral("-1.0")).toFloat();
0522 }
0523 
0524 bool KIO::PreviewJob::handlesSequences() const
0525 {
0526     return d_func()->thumbnailWorkerMetaData.value(QStringLiteral("handlesSequences")) == QStringLiteral("1");
0527 }
0528 
0529 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 86)
0530 void KIO::PreviewJob::setDevicePixelRatio(int dpr)
0531 {
0532     d_func()->devicePixelRatio = dpr;
0533 }
0534 #endif
0535 
0536 void KIO::PreviewJob::setDevicePixelRatio(qreal dpr)
0537 {
0538     d_func()->devicePixelRatio = std::ceil(dpr);
0539 }
0540 
0541 void PreviewJob::setIgnoreMaximumSize(bool ignoreSize)
0542 {
0543     d_func()->ignoreMaximumSize = ignoreSize;
0544 }
0545 
0546 void PreviewJobPrivate::cleanupTempFile()
0547 {
0548     if (!tempName.isEmpty()) {
0549         Q_ASSERT((!QFileInfo(tempName).isDir() && QFileInfo(tempName).isFile()) || QFileInfo(tempName).isSymLink());
0550         QFile::remove(tempName);
0551         tempName.clear();
0552     }
0553 }
0554 
0555 void PreviewJobPrivate::determineNextFile()
0556 {
0557     Q_Q(PreviewJob);
0558     if (!currentItem.item.isNull()) {
0559         if (!succeeded) {
0560             Q_EMIT q->failed(currentItem.item);
0561         }
0562     }
0563     // No more items ?
0564     if (items.empty()) {
0565         q->emitResult();
0566         return;
0567     } else {
0568         // First, stat the orig file
0569         state = PreviewJobPrivate::STATE_STATORIG;
0570         currentItem = items.front();
0571         items.pop_front();
0572         succeeded = false;
0573         KIO::Job *job = KIO::statDetails(currentItem.item.url(), StatJob::SourceSide, KIO::StatDefaultDetails | KIO::StatInode, KIO::HideProgressInfo);
0574         job->addMetaData(QStringLiteral("thumbnail"), QStringLiteral("1"));
0575         job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
0576         q->addSubjob(job);
0577     }
0578 }
0579 
0580 void PreviewJob::slotResult(KJob *job)
0581 {
0582     Q_D(PreviewJob);
0583 
0584     removeSubjob(job);
0585     Q_ASSERT(!hasSubjobs()); // We should have only one job at a time ...
0586     switch (d->state) {
0587     case PreviewJobPrivate::STATE_STATORIG: {
0588         if (job->error()) { // that's no good news...
0589             // Drop this one and move on to the next one
0590             d->determineNextFile();
0591             return;
0592         }
0593         const KIO::UDSEntry statResult = static_cast<KIO::StatJob *>(job)->statResult();
0594         d->currentDeviceId = statResult.numberValue(KIO::UDSEntry::UDS_DEVICE_ID, 0);
0595         d->tOrig = QDateTime::fromSecsSinceEpoch(statResult.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, 0));
0596 
0597         bool skipCurrentItem = false;
0598         const KIO::filesize_t size = (KIO::filesize_t)statResult.numberValue(KIO::UDSEntry::UDS_SIZE, 0);
0599         const QUrl itemUrl = d->currentItem.item.mostLocalUrl();
0600 
0601         if (itemUrl.isLocalFile() || KProtocolInfo::protocolClass(itemUrl.scheme()) == QLatin1String(":local")) {
0602             skipCurrentItem = !d->ignoreMaximumSize && size > d->maximumLocalSize && !d->currentItem.plugin.value(QStringLiteral("IgnoreMaximumSize"), false);
0603         } else {
0604             // For remote items the "IgnoreMaximumSize" plugin property is not respected
0605             skipCurrentItem = !d->ignoreMaximumSize && size > d->maximumRemoteSize;
0606 
0607             // Remote directories are not supported, don't try to do a file_copy on them
0608             if (!skipCurrentItem) {
0609                 // TODO update item.mimeType from the UDS entry, in case it wasn't set initially
0610                 // But we don't use the MIME type anymore, we just use isDir().
0611                 if (d->currentItem.item.isDir()) {
0612                     skipCurrentItem = true;
0613                 }
0614             }
0615         }
0616         if (skipCurrentItem) {
0617             d->determineNextFile();
0618             return;
0619         }
0620 
0621         bool pluginHandlesSequences = d->currentItem.plugin.value(QStringLiteral("HandleSequences"), false);
0622         if (!d->currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true) || (d->sequenceIndex && pluginHandlesSequences)) {
0623             // This preview will not be cached, no need to look for a saved thumbnail
0624             // Just create it, and be done
0625             d->getOrCreateThumbnail();
0626             return;
0627         }
0628 
0629         if (d->statResultThumbnail()) {
0630             return;
0631         }
0632 
0633         d->getOrCreateThumbnail();
0634         return;
0635     }
0636     case PreviewJobPrivate::STATE_DEVICE_INFO: {
0637         KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job);
0638         int id;
0639         QString path = statJob->url().toLocalFile();
0640         if (job->error()) {
0641             // We set id to 0 to know we tried getting it
0642             qCWarning(KIO_WIDGETS) << "Cannot read information about filesystem under path" << path;
0643             id = 0;
0644         } else {
0645             id = statJob->statResult().numberValue(KIO::UDSEntry::UDS_DEVICE_ID, 0);
0646         }
0647         d->deviceIdMap[path] = id;
0648         d->createThumbnail(d->currentItem.item.localPath());
0649         return;
0650     }
0651     case PreviewJobPrivate::STATE_GETORIG: {
0652         if (job->error()) {
0653             d->cleanupTempFile();
0654             d->determineNextFile();
0655             return;
0656         }
0657 
0658         d->createThumbnail(static_cast<KIO::FileCopyJob *>(job)->destUrl().toLocalFile());
0659         return;
0660     }
0661     case PreviewJobPrivate::STATE_CREATETHUMB: {
0662         d->cleanupTempFile();
0663         d->determineNextFile();
0664         return;
0665     }
0666     }
0667 }
0668 
0669 bool PreviewJobPrivate::statResultThumbnail()
0670 {
0671     if (thumbPath.isEmpty()) {
0672         return false;
0673     }
0674 
0675     bool isLocal;
0676     const QUrl url = currentItem.item.mostLocalUrl(&isLocal);
0677     if (isLocal) {
0678         const QFileInfo localFile(url.toLocalFile());
0679         const QString canonicalPath = localFile.canonicalFilePath();
0680         origName = QUrl::fromLocalFile(canonicalPath).toEncoded(QUrl::RemovePassword | QUrl::FullyEncoded);
0681         if (origName.isEmpty()) {
0682             qCWarning(KIO_WIDGETS) << "Failed to convert" << url << "to canonical path";
0683             return false;
0684         }
0685     } else {
0686         // Don't include the password if any
0687         origName = url.toEncoded(QUrl::RemovePassword);
0688     }
0689 
0690     QCryptographicHash md5(QCryptographicHash::Md5);
0691     md5.addData(origName);
0692     thumbName = QString::fromLatin1(md5.result().toHex()) + QLatin1String(".png");
0693 
0694     QImage thumb;
0695     QFile thumbFile(thumbPath + thumbName);
0696     if (!thumbFile.open(QIODevice::ReadOnly) || !thumb.load(&thumbFile, "png")) {
0697         return false;
0698     }
0699 
0700     if (thumb.text(QStringLiteral("Thumb::URI")) != QString::fromUtf8(origName)
0701         || thumb.text(QStringLiteral("Thumb::MTime")).toLongLong() != tOrig.toSecsSinceEpoch()) {
0702         return false;
0703     }
0704 
0705     const QString origSize = thumb.text(QStringLiteral("Thumb::Size"));
0706     if (!origSize.isEmpty() && origSize.toULongLong() != currentItem.item.size()) {
0707         // Thumb::Size is not required, but if it is set it should match
0708         return false;
0709     }
0710 
0711     // The DPR of the loaded thumbnail is unspecified (and typically irrelevant).
0712     // When a thumbnail is DPR-invariant, use the DPR passed in the request.
0713     thumb.setDevicePixelRatio(devicePixelRatio);
0714 
0715     QString thumbnailerVersion = currentItem.plugin.value(QStringLiteral("ThumbnailerVersion"));
0716 
0717     if (!thumbnailerVersion.isEmpty() && thumb.text(QStringLiteral("Software")).startsWith(QLatin1String("KDE Thumbnail Generator"))) {
0718         // Check if the version matches
0719         // The software string should read "KDE Thumbnail Generator pluginName (vX)"
0720         QString softwareString = thumb.text(QStringLiteral("Software")).remove(QStringLiteral("KDE Thumbnail Generator")).trimmed();
0721         if (softwareString.isEmpty()) {
0722             // The thumbnail has been created with an older version, recreating
0723             return false;
0724         }
0725         int versionIndex = softwareString.lastIndexOf(QLatin1String("(v"));
0726         if (versionIndex < 0) {
0727             return false;
0728         }
0729 
0730         QString cachedVersion = softwareString.remove(0, versionIndex + 2);
0731         cachedVersion.chop(1);
0732         uint thumbnailerMajor = thumbnailerVersion.toInt();
0733         uint cachedMajor = cachedVersion.toInt();
0734         if (thumbnailerMajor > cachedMajor) {
0735             return false;
0736         }
0737     }
0738 
0739     // Found it, use it
0740     emitPreview(thumb);
0741     succeeded = true;
0742     determineNextFile();
0743     return true;
0744 }
0745 
0746 void PreviewJobPrivate::getOrCreateThumbnail()
0747 {
0748     Q_Q(PreviewJob);
0749     // We still need to load the orig file ! (This is getting tedious) :)
0750     const KFileItem &item = currentItem.item;
0751     const QString localPath = item.localPath();
0752     if (!localPath.isEmpty()) {
0753         createThumbnail(localPath);
0754     } else {
0755         const QUrl fileUrl = item.url();
0756         // heuristics for remote URL support
0757         bool supportsProtocol = false;
0758         if (m_remoteProtocolPlugins.value(fileUrl.scheme()).contains(item.mimetype())) {
0759             // There's a plugin supporting this protocol and MIME type
0760             supportsProtocol = true;
0761         } else if (m_remoteProtocolPlugins.value(QStringLiteral("KIO")).contains(item.mimetype())) {
0762             // Assume KIO understands any URL, ThumbCreator workers who have
0763             // X-KDE-Protocols=KIO will get fed the remote URL directly.
0764             supportsProtocol = true;
0765         }
0766 
0767         if (supportsProtocol) {
0768             createThumbnail(fileUrl.toString());
0769             return;
0770         }
0771         if (item.isDir()) {
0772             // Skip remote dirs (bug 208625)
0773             cleanupTempFile();
0774             determineNextFile();
0775             return;
0776         }
0777         // No plugin support access to this remote content, copy the file
0778         // to the local machine, then create the thumbnail
0779         state = PreviewJobPrivate::STATE_GETORIG;
0780         QTemporaryFile localFile;
0781         localFile.setAutoRemove(false);
0782         localFile.open();
0783         tempName = localFile.fileName();
0784         const QUrl currentURL = item.mostLocalUrl();
0785         KIO::Job *job = KIO::file_copy(currentURL, QUrl::fromLocalFile(tempName), -1, KIO::Overwrite | KIO::HideProgressInfo /* No GUI */);
0786         job->addMetaData(QStringLiteral("thumbnail"), QStringLiteral("1"));
0787         q->addSubjob(job);
0788     }
0789 }
0790 
0791 PreviewJobPrivate::CachePolicy PreviewJobPrivate::canBeCached(const QString &path)
0792 {
0793     // If checked file is directory on a different filesystem than its parent, we need to check it separately
0794     int separatorIndex = path.lastIndexOf(QLatin1Char('/'));
0795     // special case for root folders
0796     const QString parentDirPath = separatorIndex == 0 ? path : path.left(separatorIndex);
0797 
0798     int parentId = getDeviceId(parentDirPath);
0799     if (parentId == idUnknown) {
0800         return CachePolicy::Unknown;
0801     }
0802 
0803     bool isDifferentSystem = !parentId || parentId != currentDeviceId;
0804     if (!isDifferentSystem && currentDeviceCachePolicy != CachePolicy::Unknown) {
0805         return currentDeviceCachePolicy;
0806     }
0807     int checkedId;
0808     QString checkedPath;
0809     if (isDifferentSystem) {
0810         checkedId = currentDeviceId;
0811         checkedPath = path;
0812     } else {
0813         checkedId = getDeviceId(parentDirPath);
0814         checkedPath = parentDirPath;
0815         if (checkedId == idUnknown) {
0816             return CachePolicy::Unknown;
0817         }
0818     }
0819     // If we're checking different filesystem or haven't checked yet see if filesystem matches thumbRoot
0820     int thumbRootId = getDeviceId(thumbRoot);
0821     if (thumbRootId == idUnknown) {
0822         return CachePolicy::Unknown;
0823     }
0824     bool shouldAllow = checkedId && checkedId == thumbRootId;
0825     if (!shouldAllow) {
0826         Solid::Device device = Solid::Device::storageAccessFromPath(checkedPath);
0827         if (device.isValid()) {
0828             // If the checked device is encrypted, allow thumbnailing if the thumbnails are stored in an encrypted location.
0829             // Or, if the checked device is unencrypted, allow thumbnailing.
0830             if (device.as<Solid::StorageAccess>()->isEncrypted()) {
0831                 const Solid::Device thumbRootDevice = Solid::Device::storageAccessFromPath(thumbRoot);
0832                 shouldAllow = thumbRootDevice.isValid() && thumbRootDevice.as<Solid::StorageAccess>()->isEncrypted();
0833             } else {
0834                 shouldAllow = true;
0835             }
0836         }
0837     }
0838     if (!isDifferentSystem) {
0839         currentDeviceCachePolicy = shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent;
0840     }
0841     return shouldAllow ? CachePolicy::Allow : CachePolicy::Prevent;
0842 }
0843 
0844 int PreviewJobPrivate::getDeviceId(const QString &path)
0845 {
0846     Q_Q(PreviewJob);
0847     auto iter = deviceIdMap.find(path);
0848     if (iter != deviceIdMap.end()) {
0849         return iter.value();
0850     }
0851     QUrl url = QUrl::fromLocalFile(path);
0852     if (!url.isValid()) {
0853         qCWarning(KIO_WIDGETS) << "Could not get device id for file preview, Invalid url" << path;
0854         return 0;
0855     }
0856     state = PreviewJobPrivate::STATE_DEVICE_INFO;
0857     KIO::Job *job = KIO::statDetails(url, StatJob::SourceSide, KIO::StatDefaultDetails | KIO::StatInode, KIO::HideProgressInfo);
0858     job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true"));
0859     q->addSubjob(job);
0860 
0861     return idUnknown;
0862 }
0863 
0864 void PreviewJobPrivate::createThumbnail(const QString &pixPath)
0865 {
0866     Q_Q(PreviewJob);
0867     state = PreviewJobPrivate::STATE_CREATETHUMB;
0868     QUrl thumbURL;
0869     thumbURL.setScheme(QStringLiteral("thumbnail"));
0870     thumbURL.setPath(pixPath);
0871 
0872     bool save = bSave && currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true) && !sequenceIndex;
0873 
0874     bool isRemoteProtocol = currentItem.item.localPath().isEmpty();
0875     CachePolicy cachePolicy = isRemoteProtocol ? CachePolicy::Prevent : canBeCached(pixPath);
0876 
0877     if (cachePolicy == CachePolicy::Unknown) {
0878         // If Unknown is returned, creating thumbnail should be called again by slotResult
0879         return;
0880     }
0881 
0882     KIO::TransferJob *job = KIO::get(thumbURL, NoReload, HideProgressInfo);
0883     q->addSubjob(job);
0884     q->connect(job, &KIO::TransferJob::data, q, [this](KIO::Job *job, const QByteArray &data) {
0885         slotThumbData(job, data);
0886     });
0887 
0888     int thumb_width = width;
0889     int thumb_height = height;
0890 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102)
0891     int thumb_iconSize = iconSize;
0892 #endif
0893     if (save) {
0894         thumb_width = thumb_height = cacheSize;
0895 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102)
0896         thumb_iconSize = 64;
0897 #endif
0898     }
0899 
0900     job->addMetaData(QStringLiteral("mimeType"), currentItem.item.mimetype());
0901     job->addMetaData(QStringLiteral("width"), QString::number(thumb_width));
0902     job->addMetaData(QStringLiteral("height"), QString::number(thumb_height));
0903 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 102)
0904     job->addMetaData(QStringLiteral("iconSize"), QString::number(thumb_iconSize));
0905     job->addMetaData(QStringLiteral("iconAlpha"), QString::number(iconAlpha));
0906 #endif
0907     job->addMetaData(QStringLiteral("plugin"), currentItem.plugin.fileName());
0908     job->addMetaData(QStringLiteral("enabledPlugins"), enabledPlugins.join(QLatin1Char(',')));
0909     job->addMetaData(QStringLiteral("devicePixelRatio"), QString::number(devicePixelRatio));
0910     job->addMetaData(QStringLiteral("cache"), QString::number(cachePolicy == CachePolicy::Allow));
0911     if (sequenceIndex) {
0912         job->addMetaData(QStringLiteral("sequence-index"), QString::number(sequenceIndex));
0913     }
0914 
0915 #if WITH_SHM
0916     size_t requiredSize = thumb_width * devicePixelRatio * thumb_height * devicePixelRatio * 4;
0917     if (shmid == -1 || shmsize < requiredSize) {
0918         if (shmaddr) {
0919             // clean previous shared memory segment
0920             shmdt((char *)shmaddr);
0921             shmaddr = nullptr;
0922             shmctl(shmid, IPC_RMID, nullptr);
0923             shmid = -1;
0924         }
0925         if (requiredSize > 0) {
0926             shmid = shmget(IPC_PRIVATE, requiredSize, IPC_CREAT | 0600);
0927             if (shmid != -1) {
0928                 shmsize = requiredSize;
0929                 shmaddr = (uchar *)(shmat(shmid, nullptr, SHM_RDONLY));
0930                 if (shmaddr == (uchar *)-1) {
0931                     shmctl(shmid, IPC_RMID, nullptr);
0932                     shmaddr = nullptr;
0933                     shmid = -1;
0934                 }
0935             }
0936         }
0937     }
0938     if (shmid != -1) {
0939         job->addMetaData(QStringLiteral("shmid"), QString::number(shmid));
0940     }
0941 #endif
0942 }
0943 
0944 void PreviewJobPrivate::slotThumbData(KIO::Job *job, const QByteArray &data)
0945 {
0946     thumbnailWorkerMetaData = job->metaData();
0947     /* clang-format off */
0948     const bool save = bSave
0949                       && !sequenceIndex
0950                       && currentDeviceCachePolicy == CachePolicy::Allow
0951                       && currentItem.plugin.value(QStringLiteral("CacheThumbnail"), true)
0952                       && (!currentItem.item.url().isLocalFile()
0953                           || !currentItem.item.url().adjusted(QUrl::RemoveFilename).toLocalFile().startsWith(thumbRoot));
0954     /* clang-format on */
0955 
0956     QImage thumb;
0957 #if WITH_SHM
0958     if (shmaddr) {
0959         // Keep this in sync with kio-extras|thumbnail/thumbnail.cpp
0960         QDataStream str(data);
0961         int width;
0962         int height;
0963         quint8 iFormat;
0964         int imgDevicePixelRatio = 1;
0965         // TODO KF6: add a version number as first parameter
0966         str >> width >> height >> iFormat;
0967         if (iFormat & 0x80) {
0968             // HACK to deduce if imgDevicePixelRatio is present
0969             iFormat &= 0x7f;
0970             str >> imgDevicePixelRatio;
0971         }
0972         QImage::Format format = static_cast<QImage::Format>(iFormat);
0973         thumb = QImage(shmaddr, width, height, format).copy();
0974         thumb.setDevicePixelRatio(imgDevicePixelRatio);
0975     } else {
0976         thumb.loadFromData(data);
0977     }
0978 #else
0979     thumb.loadFromData(data);
0980 #endif
0981 
0982     if (thumb.isNull()) {
0983         QDataStream s(data);
0984         s >> thumb;
0985     }
0986 
0987     if (save) {
0988         thumb.setText(QStringLiteral("Thumb::URI"), QString::fromUtf8(origName));
0989         thumb.setText(QStringLiteral("Thumb::MTime"), QString::number(tOrig.toSecsSinceEpoch()));
0990         thumb.setText(QStringLiteral("Thumb::Size"), number(currentItem.item.size()));
0991         thumb.setText(QStringLiteral("Thumb::Mimetype"), currentItem.item.mimetype());
0992         QString thumbnailerVersion = currentItem.plugin.value(QStringLiteral("ThumbnailerVersion"));
0993         QString signature = QLatin1String("KDE Thumbnail Generator ") + currentItem.plugin.name();
0994         if (!thumbnailerVersion.isEmpty()) {
0995             signature.append(QLatin1String(" (v") + thumbnailerVersion + QLatin1Char(')'));
0996         }
0997         thumb.setText(QStringLiteral("Software"), signature);
0998         QSaveFile saveFile(thumbPath + thumbName);
0999         if (saveFile.open(QIODevice::WriteOnly)) {
1000             if (thumb.save(&saveFile, "PNG")) {
1001                 saveFile.commit();
1002             }
1003         }
1004     }
1005     emitPreview(thumb);
1006     succeeded = true;
1007 }
1008 
1009 void PreviewJobPrivate::emitPreview(const QImage &thumb)
1010 {
1011     Q_Q(PreviewJob);
1012     QPixmap pix;
1013     const qreal ratio = thumb.devicePixelRatio();
1014     if (thumb.width() > width * ratio || thumb.height() > height * ratio) {
1015         pix = QPixmap::fromImage(thumb.scaled(QSize(width * ratio, height * ratio), Qt::KeepAspectRatio, Qt::SmoothTransformation));
1016     } else {
1017         pix = QPixmap::fromImage(thumb);
1018     }
1019     pix.setDevicePixelRatio(ratio);
1020     Q_EMIT q->gotPreview(currentItem.item, pix);
1021 }
1022 
1023 QVector<KPluginMetaData> PreviewJob::availableThumbnailerPlugins()
1024 {
1025     return PreviewJobPrivate::loadAvailablePlugins();
1026 }
1027 
1028 QStringList PreviewJob::availablePlugins()
1029 {
1030     QStringList result;
1031     const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
1032     for (const KPluginMetaData &plugin : plugins) {
1033         result << plugin.pluginId();
1034     }
1035     return result;
1036 }
1037 
1038 QStringList PreviewJob::defaultPlugins()
1039 {
1040     const QStringList blacklist = QStringList() << QStringLiteral("textthumbnail");
1041 
1042     QStringList defaultPlugins = availablePlugins();
1043     for (const QString &plugin : blacklist) {
1044         defaultPlugins.removeAll(plugin);
1045     }
1046 
1047     return defaultPlugins;
1048 }
1049 
1050 QStringList PreviewJob::supportedMimeTypes()
1051 {
1052     QStringList result;
1053     const auto plugins = KIO::PreviewJobPrivate::loadAvailablePlugins();
1054     for (const KPluginMetaData &plugin : plugins) {
1055         result += plugin.mimeTypes();
1056     }
1057     return result;
1058 }
1059 
1060 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 7)
1061 PreviewJob *
1062 KIO::filePreview(const KFileItemList &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins)
1063 {
1064     return new PreviewJob(items, width, height, iconSize, iconAlpha, scale, save, enabledPlugins);
1065 }
1066 #endif
1067 
1068 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 7)
1069 PreviewJob *
1070 KIO::filePreview(const QList<QUrl> &items, int width, int height, int iconSize, int iconAlpha, bool scale, bool save, const QStringList *enabledPlugins)
1071 {
1072     KFileItemList fileItems;
1073     fileItems.reserve(items.size());
1074     for (const QUrl &url : items) {
1075         Q_ASSERT(url.isValid()); // please call us with valid urls only
1076         fileItems.append(KFileItem(url));
1077     }
1078     return new PreviewJob(fileItems, width, height, iconSize, iconAlpha, scale, save, enabledPlugins);
1079 }
1080 #endif
1081 
1082 PreviewJob *KIO::filePreview(const KFileItemList &items, const QSize &size, const QStringList *enabledPlugins)
1083 {
1084     return new PreviewJob(items, size, enabledPlugins);
1085 }
1086 
1087 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(4, 5)
1088 KIO::filesize_t PreviewJob::maximumFileSize()
1089 {
1090     KConfigGroup cg(KSharedConfig::openConfig(), "PreviewSettings");
1091     return cg.readEntry("MaximumSize", 5 * 1024 * 1024LL /* 5MB */);
1092 }
1093 #endif
1094 
1095 #include "moc_previewjob.cpp"