urlgrabber.cpp 13.3 KB
Newer Older
1
/*
2
    SPDX-FileCopyrightText: 2000, 2001, 2002 Carsten Pfeiffer <pfeiffer@kde.org>
3

4
    SPDX-License-Identifier: GPL-2.0-or-later
5
6
7
8
9
*/
#include "urlgrabber.h"

#include <netwm.h>

Laurent Montel's avatar
Laurent Montel committed
10
#include "klipper_debug.h"
11
#include <QFile>
Alexander Lohnau's avatar
Alexander Lohnau committed
12
#include <QIcon>
13
#include <QMenu>
Alexander Lohnau's avatar
Alexander Lohnau committed
14
#include <QMimeDatabase>
15
#include <QRegularExpression>
Alexander Lohnau's avatar
Alexander Lohnau committed
16
17
#include <QTimer>
#include <QUuid>
18

Alexander Lohnau's avatar
Alexander Lohnau committed
19
#include <KApplicationTrader>
20
#include <KIO/ApplicationLauncherJob>
Alexander Lohnau's avatar
Alexander Lohnau committed
21
#include <KLocalizedString>
22
#include <KNotificationJobUiDelegate>
23
24
#include <KService>
#include <KStringHandler>
25
#include <KWindowSystem>
26
27

#include "clipcommandprocess.h"
Alexander Lohnau's avatar
Alexander Lohnau committed
28
#include "klippersettings.h"
29
30
31
32
33

// TODO: script-interface?
#include "history.h"
#include "historystringitem.h"

Alexander Lohnau's avatar
Alexander Lohnau committed
34
35
36
37
38
39
40
URLGrabber::URLGrabber(History *history)
    : m_myCurrentAction(nullptr)
    , m_myMenu(nullptr)
    , m_myPopupKillTimer(new QTimer(this))
    , m_myPopupKillTimeout(8)
    , m_stripWhiteSpace(true)
    , m_history(history)
41
{
Alexander Lohnau's avatar
Alexander Lohnau committed
42
    m_myPopupKillTimer->setSingleShot(true);
Laurent Montel's avatar
Laurent Montel committed
43
    connect(m_myPopupKillTimer, &QTimer::timeout, this, &URLGrabber::slotKillPopupMenu);
44
45
46
47
48
49
50
51
52
53
54
55
56
}

URLGrabber::~URLGrabber()
{
    qDeleteAll(m_myActions);
    m_myActions.clear();
    delete m_myMenu;
}

//
// Called from Klipper::slotRepeatAction, i.e. by pressing Ctrl-Alt-R
// shortcut. I.e. never from clipboard monitoring
//
Alexander Lohnau's avatar
Alexander Lohnau committed
57
void URLGrabber::invokeAction(HistoryItemConstPtr item)
58
59
{
    m_myClipItem = item;
Alexander Lohnau's avatar
Alexander Lohnau committed
60
    actionMenu(item, false);
61
62
}

Alexander Lohnau's avatar
Alexander Lohnau committed
63
void URLGrabber::setActionList(const ActionList &list)
64
65
66
67
68
69
{
    qDeleteAll(m_myActions);
    m_myActions.clear();
    m_myActions = list;
}

