File indexing completed on 2025-02-09 05:52:28
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"