File indexing completed on 2024-04-21 04:57:28

0001 /*
0002  *   SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz19@gmail.com>
0003  *   SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>
0004  *
0005  *   SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #include "kio_filenamesearch.h"
0009 #include "kio_filenamesearch_p.h"
0010 
0011 #include "kio_filenamesearch_debug.h"
0012 
0013 #include <KIO/FileCopyJob>
0014 #include <KIO/ListJob>
0015 #include <KLocalizedString>
0016 
0017 #include <QCoreApplication>
0018 #include <QDBusInterface>
0019 #include <QMimeDatabase>
0020 #include <QRegularExpression>
0021 #include <QTemporaryFile>
0022 #include <QUrl>
0023 #include <QUrlQuery>
0024 
0025 // Pseudo plugin class to embed meta data
0026 class KIOPluginForMetaData : public QObject
0027 {
0028     Q_OBJECT
0029     Q_PLUGIN_METADATA(IID "org.kde.kio.worker.filenamesearch" FILE "filenamesearch.json")
0030 };
0031 
0032 static bool contentContainsPattern(const QUrl &url, const QRegularExpression &regex)
0033 {
0034     auto fileContainsPattern = [&](const QString &path) {
0035         QFile file(path);
0036         if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
0037             return false;
0038         }
0039 
0040         QTextStream in(&file);
0041         while (!in.atEnd()) {
0042             const QString line = in.readLine();
0043             if (regex.match(line).hasMatch()) {
0044                 return true;
0045             }
0046         }
0047 
0048         return false;
0049     };
0050 
0051     if (url.isLocalFile()) {
0052         return fileContainsPattern(url.toLocalFile());
0053     } else {
0054         QTemporaryFile tempFile;
0055         if (tempFile.open()) {
0056             const QString tempName = tempFile.fileName();
0057             KIO::Job *getJob = KIO::file_copy(url, QUrl::fromLocalFile(tempName), -1, KIO::Overwrite | KIO::HideProgressInfo);
0058             if (getJob->exec()) {
0059                 // The non-local file was downloaded successfully.
0060                 return fileContainsPattern(tempName);
0061             }
0062         }
0063     }
0064 
0065     return false;
0066 }
0067 
0068 static bool match(const KIO::UDSEntry &entry, const QRegularExpression &regex, bool searchContents)
0069 {
0070     if (!searchContents) {
0071         return regex.match(entry.stringValue(KIO::UDSEntry::UDS_NAME)).hasMatch();
0072     } else {
0073         const QUrl entryUrl(entry.stringValue(KIO::UDSEntry::UDS_URL));
0074         QMimeDatabase mdb;
0075         QMimeType mimetype = mdb.mimeTypeForUrl(entryUrl);
0076         if (mimetype.inherits(QStringLiteral("text/plain"))) {
0077             return contentContainsPattern(entryUrl, regex);
0078         }
0079     }
0080 
0081     return false;
0082 }
0083 
0084 FileNameSearchProtocol::FileNameSearchProtocol(const QByteArray &pool, const QByteArray &app)
0085     : QObject()
0086     , WorkerBase("search", pool, app)
0087 {
0088     QDBusInterface kded(QStringLiteral("org.kde.kded6"), QStringLiteral("/kded"), QStringLiteral("org.kde.kded6"));
0089     kded.call(QStringLiteral("loadModule"), QStringLiteral("filenamesearchmodule"));
0090 }
0091 
0092 FileNameSearchProtocol::~FileNameSearchProtocol() = default;
0093 
0094 KIO::WorkerResult FileNameSearchProtocol::stat(const QUrl &url)
0095 {
0096     KIO::UDSEntry uds;
0097     uds.reserve(9);
0098     uds.fastInsert(KIO::UDSEntry::UDS_ACCESS, 0700);
0099     uds.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
0100     uds.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
0101     uds.fastInsert(KIO::UDSEntry::UDS_ICON_OVERLAY_NAMES, QStringLiteral("baloo"));
0102     uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_TYPE, i18n("Search Folder"));
0103     uds.fastInsert(KIO::UDSEntry::UDS_URL, url.url());
0104 
0105     QUrlQuery query(url);
0106     QString title = query.queryItemValue(QStringLiteral("title"), QUrl::FullyDecoded);
0107     if (!title.isEmpty()) {
0108         uds.fastInsert(KIO::UDSEntry::UDS_NAME, title);
0109         uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, title);
0110     }
0111 
0112     statEntry(uds);
0113     return KIO::WorkerResult::pass();
0114 }
0115 
0116 // Create a UDSEntry for "."
0117 void FileNameSearchProtocol::listRootEntry()
0118 {
0119     KIO::UDSEntry entry;
0120     entry.reserve(4);
0121     entry.fastInsert(KIO::UDSEntry::UDS_NAME, QStringLiteral("."));
0122     entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
0123     entry.fastInsert(KIO::UDSEntry::UDS_SIZE, 0);
0124     entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IXOTH);
0125     listEntry(entry);
0126 }
0127 
0128 KIO::WorkerResult FileNameSearchProtocol::listDir(const QUrl &url)
0129 {
0130     listRootEntry();
0131 
0132     const QUrlQuery urlQuery(url);
0133     const QString search = urlQuery.queryItemValue(QStringLiteral("search"));
0134     if (search.isEmpty()) {
0135         return KIO::WorkerResult::pass();
0136     }
0137 
0138     const QRegularExpression regex(search, QRegularExpression::CaseInsensitiveOption);
0139     if (!regex.isValid()) {
0140         qCWarning(KIO_FILENAMESEARCH) << "Invalid QRegularExpression/PCRE search pattern:" << search;
0141         return KIO::WorkerResult::pass();
0142     }
0143 
0144     const QUrl dirUrl = QUrl(urlQuery.queryItemValue(QStringLiteral("url")));
0145 
0146     // Don't try to iterate the /proc directory of Linux
0147     if (dirUrl.isLocalFile() && dirUrl.toLocalFile() == QLatin1String("/proc")) {
0148         return KIO::WorkerResult::pass();
0149     }
0150 
0151     const bool isContent = urlQuery.queryItemValue(QStringLiteral("checkContent")) == QLatin1String("yes");
0152 
0153     std::set<QString> iteratedDirs;
0154     std::queue<QUrl> pendingDirs;
0155 
0156     searchDir(dirUrl, regex, isContent, iteratedDirs, pendingDirs);
0157 
0158     while (!pendingDirs.empty()) {
0159         const QUrl pendingUrl = pendingDirs.front();
0160         pendingDirs.pop();
0161         searchDir(pendingUrl, regex, isContent, iteratedDirs, pendingDirs);
0162     }
0163 
0164     return KIO::WorkerResult::pass();
0165 }
0166 
0167 void FileNameSearchProtocol::searchDir(const QUrl &dirUrl,
0168                                        const QRegularExpression &regex,
0169                                        bool searchContents,
0170                                        std::set<QString> &iteratedDirs,
0171                                        std::queue<QUrl> &pendingDirs)
0172 {
0173     KIO::ListJob *listJob = KIO::listRecursive(dirUrl, KIO::HideProgressInfo, KIO::ListJob::ListFlags{});
0174 
0175     connect(this, &QObject::destroyed, listJob, [listJob]() {
0176         listJob->kill();
0177     });
0178 
0179     connect(listJob, &KIO::ListJob::entries, this, [&](KJob *, const KIO::UDSEntryList &list) {
0180         if (wasKilled()) { // don't finish the search if we are meant to shut down
0181             listJob->kill();
0182             return;
0183         }
0184 
0185         if (listJob->error()) {
0186             qCWarning(KIO_FILENAMESEARCH) << "Searching failed:" << listJob->errorText();
0187             return;
0188         }
0189 
0190         for (auto entry : list) {
0191             QUrl entryUrl(dirUrl);
0192             QString path = entryUrl.path();
0193             if (!path.endsWith(QLatin1Char('/'))) {
0194                 path += QLatin1Char('/');
0195             }
0196             // UDS_NAME is e.g. "foo/bar/somefile.txt"
0197             entryUrl.setPath(path + entry.stringValue(KIO::UDSEntry::UDS_NAME));
0198 
0199             const QString urlStr = entryUrl.toDisplayString();
0200             entry.replace(KIO::UDSEntry::UDS_URL, urlStr);
0201 
0202             const QString fileName = entryUrl.fileName();
0203             entry.replace(KIO::UDSEntry::UDS_NAME, fileName);
0204 
0205             if (entry.isDir()) {
0206                 // Also search the target of a dir symlink
0207                 if (const QString linkDest = entry.stringValue(KIO::UDSEntry::UDS_LINK_DEST); !linkDest.isEmpty()) {
0208                     // Remember the dir to prevent endless loops
0209                     const auto [it, isInserted] = iteratedDirs.insert(linkDest);
0210                     if (isInserted) {
0211                         pendingDirs.push(entryUrl.resolved(QUrl(linkDest)));
0212                     }
0213                 }
0214 
0215                 iteratedDirs.insert(urlStr);
0216             }
0217 
0218             if (match(entry, regex, searchContents)) {
0219                 // UDS_DISPLAY_NAME is e.g. "foo/bar/somefile.txt"
0220                 entry.replace(KIO::UDSEntry::UDS_DISPLAY_NAME, fileName);
0221                 listEntry(entry);
0222             }
0223         }
0224     });
0225 
0226     listJob->exec();
0227 }
0228 
0229 extern "C" int Q_DECL_EXPORT kdemain(int argc, char **argv)
0230 {
0231     QCoreApplication app(argc, argv);
0232 
0233     if (argc != 4) {
0234         qCDebug(KIO_FILENAMESEARCH) << "Usage: kio_filenamesearch protocol domain-socket1 domain-socket2";
0235         return -1;
0236     }
0237 
0238     FileNameSearchProtocol worker(argv[2], argv[3]);
0239     worker.dispatchLoop();
0240 
0241     return 0;
0242 }
0243 
0244 #include "kio_filenamesearch.moc"
0245 #include "moc_kio_filenamesearch.cpp"