Alexander Lohnau's avatar
Alexander Lohnau committed
70
void URLGrabber::matchingMimeActions(const QString &clipData)
71
72
{
    QUrl url(clipData);
Alexander Lohnau's avatar
Alexander Lohnau committed
73
    if (!KlipperSettings::enableMagicMimeActions()) {
74
75
        return;
    }
Alexander Lohnau's avatar
Alexander Lohnau committed
76
    if (!url.isValid()) {
77
78
        return;
    }
Alexander Lohnau's avatar
Alexander Lohnau committed
79
    if (url.isRelative()) { // openinng a relative path will just not work. what path should be used?
80
81
        return;
    }
Alexander Lohnau's avatar
Alexander Lohnau committed
82
83
    if (url.isLocalFile()) {
        if (clipData == QLatin1String("//")) {
84
85
            return;
        }
Alexander Lohnau's avatar
Alexander Lohnau committed
86
        if (!QFile::exists(url.toLocalFile())) {
87
88
89
90
91
            return;
        }
    }

    // try to figure out if clipData contains a filename
92
93
    QMimeDatabase db;
    QMimeType mimetype = db.mimeTypeForUrl(url);
94
95
96
97
98
99
100
101
102

    // let's see if we found some reasonable mimetype.
    // If we do we'll populate menu with actions for apps
    // that can handle that mimetype

    // first: if clipboard contents starts with http, let's assume it's "text/html".
    // That is even if we've url like "http://www.kde.org/somescript.pl", we'll
    // still treat that as html page, because determining a mimetype using kio
    // might take a long time, and i want this function to be quick!
Alexander Lohnau's avatar
Alexander Lohnau committed
103
    if ((clipData.startsWith(QLatin1String("http://")) || clipData.startsWith(QLatin1String("https://"))) && mimetype.name() != QLatin1String("text/html")) {
104
        mimetype = db.mimeTypeForName(QStringLiteral("text/html"));
105
106
    }

Alexander Lohnau's avatar
Alexander Lohnau committed
107
    if (!mimetype.isDefault()) {
Nicolas Fella's avatar
Nicolas Fella committed
108
        KService::List lst = KApplicationTrader::queryByMimeType(mimetype.name());
Alexander Lohnau's avatar
Alexander Lohnau committed
109
110
111
112
        if (!lst.isEmpty()) {
            ClipAction *action = new ClipAction(QString(), mimetype.comment());
            foreach (const KService::Ptr &service, lst) {
                action->addCommand(ClipCommand(QString(), service->name(), true, service->icon(), ClipCommand::IGNORE, service->storageId()));
113
            }
Alexander Lohnau's avatar
Alexander Lohnau committed
114
            m_myMatches.append(action);
115
        }
116
117
118
    }
}

Alexander Lohnau's avatar
Alexander Lohnau committed
119
const ActionList &URLGrabber::matchingActions(const QString &clipData, bool automatically_invoked)
120
121
122
123
124
125
{
    m_myMatches.clear();

    matchingMimeActions(clipData);

    // now look for matches in custom user actions
126
    QRegularExpression re;
Alexander Lohnau's avatar
Alexander Lohnau committed
127
    foreach (ClipAction *action, m_myActions) {
128
129
130
131
        re.setPattern(action->actionRegexPattern());
        const QRegularExpressionMatch match = re.match(clipData);
        if (match.hasMatch() && (action->automatic() || !automatically_invoked)) {
            action->setActionCapturedTexts(match.capturedTexts());
Alexander Lohnau's avatar
Alexander Lohnau committed
132
            m_myMatches.append(action);
133
134
135
136
137
138
        }
    }

    return m_myMatches;
}

Alexander Lohnau's avatar
Alexander Lohnau committed
139
void URLGrabber::checkNewData(HistoryItemConstPtr item)
140
{
Alexander Lohnau's avatar
Alexander Lohnau committed
141
    actionMenu(item, true); // also creates m_myMatches
142
143
}

