File indexing completed on 2025-01-26 05:00:55

0001 /*
0002  *   SPDX-FileCopyrightText: 2019 Méven Car (meven.car@kdemail.net)
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "RecentlyUsedEventSpy.h"
0008 
0009 #include <QFile>
0010 #include <QStandardPaths>
0011 #include <QString>
0012 #include <QUrl>
0013 #include <QXmlStreamReader>
0014 
0015 #include <KApplicationTrader>
0016 #include <KDirWatch>
0017 
0018 #include "DebugPluginRecentlyUsedEventSpy.h"
0019 
0020 K_PLUGIN_CLASS(RecentlyUsedEventSpyPlugin)
0021 
0022 RecentlyUsedEventSpyPlugin::RecentlyUsedEventSpyPlugin(QObject *parent)
0023     : Plugin(parent)
0024     , m_resources(nullptr)
0025     , m_dirWatcher(new KDirWatch(this))
0026     , m_lastUpdate(QDateTime::currentDateTime())
0027 {
0028     // recently-used xml history file
0029     // usually $HOME/.local/share/recently-used.xbel
0030     QString filename = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/recently-used.xbel");
0031 
0032     m_dirWatcher->addFile(filename);
0033     connect(m_dirWatcher.get(), &KDirWatch::dirty, this, &RecentlyUsedEventSpyPlugin::fileUpdated);
0034     connect(m_dirWatcher.get(), &KDirWatch::created, this, &RecentlyUsedEventSpyPlugin::fileUpdated);
0035 }
0036 
0037 struct Application {
0038     QString name;
0039     QDateTime modified;
0040 };
0041 
0042 class Bookmark
0043 {
0044 public:
0045     QUrl href;
0046     QDateTime added;
0047     QDateTime modified;
0048     QDateTime visited;
0049     QString mimetype;
0050     QList<Application> applications;
0051 
0052     QString latestApplication() const;
0053 };
0054 
0055 QString Bookmark::latestApplication() const
0056 {
0057     Application current = applications.first();
0058     for (const Application &app : applications) {
0059         if (app.modified > current.modified) {
0060             current = app;
0061         }
0062     }
0063     return current.name;
0064 }
0065 
0066 void RecentlyUsedEventSpyPlugin::fileUpdated(const QString &filename)
0067 {
0068     QFile file(filename);
0069     if (!file.open(QFile::ReadOnly | QFile::Text)) {
0070         qCWarning(KAMD_LOG_PLUGIN_RECENTLYUSED_EVENTSPY) << "Could not read" << filename;
0071         return;
0072     }
0073 
0074     // must parse the xbel xml file
0075     QXmlStreamReader reader(&file);
0076     QList<Bookmark> bookmarks;
0077     Bookmark current;
0078     while (!reader.atEnd()) {
0079         const auto token = reader.readNext();
0080         if (token == QXmlStreamReader::StartElement) {
0081             if (reader.qualifiedName() == QLatin1String("bookmark")) {
0082                 current = Bookmark();
0083                 current.href = QUrl(reader.attributes().value("href").toString());
0084                 QString added = reader.attributes().value("added").toString();
0085                 QString modified = reader.attributes().value("modified").toString();
0086                 QString visited = reader.attributes().value("visited").toString();
0087                 current.added = QDateTime::fromString(added, Qt::ISODate);
0088                 current.modified = QDateTime::fromString(modified, Qt::ISODate);
0089                 current.visited = QDateTime::fromString(visited, Qt::ISODate);
0090 
0091                 // application for the current bookmark
0092             } else if (reader.qualifiedName() == QLatin1String("bookmark:application")) {
0093                 Application app;
0094 
0095                 QString exec = reader.attributes().value("exec").toString();
0096 
0097                 if (exec.startsWith(QLatin1Char('\'')) && exec.endsWith(QLatin1Char('\''))) {
0098                     // remove "'" characters wrapping the command
0099                     exec = exec.mid(1, exec.size() - 2);
0100                 }
0101 
0102                 // Search for applications which are executable and case-insensitively match the search term
0103                 const KService::List services = KApplicationTrader::query([exec](const KService::Ptr &app) {
0104                     return app->exec().compare(exec, Qt::CaseInsensitive) == 0;
0105                 });
0106 
0107                 if (!services.isEmpty()) {
0108                     // use the first item matching
0109                     const auto &service = services.first();
0110                     app.name = service->desktopEntryName();
0111                 } else {
0112                     // when no services are found, sanitize a little the exec
0113                     // remove space and any character after
0114                     const int spaceIndex = exec.indexOf(" ");
0115                     if (spaceIndex != -1) {
0116                         exec = exec.mid(0, spaceIndex);
0117                     }
0118                     app.name = exec;
0119                 }
0120 
0121                 app.modified = QDateTime::fromString(reader.attributes().value("modified").toString(), Qt::ISODate);
0122 
0123                 current.applications.append(app);
0124             } else if (reader.qualifiedName() == QLatin1String("mime:mime-type")) {
0125                 current.mimetype = reader.attributes().value("type").toString();
0126             }
0127         } else if (token == QXmlStreamReader::EndElement) {
0128             if (reader.qualifiedName() == QLatin1String("bookmark")) {
0129                 // keep track of the finished parsed bookmark
0130                 bookmarks << current;
0131             }
0132         }
0133     }
0134 
0135     if (reader.hasError()) {
0136         qCWarning(KAMD_LOG_PLUGIN_RECENTLYUSED_EVENTSPY) << "could not parse" << file.fileName() << "error was " << reader.errorString();
0137         return;
0138     }
0139 
0140     // then find the files that were accessed since last run
0141     for (const Bookmark &mark : bookmarks) {
0142         if (mark.added > m_lastUpdate || mark.modified > m_lastUpdate || mark.visited > m_lastUpdate) {
0143             addDocument(mark.href, mark.latestApplication(), mark.mimetype);
0144         }
0145     }
0146 
0147     m_lastUpdate = QDateTime::currentDateTime();
0148 }
0149 
0150 void RecentlyUsedEventSpyPlugin::addDocument(const QUrl &url, const QString &application, const QString &mimetype)
0151 {
0152     const auto urlString = url.toString(QUrl::PreferLocalFile);
0153     Plugin::invoke<Qt::QueuedConnection>(m_resources,
0154                                          "RegisterResourceEvent",
0155                                          Q_ARG(QString, application), // Application
0156                                          Q_ARG(uint, 0), // Window ID
0157                                          Q_ARG(QString, urlString), // URI
0158                                          Q_ARG(uint, 0) // Event Activities::Accessed
0159     );
0160 
0161     Plugin::invoke<Qt::QueuedConnection>(m_resources,
0162                                          "RegisteredResourceMimetype",
0163                                          Q_ARG(QString, urlString), // uri
0164                                          Q_ARG(QString, mimetype) // mimetype
0165     );
0166 
0167     Plugin::invoke<Qt::QueuedConnection>(m_resources,
0168                                          "RegisterResourceTitle",
0169                                          Q_ARG(QString, urlString), // uri
0170                                          Q_ARG(QString, url.fileName()) // title
0171     );
0172 }
0173 
0174 RecentlyUsedEventSpyPlugin::~RecentlyUsedEventSpyPlugin()
0175 {
0176 }
0177 
0178 bool RecentlyUsedEventSpyPlugin::init(QHash<QString, QObject *> &modules)
0179 {
0180     Plugin::init(modules);
0181 
0182     m_resources = modules["resources"];
0183 
0184     return true;
0185 }
0186 
0187 #include "RecentlyUsedEventSpy.moc"
0188 
0189 #include "moc_RecentlyUsedEventSpy.cpp"