File indexing completed on 2024-04-28 05:35:29
0001 /* 0002 SPDX-FileCopyrightText: 2000, 2001, 2002 Carsten Pfeiffer <pfeiffer@kde.org> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 #include "urlgrabber.h" 0007 0008 #include <netwm.h> 0009 0010 #include "klipper_debug.h" 0011 #include <QFile> 0012 #include <QIcon> 0013 #include <QMenu> 0014 #include <QMimeDatabase> 0015 #include <QRegularExpression> 0016 #include <QTimer> 0017 #include <QUuid> 0018 0019 #include <KApplicationTrader> 0020 #include <KIO/ApplicationLauncherJob> 0021 #include <KLocalizedString> 0022 #include <KNotificationJobUiDelegate> 0023 #include <KService> 0024 #include <KStringHandler> 0025 #include <KWindowInfo> 0026 #include <KX11Extras> 0027 0028 #include "clipcommandprocess.h" 0029 #include "klippersettings.h" 0030 0031 // TODO: script-interface? 0032 #include "history.h" 0033 #include "historystringitem.h" 0034 0035 URLGrabber::URLGrabber(History *history) 0036 : m_myCurrentAction(nullptr) 0037 , m_myMenu(nullptr) 0038 , m_myPopupKillTimer(new QTimer(this)) 0039 , m_myPopupKillTimeout(8) 0040 , m_stripWhiteSpace(true) 0041 , m_history(history) 0042 { 0043 m_myPopupKillTimer->setSingleShot(true); 0044 connect(m_myPopupKillTimer, &QTimer::timeout, this, &URLGrabber::slotKillPopupMenu); 0045 } 0046 0047 URLGrabber::~URLGrabber() 0048 { 0049 qDeleteAll(m_myActions); 0050 m_myActions.clear(); 0051 delete m_myMenu; 0052 } 0053 0054 // 0055 // Called from Klipper::slotRepeatAction, i.e. by pressing Ctrl-Alt-R 0056 // shortcut. I.e. never from clipboard monitoring 0057 // 0058 void URLGrabber::invokeAction(HistoryItemConstPtr item) 0059 { 0060 m_myClipItem = item; 0061 actionMenu(item, false); 0062 } 0063 0064 void URLGrabber::setActionList(const ActionList &list) 0065 { 0066 qDeleteAll(m_myActions); 0067 m_myActions.clear(); 0068 m_myActions = list; 0069 } 0070 0071 void URLGrabber::matchingMimeActions(const QString &clipData) 0072 { 0073 QUrl url(clipData); 0074 if (!KlipperSettings::enableMagicMimeActions()) { 0075 return; 0076 } 0077 if (!url.isValid()) { 0078 return; 0079 } 0080 if (url.isRelative()) { // openinng a relative path will just not work. what path should be used? 0081 return; 0082 } 0083 if (url.isLocalFile()) { 0084 if (clipData == QLatin1String("//")) { 0085 return; 0086 } 0087 if (!QFile::exists(url.toLocalFile())) { 0088 return; 0089 } 0090 } 0091 0092 // try to figure out if clipData contains a filename 0093 QMimeDatabase db; 0094 QMimeType mimetype = db.mimeTypeForUrl(url); 0095 0096 // let's see if we found some reasonable mimetype. 0097 // If we do we'll populate menu with actions for apps 0098 // that can handle that mimetype 0099 0100 // first: if clipboard contents starts with http, let's assume it's "text/html". 0101 // That is even if we've url like "http://www.kde.org/somescript.pl", we'll 0102 // still treat that as html page, because determining a mimetype using kio 0103 // might take a long time, and i want this function to be quick! 0104 if ((clipData.startsWith(QLatin1String("http://")) || clipData.startsWith(QLatin1String("https://"))) && mimetype.name() != QLatin1String("text/html")) { 0105 mimetype = db.mimeTypeForName(QStringLiteral("text/html")); 0106 } 0107 0108 if (!mimetype.isDefault()) { 0109 KService::List lst = KApplicationTrader::queryByMimeType(mimetype.name()); 0110 if (!lst.isEmpty()) { 0111 ClipAction *action = new ClipAction(QString(), mimetype.comment()); 0112 foreach (const KService::Ptr &service, lst) { 0113 action->addCommand(ClipCommand(QString(), service->name(), true, service->icon(), ClipCommand::IGNORE, service->storageId())); 0114 } 0115 m_myMatches.append(action); 0116 } 0117 } 0118 } 0119 0120 const ActionList &URLGrabber::matchingActions(const QString &clipData, bool automatically_invoked) 0121 { 0122 m_myMatches.clear(); 0123 0124 matchingMimeActions(clipData); 0125 0126 // now look for matches in custom user actions 0127 QRegularExpression re; 0128 foreach (ClipAction *action, m_myActions) { 0129 re.setPattern(action->actionRegexPattern()); 0130 const QRegularExpressionMatch match = re.match(clipData); 0131 if (match.hasMatch() && (action->automatic() || !automatically_invoked)) { 0132 action->setActionCapturedTexts(match.capturedTexts()); 0133 m_myMatches.append(action); 0134 } 0135 } 0136 0137 return m_myMatches; 0138 } 0139 0140 void URLGrabber::checkNewData(HistoryItemConstPtr item) 0141 { 0142 actionMenu(item, true); // also creates m_myMatches 0143 } 0144 0145 void URLGrabber::actionMenu(HistoryItemConstPtr item, bool automatically_invoked) 0146 { 0147 if (!item) { 0148 qCWarning(KLIPPER_LOG, "Attempt to invoke URLGrabber without an item"); 0149 return; 0150 } 0151 QString text(item->text()); 0152 if (m_stripWhiteSpace) { 0153 text = std::move(text).trimmed(); 0154 } 0155 ActionList matchingActionsList = matchingActions(text, automatically_invoked); 0156 0157 if (!matchingActionsList.isEmpty()) { 0158 // don't react on blacklisted (e.g. konqi's/netscape's urls) unless the user explicitly asked for it 0159 if (automatically_invoked && isAvoidedWindow()) { 0160 return; 0161 } 0162 0163 m_myCommandMapper.clear(); 0164 0165 m_myPopupKillTimer->stop(); 0166 0167 m_myMenu = new QMenu; 0168 m_myMenu->setWindowFlags(m_myMenu->windowFlags() | Qt::FramelessWindowHint); 0169 0170 connect(m_myMenu, &QMenu::triggered, this, &URLGrabber::slotItemSelected); 0171 0172 foreach (ClipAction *clipAct, matchingActionsList) { 0173 m_myMenu->addSection(QIcon::fromTheme(QStringLiteral("klipper")), clipAct->description()); 0174 QList<ClipCommand> cmdList = clipAct->commands(); 0175 int listSize = cmdList.count(); 0176 for (int i = 0; i < listSize; ++i) { 0177 ClipCommand command = cmdList.at(i); 0178 0179 QString item = command.description; 0180 if (item.isEmpty()) 0181 item = command.command; 0182 0183 QString id = QUuid::createUuid().toString(); 0184 QAction *action = new QAction(this); 0185 action->setData(id); 0186 action->setText(item); 0187 0188 if (!command.icon.isEmpty()) 0189 action->setIcon(QIcon::fromTheme(command.icon)); 0190 0191 m_myCommandMapper.insert(id, qMakePair(clipAct, i)); 0192 m_myMenu->addAction(action); 0193 } 0194 } 0195 0196 // only insert this when invoked via clipboard monitoring, not from an 0197 // explicit Ctrl-Alt-R 0198 if (automatically_invoked) { 0199 m_myMenu->addSeparator(); 0200 QAction *disableAction = new QAction(i18n("Disable This Popup"), this); 0201 connect(disableAction, &QAction::triggered, this, &URLGrabber::sigDisablePopup); 0202 m_myMenu->addAction(disableAction); 0203 } 0204 m_myMenu->addSeparator(); 0205 0206 QAction *cancelAction = new QAction(QIcon::fromTheme(QStringLiteral("dialog-cancel")), i18n("&Cancel"), this); 0207 connect(cancelAction, &QAction::triggered, m_myMenu, &QMenu::hide); 0208 m_myMenu->addAction(cancelAction); 0209 m_myClipItem = item; 0210 0211 if (m_myPopupKillTimeout > 0) 0212 m_myPopupKillTimer->start(1000 * m_myPopupKillTimeout); 0213 0214 Q_EMIT sigPopup(m_myMenu); 0215 } 0216 } 0217 0218 void URLGrabber::slotItemSelected(QAction *action) 0219 { 0220 if (m_myMenu) 0221 m_myMenu->hide(); // deleted by the timer or the next action 0222 0223 QString id = action->data().toString(); 0224 0225 if (id.isEmpty()) { 0226 qCDebug(KLIPPER_LOG) << "Klipper: no command associated"; 0227 return; 0228 } 0229 0230 // first is action ptr, second is command index 0231 QPair<ClipAction *, int> actionCommand = m_myCommandMapper.value(id); 0232 0233 if (actionCommand.first) 0234 execute(actionCommand.first, actionCommand.second); 0235 else 0236 qCDebug(KLIPPER_LOG) << "Klipper: cannot find associated action"; 0237 } 0238 0239 void URLGrabber::execute(const ClipAction *action, int cmdIdx) const 0240 { 0241 if (!action) { 0242 qCDebug(KLIPPER_LOG) << "Action object is null"; 0243 return; 0244 } 0245 0246 ClipCommand command = action->command(cmdIdx); 0247 0248 if (command.isEnabled) { 0249 QString text(m_myClipItem->text()); 0250 if (m_stripWhiteSpace) { 0251 text = std::move(text).trimmed(); 0252 } 0253 if (!command.serviceStorageId.isEmpty()) { 0254 KService::Ptr service = KService::serviceByStorageId(command.serviceStorageId); 0255 auto *job = new KIO::ApplicationLauncherJob(service); 0256 job->setUrls({QUrl(text)}); 0257 job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled)); 0258 job->start(); 0259 } else { 0260 ClipCommandProcess *proc = new ClipCommandProcess(*action, command, text, m_history, m_myClipItem); 0261 if (proc->program().isEmpty()) { 0262 delete proc; 0263 proc = nullptr; 0264 } else { 0265 proc->start(); 0266 } 0267 } 0268 } 0269 } 0270 0271 void URLGrabber::loadSettings() 0272 { 0273 m_stripWhiteSpace = KlipperSettings::stripWhiteSpace(); 0274 m_myAvoidWindows = KlipperSettings::noActionsForWM_CLASS(); 0275 m_myPopupKillTimeout = KlipperSettings::timeoutForActionPopups(); 0276 0277 qDeleteAll(m_myActions); 0278 m_myActions.clear(); 0279 0280 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("General")); 0281 int num = cg.readEntry("Number of Actions", 0); 0282 QString group; 0283 for (int i = 0; i < num; i++) { 0284 group = QStringLiteral("Action_%1").arg(i); 0285 m_myActions.append(new ClipAction(KSharedConfig::openConfig(), group)); 0286 } 0287 } 0288 0289 void URLGrabber::saveSettings() const 0290 { 0291 KConfigGroup cg(KSharedConfig::openConfig(), QStringLiteral("General")); 0292 cg.writeEntry("Number of Actions", m_myActions.count()); 0293 0294 int i = 0; 0295 QString group; 0296 foreach (ClipAction *action, m_myActions) { 0297 group = QStringLiteral("Action_%1").arg(i); 0298 action->save(KSharedConfig::openConfig(), group); 0299 ++i; 0300 } 0301 0302 KlipperSettings::setNoActionsForWM_CLASS(m_myAvoidWindows); 0303 } 0304 0305 // find out whether the active window's WM_CLASS is in our avoid-list 0306 bool URLGrabber::isAvoidedWindow() const 0307 { 0308 const WId active = KX11Extras::activeWindow(); 0309 if (!active) { 0310 return false; 0311 } 0312 KWindowInfo info(active, NET::Properties(), NET::WM2WindowClass); 0313 return m_myAvoidWindows.contains(QString::fromLatin1(info.windowClassName())); 0314 } 0315 0316 void URLGrabber::slotKillPopupMenu() 0317 { 0318 if (m_myMenu && m_myMenu->isVisible()) { 0319 if (m_myMenu->geometry().contains(QCursor::pos()) && m_myPopupKillTimeout > 0) { 0320 m_myPopupKillTimer->start(1000 * m_myPopupKillTimeout); 0321 return; 0322 } 0323 } 0324 0325 if (m_myMenu) { 0326 m_myMenu->deleteLater(); 0327 m_myMenu = nullptr; 0328 } 0329 } 0330 0331 /////////////////////////////////////////////////////////////////////////// 0332 //////// 0333 0334 ClipCommand::ClipCommand(const QString &_command, 0335 const QString &_description, 0336 bool _isEnabled, 0337 const QString &_icon, 0338 Output _output, 0339 const QString &_serviceStorageId) 0340 : command(_command) 0341 , description(_description) 0342 , isEnabled(_isEnabled) 0343 , output(_output) 0344 , serviceStorageId(_serviceStorageId) 0345 { 0346 if (!_icon.isEmpty()) 0347 icon = _icon; 0348 else { 0349 // try to find suitable icon 0350 QString appName = command.section(QLatin1Char(' '), 0, 0); 0351 if (!appName.isEmpty()) { 0352 if (QIcon::hasThemeIcon(appName)) 0353 icon = appName; 0354 else 0355 icon.clear(); 0356 } 0357 } 0358 } 0359 0360 ClipAction::ClipAction(const QString ®Exp, const QString &description, bool automatic) 0361 : m_regexPattern(regExp) 0362 , m_myDescription(description) 0363 , m_automatic(automatic) 0364 { 0365 } 0366 0367 ClipAction::ClipAction(KSharedConfigPtr kc, const QString &group) 0368 : m_regexPattern(kc->group(group).readEntry("Regexp")) 0369 , m_myDescription(kc->group(group).readEntry("Description")) 0370 , m_automatic(kc->group(group).readEntry("Automatic", QVariant(true)).toBool()) 0371 { 0372 KConfigGroup cg(kc, group); 0373 0374 int num = cg.readEntry("Number of commands", 0); 0375 0376 // read the commands 0377 for (int i = 0; i < num; i++) { 0378 QString _group = group + QStringLiteral("/Command_%1"); 0379 KConfigGroup _cg(kc, _group.arg(i)); 0380 0381 addCommand(ClipCommand(_cg.readPathEntry("Commandline", QString()), 0382 _cg.readEntry("Description"), // i18n'ed 0383 _cg.readEntry("Enabled", false), 0384 _cg.readEntry("Icon"), 0385 static_cast<ClipCommand::Output>(_cg.readEntry("Output", QVariant(ClipCommand::IGNORE)).toInt()))); 0386 } 0387 } 0388 0389 ClipAction::~ClipAction() 0390 { 0391 m_myCommands.clear(); 0392 } 0393 0394 void ClipAction::addCommand(const ClipCommand &cmd) 0395 { 0396 if (cmd.command.isEmpty() && cmd.serviceStorageId.isEmpty()) 0397 return; 0398 0399 m_myCommands.append(cmd); 0400 } 0401 0402 void ClipAction::replaceCommand(int idx, const ClipCommand &cmd) 0403 { 0404 if (idx < 0 || idx >= m_myCommands.count()) { 0405 qCDebug(KLIPPER_LOG) << "wrong command index given"; 0406 return; 0407 } 0408 0409 m_myCommands.replace(idx, cmd); 0410 } 0411 0412 // precondition: we're in the correct action's group of the KConfig object 0413 void ClipAction::save(KSharedConfigPtr kc, const QString &group) const 0414 { 0415 KConfigGroup cg(kc, group); 0416 cg.writeEntry("Description", description()); 0417 cg.writeEntry("Regexp", actionRegexPattern()); 0418 cg.writeEntry("Number of commands", m_myCommands.count()); 0419 cg.writeEntry("Automatic", automatic()); 0420 0421 int i = 0; 0422 // now iterate over all commands of this action 0423 foreach (const ClipCommand &cmd, m_myCommands) { 0424 QString _group = group + QStringLiteral("/Command_%1"); 0425 KConfigGroup cg(kc, _group.arg(i)); 0426 0427 cg.writePathEntry("Commandline", cmd.command); 0428 cg.writeEntry("Description", cmd.description); 0429 cg.writeEntry("Enabled", cmd.isEnabled); 0430 cg.writeEntry("Icon", cmd.icon); 0431 cg.writeEntry("Output", static_cast<int>(cmd.output)); 0432 0433 ++i; 0434 } 0435 }