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"