Alexander Lohnau's avatar
Alexander Lohnau committed
144
void URLGrabber::actionMenu(HistoryItemConstPtr item, bool automatically_invoked)
145
146
{
    if (!item) {
Alexander Lohnau's avatar
Alexander Lohnau committed
147
148
        qWarning("Attempt to invoke URLGrabber without an item");
        return;
149
150
151
152
153
    }
    QString text(item->text());
    if (m_stripWhiteSpace) {
        text = text.trimmed();
    }
Alexander Lohnau's avatar
Alexander Lohnau committed
154
    ActionList matchingActionsList = matchingActions(text, automatically_invoked);
155
156
157

    if (!matchingActionsList.isEmpty()) {
        // don't react on blacklisted (e.g. konqi's/netscape's urls) unless the user explicitly asked for it
Alexander Lohnau's avatar
Alexander Lohnau committed
158
        if (automatically_invoked && isAvoidedWindow()) {
159
160
161
162
163
164
165
            return;
        }

        m_myCommandMapper.clear();

        m_myPopupKillTimer->stop();

166
        m_myMenu = new QMenu;
167

Laurent Montel's avatar
Laurent Montel committed
168
        connect(m_myMenu, &QMenu::triggered, this, &URLGrabber::slotItemSelected);
169

Alexander Lohnau's avatar
Alexander Lohnau committed
170
        foreach (ClipAction *clipAct, matchingActionsList) {
171
            m_myMenu->addSection(QIcon::fromTheme(QStringLiteral("klipper")), clipAct->description());
172
173
            QList<ClipCommand> cmdList = clipAct->commands();
            int listSize = cmdList.count();
Alexander Lohnau's avatar
Alexander Lohnau committed
174
            for (int i = 0; i < listSize; ++i) {
175
176
177
                ClipCommand command = cmdList.at(i);

                QString item = command.description;
Alexander Lohnau's avatar
Alexander Lohnau committed
178
                if (item.isEmpty())
179
180
181
                    item = command.command;

                QString id = QUuid::createUuid().toString();
Alexander Lohnau's avatar
Alexander Lohnau committed
182
                QAction *action = new QAction(this);
183
184
185
186
187
188
                action->setData(id);
                action->setText(item);

                if (!command.icon.isEmpty())
                    action->setIcon(QIcon::fromTheme(command.icon));

Alexander Lohnau's avatar
Alexander Lohnau committed
189
                m_myCommandMapper.insert(id, qMakePair(clipAct, i));
190
191
192
193
194
195
                m_myMenu->addAction(action);
            }
        }

        // only insert this when invoked via clipboard monitoring, not from an
        // explicit Ctrl-Alt-R
Alexander Lohnau's avatar
Alexander Lohnau committed
196
        if (automatically_invoked) {
197
198
            m_myMenu->addSeparator();
            QAction *disableAction = new QAction(i18n("Disable This Popup"), this);
Laurent Montel's avatar
Laurent Montel committed
199
            connect(disableAction, &QAction::triggered, this, &URLGrabber::sigDisablePopup);
200
201
202
203
            m_myMenu->addAction(disableAction);
        }
        m_myMenu->addSeparator();

204
        QAction *cancelAction = new QAction(QIcon::fromTheme(QStringLiteral("dialog-cancel")), i18n("&Cancel"), this);
Laurent Montel's avatar
Laurent Montel committed
205
        connect(cancelAction, &QAction::triggered, m_myMenu, &QMenu::hide);
206
207
208
        m_myMenu->addAction(cancelAction);
        m_myClipItem = item;

Alexander Lohnau's avatar
Alexander Lohnau committed
209
210
        if (m_myPopupKillTimeout > 0)
            m_myPopupKillTimer->start(1000 * m_myPopupKillTimeout);
211

Laurent Montel's avatar
Laurent Montel committed
212
        Q_EMIT sigPopup(m_myMenu);
213
214
215
    }
}

Alexander Lohnau's avatar
Alexander Lohnau committed
216
void URLGrabber::slotItemSelected(QAction *action)
217
218
219
220
221
222
223
{
    if (m_myMenu)
        m_myMenu->hide(); // deleted by the timer or the next action

    QString id = action->data().toString();

    if (id.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
224
        qCDebug(KLIPPER_LOG) << "Klipper: no command associated";
225
226
227
228
        return;
    }

    // first is action ptr, second is command index
Alexander Lohnau's avatar
Alexander Lohnau committed
229
    QPair<ClipAction *, int> actionCommand = m_myCommandMapper.value(id);
230
231
232
233

    if (actionCommand.first)
        execute(actionCommand.first, actionCommand.second);
    else
Laurent Montel's avatar
Laurent Montel committed
234
        qCDebug(KLIPPER_LOG) << "Klipper: cannot find associated action";
235
236
}

Alexander Lohnau's avatar
Alexander Lohnau committed
237
void URLGrabber::execute(const ClipAction *action, int cmdIdx) const
238
239
{
    if (!action) {
Laurent Montel's avatar
Laurent Montel committed
240
        qCDebug(KLIPPER_LOG) << "Action object is null";
241
242
243
244
245
        return;
    }

    ClipCommand command = action->command(cmdIdx);

Alexander Lohnau's avatar
Alexander Lohnau committed
246
    if (command.isEnabled) {
247
        QString text(m_myClipItem->text());
248
249
250
        if (m_stripWhiteSpace) {
            text = text.trimmed();
        }
Alexander Lohnau's avatar
Alexander Lohnau committed
251
252
        if (!command.serviceStorageId.isEmpty()) {
            KService::Ptr service = KService::serviceByStorageId(command.serviceStorageId);
253
254
255
256
            auto *job = new KIO::ApplicationLauncherJob(service);
            job->setUrls({QUrl(text)});
            job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled));
            job->start();
257
        } else {
Alexander Lohnau's avatar
Alexander Lohnau committed
258
            ClipCommandProcess *proc = new ClipCommandProcess(*action, command, text, m_history, m_myClipItem);
259
260
            if (proc->program().isEmpty()) {
                delete proc;
Friedrich W. H. Kossebau's avatar
Friedrich W. H. Kossebau committed
261
                proc = nullptr;
262
263
264
            } else {
                proc->start();
            }
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
        }
    }
}

