File indexing completed on 2024-04-28 03:55:33

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