File indexing completed on 2024-05-05 17:45:03

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