File indexing completed on 2024-05-19 05:00:33

0001 /*
0002  *   SPDX-FileCopyrightText: 2019 Méven Car <meven.car@kdemail.net>
0003  *
0004  *   SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005  */
0006 
0007 #include "recentlyused.h"
0008 #include "recentlyused-logsettings.h"
0009 
0010 #include <QCoreApplication>
0011 #include <QDataStream>
0012 #include <QUrl>
0013 #include <QUrlQuery>
0014 
0015 #include <KIO/StatJob>
0016 #include <KLocalizedString>
0017 
0018 #include <PlasmaActivities/Stats/Cleaning>
0019 #include <PlasmaActivities/Stats/ResultModel>
0020 #include <PlasmaActivities/Stats/Terms>
0021 
0022 #ifdef Q_OS_WIN
0023 #include <sys/stat.h>
0024 #endif
0025 
0026 namespace KAStats = KActivities::Stats;
0027 
0028 using namespace KAStats;
0029 using namespace KAStats::Terms;
0030 
0031 // Pseudo plugin class to embed meta data
0032 class KIOPluginForMetaData : public QObject
0033 {
0034     Q_OBJECT
0035     Q_PLUGIN_METADATA(IID "org.kde.kio.worker.recentlyused" FILE "recentlyused.json")
0036 };
0037 
0038 extern "C" int Q_DECL_EXPORT kdemain(int argc, char **argv)
0039 {
0040     // necessary to use other kio workers
0041     QCoreApplication app(argc, argv);
0042     app.setApplicationName(QStringLiteral("kio_recentlyused"));
0043     if (argc != 4) {
0044         fprintf(stderr, "Usage: kio_recentlyused protocol domain-socket1 domain-socket2\n");
0045         exit(-1);
0046     }
0047     // start the worker
0048     RecentlyUsed worker(argv[2], argv[3]);
0049     worker.dispatchLoop();
0050     return 0;
0051 }
0052 
0053 static bool isRootUrl(const QUrl &url)
0054 {
0055     const QString path = url.adjusted(QUrl::StripTrailingSlash).path();
0056     return path.isEmpty() || path == QLatin1String("/");
0057 }
0058 
0059 RecentlyUsed::RecentlyUsed(const QByteArray &pool, const QByteArray &app)
0060     : WorkerBase("recentlyused", pool, app)
0061 {
0062 }
0063 
0064 RecentlyUsed::~RecentlyUsed()
0065 {
0066 }
0067 
0068 int queryLimit(QUrl url)
0069 {
0070     const auto urlQuery = QUrlQuery(url);
0071     // limit parameter
0072     if (urlQuery.hasQueryItem(QStringLiteral("limit"))) {
0073         const auto limitValue = urlQuery.queryItemValue(QStringLiteral("limit"));
0074         bool parseOk;
0075         const auto limitInt = limitValue.toInt(&parseOk);
0076         if (parseOk) {
0077             return limitInt;
0078         }
0079     }
0080     return 30;
0081 }
0082 
0083 ResultModel *runQuery(const QUrl &url, int limit)
0084 {
0085     qCDebug(KIO_RECENTLYUSED_LOG) << "runQuery for url" << url.toString();
0086 
0087     auto query = UsedResources | Limit(30);
0088 
0089     // Parse url query parameter
0090     const auto urlQuery = QUrlQuery(url);
0091 
0092     const auto path = url.path();
0093     if (path.startsWith(QStringLiteral("/locations"))) {
0094         query.setTypes(Type::directories());
0095     } else {
0096         if (urlQuery.hasQueryItem(QStringLiteral("type"))) {
0097             // handles type parameter aka mimetype
0098             const auto typeValue = urlQuery.queryItemValue(QStringLiteral("type"));
0099             const auto types = typeValue.split(QLatin1Char(','));
0100             query.setTypes(types);
0101         } else if (path == QStringLiteral("/files")) {
0102             query.setTypes(Type::files());
0103         }
0104     }
0105 
0106     // activity parameter, filter using the uuid of the activity
0107     if (urlQuery.hasQueryItem(QStringLiteral("activity"))) {
0108         const auto activityValue = urlQuery.queryItemValue(QStringLiteral("activity"));
0109         if (activityValue == QStringLiteral("any")) {
0110             query.setActivities(Activity::any());
0111         } else {
0112             query.setActivities(activityValue);
0113         }
0114     } else {
0115         query.setActivities(Activity::current());
0116     }
0117 
0118     // date parameter, filter using the date when an event occurred on a resource
0119     if (urlQuery.hasQueryItem(QStringLiteral("date"))) {
0120         const auto dateValue = urlQuery.queryItemValue(QStringLiteral("date"));
0121         if (dateValue == QStringLiteral("today")) {
0122             query.setDate(Date::today());
0123         } else if (dateValue == QStringLiteral("yesterday")) {
0124             query.setDate(Date::yesterday());
0125         } else {
0126             query.setDate(Date::fromString(dateValue));
0127         }
0128     }
0129 
0130     // agent parameter, filter using the application name that used the resource
0131     if (urlQuery.hasQueryItem(QStringLiteral("agent"))) {
0132         const auto agentValue = urlQuery.queryItemValue(QStringLiteral("agent"));
0133         const auto agents = agentValue.split(QLatin1Char(','));
0134         query.setAgents(agents);
0135     } else {
0136         query.setAgents(Agent::any());
0137     }
0138 
0139     // path parameter for exact path match or folders, supports wildcard pattern matching
0140     if (urlQuery.hasQueryItem(QStringLiteral("path"))) {
0141         const auto pathValue = urlQuery.queryItemValue(QStringLiteral("path"));
0142         query.setUrlFilters(pathValue);
0143     } else {
0144         // only files are supported for now, because of limited support in udsEntryFromResource
0145         query.setUrlFilters(Url::file());
0146     }
0147 
0148     // see KActivities::Stats::Terms::Order
0149     if (urlQuery.hasQueryItem(QStringLiteral("order"))) {
0150         const auto orderValue = urlQuery.queryItemValue(QStringLiteral("order"));
0151 
0152         if (orderValue == QStringLiteral("HighScoredFirst")) {
0153             query.setOrdering(Order::HighScoredFirst);
0154         } else if (orderValue == QStringLiteral("RecentlyCreatedFirst")) {
0155             query.setOrdering(Order::RecentlyCreatedFirst);
0156         } else if (orderValue == QStringLiteral("OrderByUrl")) {
0157             query.setOrdering(Order::OrderByUrl);
0158         } else if (orderValue == QStringLiteral("OrderByTitle")) {
0159             query.setOrdering(Order::OrderByTitle);
0160         } else {
0161             query.setOrdering(Order::RecentlyUsedFirst);
0162         }
0163     } else {
0164         query.setOrdering(Order::RecentlyUsedFirst);
0165     }
0166 
0167     return new ResultModel(query);
0168 }
0169 
0170 KIO::UDSEntry RecentlyUsed::udsEntryFromResource(int row, const QString &resource, const QString &mimeType, const QString &agent, int lastUpdateTime)
0171 {
0172     qCDebug(KIO_RECENTLYUSED_LOG) << "udsEntryFromResource" << resource;
0173 
0174     // the query only returns files and folders
0175     QUrl resourceUrl = QUrl::fromUserInput(resource);
0176 
0177     KIO::UDSEntry uds;
0178     KIO::StatJob *job = KIO::stat(resourceUrl, KIO::HideProgressInfo);
0179 
0180     // we do not want to wait for the event loop to delete the job
0181     QScopedPointer<KIO::StatJob> sp(job);
0182     sp->setAutoDelete(false);
0183     if (sp->exec()) {
0184         uds = sp->statResult();
0185     } else {
0186         // not found / not existing anymore
0187         return uds;
0188     }
0189     // replace name with a technical unique name
0190     const auto name = uds.stringValue(KIO::UDSEntry::UDS_NAME);
0191     uds.replace(KIO::UDSEntry::UDS_NAME, QStringLiteral("%1-%2").arg(name).arg(row));
0192     uds.reserve(uds.count() + 5);
0193     if (name.isEmpty()) {
0194         uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, resource);
0195     } else {
0196         uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, name);
0197     }
0198     uds.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, mimeType);
0199     uds.fastInsert(KIO::UDSEntry::UDS_TARGET_URL, resourceUrl.toString());
0200     if (resourceUrl.isLocalFile()) {
0201         uds.fastInsert(KIO::UDSEntry::UDS_LOCAL_PATH, resource);
0202     }
0203     if (!uds.contains(KIO::UDSEntry::UDS_ACCESS_TIME)) {
0204         // default access time
0205         uds.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, lastUpdateTime);
0206     }
0207     uds.fastInsert(KIO::UDSEntry::UDS_EXTRA, agent);
0208     return uds;
0209 }
0210 
0211 KIO::WorkerResult RecentlyUsed::listDir(const QUrl &url)
0212 {
0213     // / /files and /locations
0214     const auto path = url.path();
0215     if (path == QStringLiteral("/") || path == QStringLiteral("/files") || path == QStringLiteral("/locations")) {
0216         KIO::UDSEntryList udslist;
0217 
0218         // add "." to transmit permissions for current directory
0219         KIO::UDSEntry uds;
0220         uds.reserve(4);
0221         uds.fastInsert(KIO::UDSEntry::UDS_NAME, QStringLiteral("."));
0222         uds.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
0223         uds.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
0224 #ifdef Q_OS_WIN
0225         uds.fastInsert(KIO::UDSEntry::UDS_ACCESS, _S_IREAD);
0226 #else
0227         uds.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR);
0228 #endif
0229         udslist << uds;
0230 
0231         int limit = queryLimit(url);
0232         // query twice the limit size to be able to pass not existing files
0233         const auto model = runQuery(url, limit * 2);
0234 
0235         bool canFetchMore = true;
0236         int row = 0;
0237 
0238         while (canFetchMore) {
0239             for (; udslist.count() != limit + 1 && row < model->rowCount(); ++row) {
0240                 const QModelIndex index = model->index(row, 0);
0241                 const QString resource = model->data(index, ResultModel::ResourceRole).toString();
0242                 const QString mimeType = model->data(index, ResultModel::MimeType).toString();
0243                 const int lastUpdate = model->data(index, ResultModel::LastUpdateRole).toInt();
0244                 const QString agent = model->data(index, ResultModel::Agent).toString();
0245 
0246                 const auto entry = udsEntryFromResource(row, resource, mimeType, agent, lastUpdate);
0247                 if (entry.count() > 0) {
0248                     udslist << entry;
0249                 }
0250             }
0251             canFetchMore = udslist.count() != limit + 1 && model->canFetchMore(QModelIndex());
0252             if (canFetchMore) {
0253                 model->fetchMore(QModelIndex());
0254             }
0255         }
0256 
0257         listEntries(udslist);
0258 
0259         return KIO::WorkerResult::pass();
0260     }
0261 
0262     // subdirs
0263 
0264     // parse the technical id: filename-id, id being an index in the model
0265     const auto splitted = QStringView(url.fileName()).split(QLatin1Char('-'));
0266     if (splitted.count() < 2) {
0267         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.toDisplayString());
0268     }
0269     bool ok;
0270     int id = splitted.last().toInt(&ok);
0271     if (!ok) {
0272         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.toDisplayString());
0273     }
0274 
0275     // query twice the limit size to be able to pass not existing files
0276     const auto model = runQuery(url, queryLimit(url) * 2);
0277     const auto index = model->index(id, 0);
0278 
0279     if (!index.isValid()) {
0280         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.toDisplayString());
0281     }
0282 
0283     const QString resource = model->data(index, ResultModel::ResourceRole).toString();
0284     qCDebug(KIO_RECENTLYUSED_LOG) << "redirection to " << resource << url;
0285     redirection(QUrl::fromUserInput(resource));
0286     return KIO::WorkerResult::pass();
0287 }
0288 
0289 KIO::UDSEntry RecentlyUsed::udsEntryForRoot(const QString &dirName, const QString &iconName)
0290 {
0291     KIO::UDSEntry uds;
0292     uds.reserve(7);
0293     uds.fastInsert(KIO::UDSEntry::UDS_NAME, dirName);
0294     uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, dirName);
0295     uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_TYPE, dirName);
0296     uds.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, iconName);
0297     uds.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR);
0298     uds.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory"));
0299 #ifdef Q_OS_WIN
0300     uds.fastInsert(KIO::UDSEntry::UDS_ACCESS, _S_IREAD);
0301 #else
0302     uds.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IXUSR);
0303 #endif
0304     return uds;
0305 }
0306 
0307 KIO::WorkerResult RecentlyUsed::stat(const QUrl &url)
0308 {
0309     qCDebug(KIO_RECENTLYUSED_LOG) << "stating"
0310                                   << " " << url;
0311 
0312     if (isRootUrl(url)) {
0313         //
0314         // stat the root path
0315         //
0316 
0317         const QString dirName = i18n("Recent Documents");
0318 
0319         statEntry(udsEntryForRoot(dirName, QStringLiteral("document-open-recent")));
0320         return KIO::WorkerResult::pass();
0321     }
0322 
0323     const auto path = url.path();
0324     if (path == QStringLiteral("/files")) {
0325         const QString dirName = i18n("Recent Files");
0326         statEntry(udsEntryForRoot(dirName, QStringLiteral("document-open-recent")));
0327     } else if (path == QStringLiteral("/locations")) {
0328         const QString dirName = i18n("Recent Locations");
0329         statEntry(udsEntryForRoot(dirName, QStringLiteral("folder-open-recent")));
0330     } else {
0331         // only / /files and /locations paths are supported
0332         return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.toDisplayString());
0333     }
0334     return KIO::WorkerResult::pass();
0335 }
0336 
0337 KIO::WorkerResult RecentlyUsed::mimetype(const QUrl &url)
0338 {
0339     // the root url is always a folder
0340     if (isRootUrl(url)) {
0341         mimeType(QStringLiteral("inode/directory"));
0342         return KIO::WorkerResult::pass();
0343     }
0344 
0345     // only the root path is supported
0346     return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, url.toDisplayString());
0347 }
0348 
0349 KIO::WorkerResult RecentlyUsed::special(const QByteArray &data)
0350 {
0351     int id;
0352     QDataStream stream(data);
0353     stream >> id;
0354 
0355     switch (id) {
0356     case 1: { // Forget
0357         QList<QUrl> urls;
0358         stream >> urls;
0359 
0360         QList<QString> paths;
0361         for (const auto &url : qAsConst(urls)) {
0362             if (url.isLocalFile() || url.scheme().isEmpty()) {
0363                 paths.append(url.path());
0364             } else {
0365                 paths.append(url.toString());
0366             }
0367         }
0368 
0369         Query query = UsedResources | Limit(paths.size());
0370         query.setUrlFilters(Url(paths));
0371         query.setAgents(Agent::any());
0372         query.setActivities(Activity::any());
0373 
0374         ResultModel model(query);
0375         model.forgetResources(paths);
0376 
0377         break;
0378     }
0379     default:
0380         break;
0381     }
0382 
0383     return KIO::WorkerResult::pass();
0384 }
0385 
0386 // needed for JSON file embedding
0387 #include "recentlyused.moc"