void URLGrabber::loadSettings()
{
    m_stripWhiteSpace = KlipperSettings::stripWhiteSpace();
    m_myAvoidWindows = KlipperSettings::noActionsForWM_CLASS();
    m_myPopupKillTimeout = KlipperSettings::timeoutForActionPopups();

    qDeleteAll(m_myActions);
    m_myActions.clear();

    KConfigGroup cg(KSharedConfig::openConfig(), "General");
    int num = cg.readEntry("Number of Actions", 0);
    QString group;
Alexander Lohnau's avatar
Alexander Lohnau committed
281
282
283
    for (int i = 0; i < num; i++) {
        group = QStringLiteral("Action_%1").arg(i);
        m_myActions.append(new ClipAction(KSharedConfig::openConfig(), group));
284
285
286
287
288
289
    }
}

void URLGrabber::saveSettings() const
{
    KConfigGroup cg(KSharedConfig::openConfig(), "General");
Alexander Lohnau's avatar
Alexander Lohnau committed
290
    cg.writeEntry("Number of Actions", m_myActions.count());
291
292
293

    int i = 0;
    QString group;
Alexander Lohnau's avatar
Alexander Lohnau committed
294
295
296
    foreach (ClipAction *action, m_myActions) {
        group = QStringLiteral("Action_%1").arg(i);
        action->save(KSharedConfig::openConfig(), group);
297
298
299
300
301
302
303
304
305
        ++i;
    }

    KlipperSettings::setNoActionsForWM_CLASS(m_myAvoidWindows);
}

// find out whether the active window's WM_CLASS is in our avoid-list
bool URLGrabber::isAvoidedWindow() const
{
306
307
    const WId active = KWindowSystem::activeWindow();
    if (!active) {
308
309
        return false;
    }
310
    KWindowInfo info(active, NET::Properties(), NET::WM2WindowClass);
Laurent Montel's avatar
Laurent Montel committed
311
    return m_myAvoidWindows.contains(QString::fromLatin1(info.windowClassName()));
312
313
314
315
}

void URLGrabber::slotKillPopupMenu()
{
Alexander Lohnau's avatar
Alexander Lohnau committed
316
317
318
    if (m_myMenu && m_myMenu->isVisible()) {
        if (m_myMenu->geometry().contains(QCursor::pos()) && m_myPopupKillTimeout > 0) {
            m_myPopupKillTimer->start(1000 * m_myPopupKillTimeout);
319
320
321
322
            return;
        }
    }

Alexander Lohnau's avatar
Alexander Lohnau committed
323
    if (m_myMenu) {
324
        m_myMenu->deleteLater();
Laurent Montel's avatar
Laurent Montel committed
325
        m_myMenu = nullptr;
326
327
328
329
330
331
    }
}

///////////////////////////////////////////////////////////////////////////
////////

Alexander Lohnau's avatar
Alexander Lohnau committed
332
333
334
335
336
337
338
339
340
341
342
ClipCommand::ClipCommand(const QString &_command,
                         const QString &_description,
                         bool _isEnabled,
                         const QString &_icon,
                         Output _output,
                         const QString &_serviceStorageId)
    : command(_command)
    , description(_description)
    , isEnabled(_isEnabled)
    , output(_output)
    , serviceStorageId(_serviceStorageId)
343
344
345
{
    if (!_icon.isEmpty())
        icon = _icon;
Alexander Lohnau's avatar
Alexander Lohnau committed
346
    else {
347
        // try to find suitable icon
Alexander Lohnau's avatar
Alexander Lohnau committed
348
349
        QString appName = command.section(QLatin1Char(' '), 0, 0);
        if (!appName.isEmpty()) {
Kai Uwe Broulik's avatar
Kai Uwe Broulik committed
350
            if (QIcon::hasThemeIcon(appName))
351
352
353
354
355
356
357
                icon = appName;
            else
                icon.clear();
        }
    }
}

