File indexing completed on 2024-05-12 05:38:21

0001 /*
0002     SPDX-FileCopyrightText: 2007 Teemu Rytilahti <tpr@iki.fi>
0003     SPDX-FileCopyrightText: 2023 Harald Sitter <sitter@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only
0006 */
0007 
0008 #include "webshortcutrunner.h"
0009 
0010 #include <KApplicationTrader>
0011 #include <KConfigGroup>
0012 #include <KIO/CommandLauncherJob>
0013 #include <KIO/OpenUrlJob>
0014 #include <KLocalizedString>
0015 #include <KRunner/RunnerManager>
0016 #include <KSharedConfig>
0017 #include <KShell>
0018 #include <KSycoca>
0019 #include <KUriFilter>
0020 #include <QDBusConnection>
0021 #include <QIcon>
0022 #include <defaultservice.h>
0023 
0024 using namespace Qt::StringLiterals;
0025 
0026 WebshortcutRunner::WebshortcutRunner(QObject *parent, const KPluginMetaData &metaData)
0027     : KRunner::AbstractRunner(parent, metaData)
0028     , m_match(this)
0029     , m_filterBeforeRun(false)
0030 {
0031     m_match.setCategoryRelevance(KRunner::QueryMatch::CategoryRelevance::Highest);
0032     m_match.setRelevance(0.9);
0033 
0034     // Listen for KUriFilter plugin config changes and update state...
0035     QDBusConnection sessionDbus = QDBusConnection::sessionBus();
0036     sessionDbus.connect(QString(), QStringLiteral("/"), QStringLiteral("org.kde.KUriFilterPlugin"), QStringLiteral("configure"), this, SLOT(loadSyntaxes()));
0037     connect(KSycoca::self(), &KSycoca::databaseChanged, this, &WebshortcutRunner::configurePrivateBrowsingActions);
0038     setMinLetterCount(3);
0039 
0040     connect(qobject_cast<KRunner::RunnerManager *>(parent), &KRunner::RunnerManager::queryFinished, this, [this]() {
0041         if (m_lastUsedContext.isValid() && !m_defaultKey.isEmpty() && m_lastUsedContext.matches().isEmpty()) {
0042             const QString queryWithDefaultProvider = m_defaultKey + m_delimiter + m_lastUsedContext.query();
0043             KUriFilterData filterData(queryWithDefaultProvider);
0044             if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::WebShortcutFilter)) {
0045                 m_match.setText(i18n("Search %1 for %2", filterData.searchProvider(), filterData.searchTerm()));
0046                 m_match.setData(filterData.uri());
0047                 m_match.setIconName(filterData.iconName());
0048                 m_lastUsedContext.addMatch(m_match);
0049             }
0050         }
0051     });
0052 }
0053 
0054 void WebshortcutRunner::loadSyntaxes()
0055 {
0056     KUriFilterData filterData(QStringLiteral(":q"));
0057     filterData.setSearchFilteringOptions(KUriFilterData::RetrieveAvailableSearchProvidersOnly);
0058     if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::NormalTextFilter)) {
0059         m_delimiter = filterData.searchTermSeparator();
0060     }
0061     m_regex = QRegularExpression(QStringLiteral("^([^ ]+)%1").arg(QRegularExpression::escape(m_delimiter)));
0062 
0063     QList<KRunner::RunnerSyntax> syns;
0064     const QStringList providers = filterData.preferredSearchProviders();
0065     const static QRegularExpression replaceRegex(QStringLiteral(":q$"));
0066     const QString placeholder = QStringLiteral(":q:");
0067     for (const QString &provider : providers) {
0068         KRunner::RunnerSyntax s(filterData.queryForPreferredSearchProvider(provider).replace(replaceRegex, placeholder),
0069                                 i18n("Opens \"%1\" in a web browser with the query :q:.", provider));
0070         syns << s;
0071     }
0072     if (!providers.isEmpty()) {
0073         QString defaultKey = filterData.queryForSearchProvider(providers.constFirst()).defaultKey();
0074         KRunner::RunnerSyntax s(QStringLiteral("!%1 :q:").arg(defaultKey), i18n("Search using the DuckDuckGo bang syntax"));
0075         syns << s;
0076     }
0077 
0078     setSyntaxes(syns);
0079     m_lastFailedKey.clear();
0080     m_lastProvider.clear();
0081     m_lastKey.clear();
0082 
0083     // When we reload the syntaxes, our WebShortcut config has changed or is initialized
0084     const KConfigGroup grp = KSharedConfig::openConfig(u"kuriikwsfilterrc"_s)->group(u"General"_s);
0085     m_defaultKey = grp.readEntry("DefaultWebShortcut", QStringLiteral("duckduckgo"));
0086 }
0087 
0088 void WebshortcutRunner::configurePrivateBrowsingActions()
0089 {
0090     m_match.setActions({});
0091 
0092     auto service = DefaultService::browser();
0093     if (!service) {
0094         return;
0095     }
0096     const auto actions = service->actions();
0097     for (const auto &action : actions) {
0098         bool containsPrivate = action.text().contains(QLatin1String("private"), Qt::CaseInsensitive);
0099         bool containsIncognito = action.text().contains(QLatin1String("incognito"), Qt::CaseInsensitive);
0100         if (containsPrivate || containsIncognito) {
0101             m_privateAction = action;
0102             const QString text = containsPrivate ? i18n("Search in private window") : i18n("Search in incognito window");
0103             const QIcon icon = QIcon::fromTheme(QStringLiteral("view-private"), QIcon::fromTheme(QStringLiteral("view-hidden")));
0104             m_match.setActions({KRunner::Action(action.exec(), icon.name(), text)});
0105             return;
0106         }
0107     }
0108 }
0109 
0110 void WebshortcutRunner::match(KRunner::RunnerContext &context)
0111 {
0112     m_lastUsedContext = context;
0113     const QString term = context.query();
0114     const static QRegularExpression bangRegex(QStringLiteral("!([^ ]+).*"));
0115     const auto bangMatch = bangRegex.match(term);
0116     QString key;
0117     QString rawQuery = term;
0118 
0119     if (bangMatch.hasMatch()) {
0120         key = bangMatch.captured(1);
0121         rawQuery = rawQuery.remove(rawQuery.indexOf(key) - 1, key.size() + 1);
0122     } else {
0123         const auto normalMatch = m_regex.match(term);
0124         if (normalMatch.hasMatch()) {
0125             key = normalMatch.captured(0);
0126             rawQuery = rawQuery.mid(key.length());
0127         }
0128     }
0129     if (key.isEmpty() || key == m_lastFailedKey) {
0130         return; // we already know it's going to suck ;)
0131     }
0132 
0133     // Do a fake user feedback text update if the keyword has not changed.
0134     // There is no point filtering the request on every key stroke.
0135     // filtering
0136     if (m_lastKey == key) {
0137         m_filterBeforeRun = true;
0138         m_match.setText(i18n("Search %1 for %2", m_lastProvider, rawQuery));
0139         context.addMatch(m_match);
0140         return;
0141     }
0142 
0143     KUriFilterData filterData(term);
0144     if (!KUriFilter::self()->filterSearchUri(filterData, KUriFilter::WebShortcutFilter)) {
0145         m_lastFailedKey = key;
0146         return;
0147     }
0148 
0149     // Reuse key/provider for next matches. Other variables ca be reused, because the same match object is used
0150     m_lastKey = key;
0151     m_lastProvider = filterData.searchProvider();
0152     m_match.setIconName(filterData.iconName());
0153     m_match.setId(QStringLiteral("WebShortcut:") + key);
0154 
0155     m_match.setText(i18n("Search %1 for %2", m_lastProvider, filterData.searchTerm()));
0156     m_match.setData(filterData.uri());
0157     m_match.setUrls(QList<QUrl>{filterData.uri()});
0158     context.addMatch(m_match);
0159 }
0160 
0161 void WebshortcutRunner::run(const KRunner::RunnerContext &context, const KRunner::QueryMatch &match)
0162 {
0163     QUrl location;
0164     if (m_filterBeforeRun) {
0165         m_filterBeforeRun = false;
0166         KUriFilterData filterData(context.query());
0167         if (KUriFilter::self()->filterSearchUri(filterData, KUriFilter::WebShortcutFilter))
0168             location = filterData.uri();
0169     } else {
0170         location = match.data().toUrl();
0171     }
0172 
0173     if (!location.isEmpty()) {
0174         if (match.selectedAction()) {
0175             QString command;
0176 
0177             // Chrome's exec line does not have a URL placeholder
0178             // Firefox's does, but only sometimes, depending on the distro
0179             // Replace placeholders if found, otherwise append at the end
0180             if (m_privateAction.exec().contains("%u")) {
0181                 command = m_privateAction.exec().replace("%u", KShell::quoteArg(location.toString()));
0182             } else if (m_privateAction.exec().contains("%U")) {
0183                 command = m_privateAction.exec().replace("%U", KShell::quoteArg(location.toString()));
0184             } else {
0185                 command = m_privateAction.exec() + QLatin1Char(' ') + KShell::quoteArg(location.toString());
0186             }
0187 
0188             auto *job = new KIO::CommandLauncherJob(command);
0189             job->start();
0190         } else {
0191             auto job = new KIO::OpenUrlJob(location);
0192             job->start();
0193         }
0194     }
0195 }
0196 
0197 K_PLUGIN_CLASS_WITH_JSON(WebshortcutRunner, "plasma-runner-webshortcuts.json")
0198 
0199 #include "webshortcutrunner.moc"