File indexing completed on 2024-04-28 11:44:19

0001 /*
0002     SPDX-FileCopyrightText: 2006-2007 Aaron Seigo <aseigo@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "runnercontext.h"
0008 
0009 #include <cmath>
0010 
0011 #include <QDir>
0012 #include <QFile>
0013 #include <QFileInfo>
0014 #include <QMimeDatabase>
0015 #include <QReadWriteLock>
0016 #include <QRegularExpression>
0017 #include <QSharedData>
0018 #include <QStandardPaths>
0019 #include <QUrl>
0020 
0021 #include <KConfigGroup>
0022 #include <KShell>
0023 
0024 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
0025 #include <KProtocolInfo>
0026 #endif
0027 
0028 #include "abstractrunner.h"
0029 #include "krunner_debug.h"
0030 #include "querymatch.h"
0031 
0032 #define LOCK_FOR_READ(d) d->lock.lockForRead();
0033 #define LOCK_FOR_WRITE(d) d->lock.lockForWrite();
0034 #define UNLOCK(d) d->lock.unlock();
0035 
0036 namespace Plasma
0037 {
0038 class RunnerContextPrivate : public QSharedData
0039 {
0040 public:
0041     explicit RunnerContextPrivate(RunnerContext *context)
0042         : QSharedData()
0043         , type(RunnerContext::UnknownType)
0044         , q(context)
0045     {
0046     }
0047 
0048     explicit RunnerContextPrivate(const RunnerContextPrivate &p)
0049         : QSharedData()
0050         , launchCounts(p.launchCounts)
0051         , type(RunnerContext::None)
0052         , q(p.q)
0053     {
0054     }
0055 
0056     ~RunnerContextPrivate()
0057     {
0058     }
0059 
0060 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
0061     /**
0062      * Determines type of query
0063                 &&
0064      */
0065     void determineType()
0066     {
0067         // NOTE! this method must NEVER be called from
0068         // code that may be running in multiple threads
0069         // with the same data.
0070         type = RunnerContext::UnknownType;
0071         QString path = QDir::cleanPath(KShell::tildeExpand(term));
0072 
0073         int space = path.indexOf(QLatin1Char(' '));
0074         if (!QStandardPaths::findExecutable(path.left(space)).isEmpty()) {
0075             // it's a shell command if there's a space because that implies
0076             // that it has arguments!
0077             type = (space > 0) ? RunnerContext::ShellCommand : RunnerContext::Executable;
0078         } else {
0079             QUrl url = QUrl::fromUserInput(term);
0080 
0081             // QUrl::fromUserInput assigns http to everything if it cannot match it to
0082             // anything else. We do not want that.
0083             if (url.scheme() == QLatin1String("http")) {
0084                 if (!term.startsWith(QLatin1String("http"))) {
0085                     url.setScheme(QString());
0086                 }
0087             }
0088 
0089             const bool hasProtocol = !url.scheme().isEmpty();
0090             const bool isLocalProtocol = !hasProtocol || KProtocolInfo::protocolClass(url.scheme()) == QLatin1String(":local");
0091             if ((hasProtocol && ((!isLocalProtocol && !url.host().isEmpty()) || (isLocalProtocol && url.scheme() != QLatin1String("file"))))
0092                 || term.startsWith(QLatin1String("\\\\"))) {
0093                 // we either have a network protocol with a host, so we can show matches for it
0094                 // or we have a non-file url that may be local so a host isn't required
0095                 // or we have an UNC path (\\foo\bar)
0096                 type = RunnerContext::NetworkLocation;
0097             } else if (isLocalProtocol) {
0098                 // at this point in the game, we assume we have a path,
0099                 // but if a path doesn't have any slashes
0100                 // it's too ambiguous to be sure we're in a filesystem context
0101                 path = !url.scheme().isEmpty() ? QDir::cleanPath(url.toLocalFile()) : path;
0102                 if ((path.indexOf(QLatin1Char('/')) != -1 || path.indexOf(QLatin1Char('\\')) != -1)) {
0103                     QFileInfo info(path);
0104                     if (info.isSymLink()) {
0105                         path = info.canonicalFilePath();
0106                         info = QFileInfo(path);
0107                     }
0108                     if (info.isDir()) {
0109                         type = RunnerContext::Directory;
0110                         mimeType = QStringLiteral("inode/folder");
0111                     } else if (info.isFile()) {
0112                         type = RunnerContext::File;
0113                         QMimeDatabase db;
0114                         QMimeType mime = db.mimeTypeForFile(path);
0115                         if (!mime.isDefault()) {
0116                             mimeType = mime.name();
0117                         }
0118                     }
0119                 }
0120             }
0121         }
0122     }
0123 #endif
0124 
0125     void invalidate()
0126     {
0127         q = &s_dummyContext;
0128     }
0129 
0130 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 81)
0131     QueryMatch *findMatchById(const QString &id)
0132     {
0133         QueryMatch *match = nullptr;
0134         lock.lockForRead();
0135         for (auto &matchEntry : matches) {
0136             if (matchEntry.id() == id) {
0137                 match = &matchEntry;
0138                 break;
0139             }
0140         }
0141         lock.unlock();
0142         return match;
0143     }
0144 #endif
0145 
0146     void addMatch(const QueryMatch &match)
0147     {
0148         if (match.runner() && match.runner()->hasUniqueResults()) {
0149             if (uniqueIds.contains(match.id())) {
0150                 const QueryMatch &existentMatch = uniqueIds.value(match.id());
0151                 if (existentMatch.runner() && existentMatch.runner()->hasWeakResults()) {
0152                     // There is an existing match with the same ID and we are allowed to replace it
0153                     matches.removeOne(existentMatch);
0154                     matches.append(match);
0155                 }
0156             } else {
0157                 // There is no existing match with the same id
0158                 uniqueIds.insert(match.id(), match);
0159                 matches.append(match);
0160             }
0161         } else {
0162             // Runner has the unique results property not set
0163             matches.append(match);
0164         }
0165     }
0166 
0167     QReadWriteLock lock;
0168     QList<QueryMatch> matches;
0169     QHash<QString, int> launchCounts;
0170     QString term;
0171     QString mimeType;
0172     QStringList enabledCategories;
0173     RunnerContext::Type type;
0174     RunnerContext *q;
0175     static RunnerContext s_dummyContext;
0176     bool singleRunnerQueryMode = false;
0177     bool shouldIgnoreCurrentMatchForHistory = false;
0178     QMap<QString, QueryMatch> uniqueIds;
0179     QString requestedText;
0180     int requestedCursorPosition = 0;
0181 };
0182 
0183 RunnerContext RunnerContextPrivate::s_dummyContext;
0184 
0185 RunnerContext::RunnerContext(QObject *parent)
0186     : QObject(parent)
0187     , d(new RunnerContextPrivate(this))
0188 {
0189 }
0190 
0191 // copy ctor
0192 RunnerContext::RunnerContext(RunnerContext &other, QObject *parent)
0193     : QObject(parent)
0194 {
0195     LOCK_FOR_READ(other.d)
0196     d = other.d;
0197     UNLOCK(other.d)
0198 }
0199 
0200 RunnerContext::~RunnerContext()
0201 {
0202 }
0203 
0204 RunnerContext &RunnerContext::operator=(const RunnerContext &other)
0205 {
0206     if (this->d == other.d) {
0207         return *this;
0208     }
0209 
0210     QExplicitlySharedDataPointer<Plasma::RunnerContextPrivate> oldD = d;
0211     LOCK_FOR_WRITE(d)
0212     LOCK_FOR_READ(other.d)
0213     d = other.d;
0214     UNLOCK(other.d)
0215     UNLOCK(oldD)
0216     return *this;
0217 }
0218 
0219 void RunnerContext::reset()
0220 {
0221     LOCK_FOR_WRITE(d);
0222     // We will detach if we are a copy of someone. But we will reset
0223     // if we are the 'main' context others copied from. Resetting
0224     // one RunnerContext makes all the copies obsolete.
0225 
0226     // We need to mark the q pointer of the detached RunnerContextPrivate
0227     // as dirty on detach to avoid receiving results for old queries
0228     d->invalidate();
0229     UNLOCK(d);
0230 
0231     d.detach();
0232 
0233     // Now that we detached the d pointer we need to reset its q pointer
0234 
0235     d->q = this;
0236 
0237     // we still have to remove all the matches, since if the
0238     // ref count was 1 (e.g. only the RunnerContext is using
0239     // the dptr) then we won't get a copy made
0240     d->matches.clear();
0241     d->term.clear();
0242     Q_EMIT matchesChanged();
0243 
0244     d->mimeType.clear();
0245     d->uniqueIds.clear();
0246     d->type = UnknownType;
0247     d->singleRunnerQueryMode = false;
0248     d->shouldIgnoreCurrentMatchForHistory = false;
0249 }
0250 
0251 void RunnerContext::setQuery(const QString &term)
0252 {
0253     if (!this->query().isEmpty()) {
0254         reset();
0255     }
0256 
0257     if (term.isEmpty()) {
0258         return;
0259     }
0260 
0261     d->requestedText.clear(); // Invalidate this field whenever the query changes
0262     d->term = term;
0263 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
0264     d->determineType();
0265 #endif
0266 }
0267 
0268 QString RunnerContext::query() const
0269 {
0270     // the query term should never be set after
0271     // a search starts. in fact, reset() ensures this
0272     // and setQuery(QString) calls reset()
0273     return d->term;
0274 }
0275 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
0276 
0277 void RunnerContext::setEnabledCategories(const QStringList &categories)
0278 {
0279     d->enabledCategories = categories;
0280 }
0281 #endif
0282 
0283 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
0284 QStringList RunnerContext::enabledCategories() const
0285 {
0286     return d->enabledCategories;
0287 }
0288 #endif
0289 
0290 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
0291 RunnerContext::Type RunnerContext::type() const
0292 {
0293     return d->type;
0294 }
0295 #endif
0296 
0297 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
0298 QString RunnerContext::mimeType() const
0299 {
0300     return d->mimeType;
0301 }
0302 #endif
0303 
0304 bool RunnerContext::isValid() const
0305 {
0306     // if our qptr is dirty, we aren't useful anymore
0307     LOCK_FOR_READ(d)
0308     const bool valid = (d->q != &(d->s_dummyContext));
0309     UNLOCK(d)
0310     return valid;
0311 }
0312 
0313 bool RunnerContext::addMatches(const QList<QueryMatch> &matches)
0314 {
0315     if (matches.isEmpty() || !isValid()) {
0316         // Bail out if the query is empty or the qptr is dirty
0317         return false;
0318     }
0319 
0320     LOCK_FOR_WRITE(d)
0321     for (QueryMatch match : matches) {
0322         // Give previously launched matches a slight boost in relevance
0323         // The boost smoothly saturates to 0.5;
0324         if (int count = d->launchCounts.value(match.id())) {
0325             match.setRelevance(match.relevance() + 0.5 * (1 - exp(-count * 0.3)));
0326         }
0327         d->addMatch(match);
0328     }
0329     UNLOCK(d);
0330     // A copied searchContext may share the d pointer,
0331     // we always want to sent the signal of the object that created
0332     // the d pointer
0333     Q_EMIT d->q->matchesChanged();
0334 
0335     return true;
0336 }
0337 
0338 bool RunnerContext::addMatch(const QueryMatch &match)
0339 {
0340     return addMatches({match});
0341 }
0342 
0343 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 81)
0344 bool RunnerContext::removeMatches(const QStringList matchIdList)
0345 {
0346     bool success = false;
0347     for (const QString &id : matchIdList) {
0348         if (removeMatch(id)) {
0349             success = true;
0350         }
0351     }
0352 
0353     return success;
0354 }
0355 #endif
0356 
0357 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 81)
0358 bool RunnerContext::removeMatch(const QString matchId)
0359 {
0360     if (!isValid()) {
0361         return false;
0362     }
0363     LOCK_FOR_READ(d)
0364     const QueryMatch *match = d->findMatchById(matchId);
0365     UNLOCK(d)
0366     if (!match) {
0367         return false;
0368     }
0369     LOCK_FOR_WRITE(d)
0370     d->matches.removeAll(*match);
0371     UNLOCK(d)
0372     Q_EMIT d->q->matchesChanged();
0373 
0374     return true;
0375 }
0376 #endif
0377 
0378 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 81)
0379 bool RunnerContext::removeMatches(Plasma::AbstractRunner *runner)
0380 {
0381     if (!isValid()) {
0382         return false;
0383     }
0384 
0385     QList<QueryMatch> presentMatchList;
0386 
0387     LOCK_FOR_READ(d)
0388     for (const QueryMatch &match : std::as_const(d->matches)) {
0389         if (match.runner() == runner) {
0390             presentMatchList << match;
0391         }
0392     }
0393     UNLOCK(d)
0394 
0395     if (presentMatchList.isEmpty()) {
0396         return false;
0397     }
0398 
0399     LOCK_FOR_WRITE(d)
0400     for (const QueryMatch &match : std::as_const(presentMatchList)) {
0401         d->matches.removeAll(match);
0402     }
0403     UNLOCK(d)
0404 
0405     Q_EMIT d->q->matchesChanged();
0406     return true;
0407 }
0408 #endif
0409 
0410 QList<QueryMatch> RunnerContext::matches() const
0411 {
0412     LOCK_FOR_READ(d)
0413     QList<QueryMatch> matches = d->matches;
0414     UNLOCK(d);
0415     return matches;
0416 }
0417 
0418 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 79)
0419 QueryMatch RunnerContext::match(const QString &id) const
0420 {
0421     LOCK_FOR_READ(d)
0422     const QueryMatch *match = d->findMatchById(id);
0423     UNLOCK(d)
0424     return match ? *match : QueryMatch(nullptr);
0425 }
0426 #endif
0427 
0428 void RunnerContext::requestQueryStringUpdate(const QString &text, int cursorPosition) const
0429 {
0430     d->requestedText = text;
0431     d->requestedCursorPosition = cursorPosition;
0432 }
0433 
0434 void RunnerContext::setSingleRunnerQueryMode(bool enabled)
0435 {
0436     d->singleRunnerQueryMode = enabled;
0437 }
0438 
0439 bool RunnerContext::singleRunnerQueryMode() const
0440 {
0441     return d->singleRunnerQueryMode;
0442 }
0443 
0444 void RunnerContext::ignoreCurrentMatchForHistory() const
0445 {
0446     d->shouldIgnoreCurrentMatchForHistory = true;
0447 }
0448 
0449 bool RunnerContext::shouldIgnoreCurrentMatchForHistory() const
0450 {
0451     return d->shouldIgnoreCurrentMatchForHistory;
0452 }
0453 
0454 void RunnerContext::restore(const KConfigGroup &config)
0455 {
0456     const QStringList cfgList = config.readEntry("LaunchCounts", QStringList());
0457 
0458     static const QRegularExpression re(QStringLiteral("(\\d*) (.+)"));
0459     for (const QString &entry : cfgList) {
0460         const QRegularExpressionMatch match = re.match(entry);
0461         if (!match.hasMatch()) {
0462             continue;
0463         }
0464         const int count = match.captured(1).toInt();
0465         const QString id = match.captured(2);
0466         d->launchCounts[id] = count;
0467     }
0468 }
0469 
0470 void RunnerContext::save(KConfigGroup &config)
0471 {
0472     QStringList countList;
0473 
0474     typedef QHash<QString, int>::const_iterator Iterator;
0475     Iterator end = d->launchCounts.constEnd();
0476     for (Iterator i = d->launchCounts.constBegin(); i != end; ++i) {
0477         countList << QStringLiteral("%2 %1").arg(i.key()).arg(i.value());
0478     }
0479 
0480     config.writeEntry("LaunchCounts", countList);
0481     config.sync();
0482 }
0483 
0484 void RunnerContext::run(const QueryMatch &match)
0485 {
0486     ++d->launchCounts[match.id()];
0487     match.run(*this);
0488 }
0489 
0490 QString RunnerContext::requestedQueryString() const
0491 {
0492     return d->requestedText;
0493 }
0494 int RunnerContext::requestedCursorPosition() const
0495 {
0496     return d->requestedCursorPosition;
0497 }
0498 
0499 } // Plasma namespace
0500 
0501 #include "moc_runnercontext.cpp"