Alexander Lohnau's avatar
Alexander Lohnau committed
358
359
360
361
ClipAction::ClipAction(const QString &regExp, const QString &description, bool automatic)
    : m_regexPattern(regExp)
    , m_myDescription(description)
    , m_automatic(automatic)
362
363
364
{
}

Alexander Lohnau's avatar
Alexander Lohnau committed
365
366
367
368
ClipAction::ClipAction(KSharedConfigPtr kc, const QString &group)
    : m_regexPattern(kc->group(group).readEntry("Regexp"))
    , m_myDescription(kc->group(group).readEntry("Description"))
    , m_automatic(kc->group(group).readEntry("Automatic", QVariant(true)).toBool())
369
370
371
{
    KConfigGroup cg(kc, group);

Alexander Lohnau's avatar
Alexander Lohnau committed
372
    int num = cg.readEntry("Number of commands", 0);
373
374

    // read the commands
Alexander Lohnau's avatar
Alexander Lohnau committed
375
    for (int i = 0; i < num; i++) {
Laurent Montel's avatar
Laurent Montel committed
376
        QString _group = group + QStringLiteral("/Command_%1");
377
378
        KConfigGroup _cg(kc, _group.arg(i));

Alexander Lohnau's avatar
Alexander Lohnau committed
379
380
381
382
383
        addCommand(ClipCommand(_cg.readPathEntry("Commandline", QString()),
                               _cg.readEntry("Description"), // i18n'ed
                               _cg.readEntry("Enabled", false),
                               _cg.readEntry("Icon"),
                               static_cast<ClipCommand::Output>(_cg.readEntry("Output", QVariant(ClipCommand::IGNORE)).toInt())));
384
385
386
387
388
389
390
391
    }
}

ClipAction::~ClipAction()
{
    m_myCommands.clear();
}

Alexander Lohnau's avatar
Alexander Lohnau committed
392
void ClipAction::addCommand(const ClipCommand &cmd)
393
{
Alexander Lohnau's avatar
Alexander Lohnau committed
394
    if (cmd.command.isEmpty() && cmd.serviceStorageId.isEmpty())
395
396
        return;

Alexander Lohnau's avatar
Alexander Lohnau committed
397
    m_myCommands.append(cmd);
398
399
}

Alexander Lohnau's avatar
Alexander Lohnau committed
400
void ClipAction::replaceCommand(int idx, const ClipCommand &cmd)
401
{
Alexander Lohnau's avatar
Alexander Lohnau committed
402
    if (idx < 0 || idx >= m_myCommands.count()) {
Laurent Montel's avatar
Laurent Montel committed
403
        qCDebug(KLIPPER_LOG) << "wrong command index given";
404
405
406
407
408
409
410
        return;
    }

    m_myCommands.replace(idx, cmd);
}

// precondition: we're in the correct action's group of the KConfig object
Alexander Lohnau's avatar
Alexander Lohnau committed
411
void ClipAction::save(KSharedConfigPtr kc, const QString &group) const
412
413
{
    KConfigGroup cg(kc, group);
Alexander Lohnau's avatar
Alexander Lohnau committed
414
415
416
417
    cg.writeEntry("Description", description());
    cg.writeEntry("Regexp", actionRegexPattern());
    cg.writeEntry("Number of commands", m_myCommands.count());
    cg.writeEntry("Automatic", automatic());
418

Alexander Lohnau's avatar
Alexander Lohnau committed
419
    int i = 0;
420
    // now iterate over all commands of this action
Alexander Lohnau's avatar
Alexander Lohnau committed
421
    foreach (const ClipCommand &cmd, m_myCommands) {
Laurent Montel's avatar
Laurent Montel committed
422
        QString _group = group + QStringLiteral("/Command_%1");
423
424
        KConfigGroup cg(kc, _group.arg(i));

Alexander Lohnau's avatar
Alexander Lohnau committed
425
426
427
428
429
        cg.writePathEntry("Commandline", cmd.command);
        cg.writeEntry("Description", cmd.description);
        cg.writeEntry("Enabled", cmd.isEnabled);
        cg.writeEntry("Icon", cmd.icon);
        cg.writeEntry("Output", static_cast<int>(cmd.output));
430
431
432
433

        ++i;
    }
}