File indexing completed on 2022-03-06 17:13:37

0001 /********************************************************************
0002  KSld - the KDE Screenlocker Daemon
0003  This file is part of the KDE project.
0004 
0005 Copyright (C) 2015 Martin Gräßlin <mgraesslin@kde.org>
0006 
0007 This program is free software; you can redistribute it and/or modify
0008 it under the terms of the GNU General Public License as published by
0009 the Free Software Foundation; either version 2 of the License, or
0010 (at your option) any later version.
0011 
0012 This program is distributed in the hope that it will be useful,
0013 but WITHOUT ANY WARRANTY; without even the implied warranty of
0014 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0015 GNU General Public License for more details.
0016 
0017 You should have received a copy of the GNU General Public License
0018 along with this program.  If not, see <http://www.gnu.org/licenses/>.
0019 *********************************************************************/
0020 #include "globalaccel.h"
0021 
0022 #include <KKeyServer>
0023 #include <netwm.h>
0024 
0025 #include <QDBusConnection>
0026 #include <QDBusMessage>
0027 #include <QDBusPendingReply>
0028 #include <QKeyEvent>
0029 #include <QRegularExpression>
0030 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0031 #include <private/qtx11extras_p.h>
0032 #else
0033 #include <QX11Info>
0034 #endif
0035 
0036 #include <X11/keysym.h>
0037 #include <xcb/xcb.h>
0038 #include <xcb/xcb_keysyms.h>
0039 
0040 static const QString s_kglobalAccelService = QStringLiteral("org.kde.kglobalaccel");
0041 static const QString s_componentInterface = QStringLiteral("org.kde.kglobalaccel.Component");
0042 
0043 /**
0044  * Whitelist of the components which are allowed to get global shortcuts.
0045  * The DBus path of the component is the key for the whitelist.
0046  * The value for each key contains a regular expression matching unique shortcut names which are allowed.
0047  * This allows to not only restrict on component, but also restrict on the shortcuts.
0048  * E.g. plasmashell might accept media shortcuts, but not shortcuts for switching the activity.
0049  **/
0050 static const QMap<QString, QRegularExpression> s_shortcutWhitelist{
0051     {QStringLiteral("/component/mediacontrol"), QRegularExpression(QStringLiteral("stopmedia|nextmedia|previousmedia|playpausemedia"))},
0052     {QStringLiteral("/component/kmix"), QRegularExpression(QStringLiteral("mute|decrease_volume|increase_volume"))},
0053     {QStringLiteral("/component/org_kde_powerdevil"),
0054      QRegularExpression(QStringLiteral(
0055          "Increase Screen Brightness|Decrease Screen Brightness|Increase Keyboard Brightness|Decrease Keyboard Brightness|Turn Off Screen|Sleep|Hibernate"))},
0056     {QStringLiteral("/component/KDE_Keyboard_Layout_Switcher"),
0057      QRegularExpression(QStringLiteral("Switch to Next Keyboard Layout|Switch keyboard layout to .*"))},
0058     {QStringLiteral("/component/kcm_touchpad"), QRegularExpression(QStringLiteral("Toggle Touchpad|Enable Touchpad|Disable Touchpad"))},
0059 };
0060 
0061 static uint g_keyModMaskXAccel = 0;
0062 static uint g_keyModMaskXOnOrOff = 0;
0063 
0064 static void calculateGrabMasks()
0065 {
0066     g_keyModMaskXAccel = KKeyServer::accelModMaskX();
0067     g_keyModMaskXOnOrOff = KKeyServer::modXLock() | KKeyServer::modXNumLock() | KKeyServer::modXScrollLock() | KKeyServer::modXModeSwitch();
0068 }
0069 
0070 GlobalAccel::GlobalAccel(QObject *parent)
0071     : QObject(parent)
0072 {
0073 }
0074 
0075 void GlobalAccel::prepare()
0076 {
0077     // recursion check
0078     if (m_updatingInformation) {
0079         return;
0080     }
0081     // first ensure that we don't have some left over
0082     release();
0083 
0084     if (QX11Info::isPlatformX11()) {
0085         m_keySymbols = xcb_key_symbols_alloc(QX11Info::connection());
0086         calculateGrabMasks();
0087     }
0088 
0089     // fetch all components from KGlobalAccel
0090     m_updatingInformation++;
0091     auto message = QDBusMessage::createMethodCall(s_kglobalAccelService,
0092                                                   QStringLiteral("/kglobalaccel"),
0093                                                   QStringLiteral("org.kde.KGlobalAccel"),
0094                                                   QStringLiteral("allComponents"));
0095     QDBusPendingReply<QList<QDBusObjectPath>> async = QDBusConnection::sessionBus().asyncCall(message);
0096     QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this);
0097     connect(callWatcher, &QDBusPendingCallWatcher::finished, this, &GlobalAccel::components);
0098 }
0099 
0100 void GlobalAccel::components(QDBusPendingCallWatcher *self)
0101 {
0102     QDBusPendingReply<QList<QDBusObjectPath>> reply = *self;
0103     self->deleteLater();
0104     if (!reply.isValid()) {
0105         m_updatingInformation--;
0106         return;
0107     }
0108     // go through all components, check whether they are in our whitelist
0109     // if they are whitelisted we check whether they are active
0110     for (const auto &path : reply.value()) {
0111         const QString objectPath = path.path();
0112         bool whitelisted = false;
0113         for (auto it = s_shortcutWhitelist.begin(); it != s_shortcutWhitelist.end(); ++it) {
0114             if (objectPath == it.key()) {
0115                 whitelisted = true;
0116                 break;
0117             }
0118         }
0119         if (!whitelisted) {
0120             continue;
0121         }
0122         auto message = QDBusMessage::createMethodCall(s_kglobalAccelService, objectPath, s_componentInterface, QStringLiteral("isActive"));
0123         QDBusPendingReply<bool> async = QDBusConnection::sessionBus().asyncCall(message);
0124         QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this);
0125         m_updatingInformation++;
0126         connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [this, objectPath](QDBusPendingCallWatcher *self) {
0127             QDBusPendingReply<bool> reply = *self;
0128             self->deleteLater();
0129             // filter out inactive components
0130             if (!reply.isValid() || !reply.value()) {
0131                 m_updatingInformation--;
0132                 return;
0133             }
0134 
0135             // active, whitelisted component: get all shortcuts
0136             auto message = QDBusMessage::createMethodCall(s_kglobalAccelService, objectPath, s_componentInterface, QStringLiteral("allShortcutInfos"));
0137             QDBusPendingReply<QList<KGlobalShortcutInfo>> async = QDBusConnection::sessionBus().asyncCall(message);
0138             QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this);
0139             connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [this, objectPath](QDBusPendingCallWatcher *self) {
0140                 m_updatingInformation--;
0141                 QDBusPendingReply<QList<KGlobalShortcutInfo>> reply = *self;
0142                 self->deleteLater();
0143                 if (!reply.isValid()) {
0144                     return;
0145                 }
0146                 // restrict to whitelist
0147                 QList<KGlobalShortcutInfo> infos;
0148                 auto whitelist = s_shortcutWhitelist.constFind(objectPath);
0149                 if (whitelist == s_shortcutWhitelist.constEnd()) {
0150                     // this should not happen, just for safety
0151                     return;
0152                 }
0153                 const auto s = reply.value();
0154                 for (auto it = s.begin(); it != s.end(); ++it) {
0155                     auto matches = whitelist.value().match((*it).uniqueName());
0156                     if (matches.hasMatch()) {
0157                         infos.append(*it);
0158                     }
0159                 }
0160                 m_shortcuts.insert(objectPath, infos);
0161             });
0162         });
0163     }
0164     m_updatingInformation--;
0165 }
0166 
0167 void GlobalAccel::release()
0168 {
0169     m_shortcuts.clear();
0170     if (m_keySymbols) {
0171         xcb_key_symbols_free(m_keySymbols);
0172         m_keySymbols = nullptr;
0173     }
0174 }
0175 
0176 bool GlobalAccel::keyEvent(QKeyEvent *event)
0177 {
0178     const int keyCodeQt = event->key();
0179     Qt::KeyboardModifiers keyModQt = event->modifiers();
0180 
0181     if (keyModQt & Qt::SHIFT && !KKeyServer::isShiftAsModifierAllowed(keyCodeQt)) {
0182         keyModQt &= ~Qt::SHIFT;
0183     }
0184 
0185     if ((keyModQt == 0 || keyModQt == Qt::SHIFT) && (keyCodeQt >= Qt::Key_Space && keyCodeQt <= Qt::Key_AsciiTilde)) {
0186         // security check: we don't allow shortcuts without modifier for "normal" keys
0187         // this is to prevent a malicious application to grab shortcuts for all keys
0188         // and by that being able to read out the keyboard
0189         return false;
0190     }
0191 
0192     const QKeySequence seq(keyCodeQt | keyModQt);
0193     // let's check whether we have a mapping shortcut
0194     for (auto it = m_shortcuts.constBegin(); it != m_shortcuts.constEnd(); ++it) {
0195         for (const auto &info : it.value()) {
0196             if (info.keys().contains(seq)) {
0197                 auto signal = QDBusMessage::createMethodCall(s_kglobalAccelService, it.key(), s_componentInterface, QStringLiteral("invokeShortcut"));
0198                 signal.setArguments(QList<QVariant>{QVariant(info.uniqueName())});
0199                 QDBusConnection::sessionBus().asyncCall(signal);
0200                 return true;
0201             }
0202         }
0203     }
0204     return false;
0205 }
0206 
0207 bool GlobalAccel::checkKeyPress(xcb_key_press_event_t *event)
0208 {
0209     if (!m_keySymbols) {
0210         return false;
0211     }
0212     // based and inspired from code in kglobalaccel_x11.cpp
0213     xcb_keycode_t keyCodeX = event->detail;
0214     uint16_t keyModX = event->state & (g_keyModMaskXAccel | KKeyServer::MODE_SWITCH);
0215 
0216     xcb_keysym_t keySymX = xcb_key_press_lookup_keysym(m_keySymbols, event, 0);
0217 
0218     // If numlock is active and a keypad key is pressed, XOR the SHIFT state.
0219     //  e.g., KP_4 => Shift+KP_Left, and Shift+KP_4 => KP_Left.
0220     if (event->state & KKeyServer::modXNumLock()) {
0221         xcb_keysym_t sym = xcb_key_symbols_get_keysym(m_keySymbols, keyCodeX, 0);
0222         // If this is a keypad key,
0223         if (sym >= XK_KP_Space && sym <= XK_KP_9) {
0224             switch (sym) {
0225             // Leave the following keys unaltered
0226             // FIXME: The proper solution is to see which keysyms don't change when shifted.
0227             case XK_KP_Multiply:
0228             case XK_KP_Add:
0229             case XK_KP_Subtract:
0230             case XK_KP_Divide:
0231                 break;
0232 
0233             default:
0234                 keyModX ^= KKeyServer::modXShift();
0235             }
0236         }
0237     }
0238 
0239     int keyCodeQt;
0240     KKeyServer::symXModXToKeyQt(keySymX, keyModX, &keyCodeQt);
0241     // Split keycode and modifier
0242     int keyModQt = keyCodeQt & Qt::KeyboardModifierMask;
0243     keyCodeQt &= ~Qt::KeyboardModifierMask;
0244 
0245     if (keyModQt & Qt::SHIFT && !KKeyServer::isShiftAsModifierAllowed(keyCodeQt)) {
0246         keyModQt &= ~Qt::SHIFT;
0247     }
0248 
0249     if ((keyModQt == 0 || keyModQt == Qt::SHIFT) && (keyCodeQt >= Qt::Key_Space && keyCodeQt <= Qt::Key_AsciiTilde)) {
0250         // security check: we don't allow shortcuts without modifier for "normal" keys
0251         // this is to prevent a malicious application to grab shortcuts for all keys
0252         // and by that being able to read out the keyboard
0253         return false;
0254     }
0255 
0256     const QKeySequence seq(keyCodeQt | keyModQt);
0257     // let's check whether we have a mapping shortcut
0258     for (auto it = m_shortcuts.begin(); it != m_shortcuts.end(); ++it) {
0259         for (const auto &info : it.value()) {
0260             if (info.keys().contains(seq)) {
0261                 auto signal = QDBusMessage::createMethodCall(s_kglobalAccelService, it.key(), s_componentInterface, QStringLiteral("invokeShortcut"));
0262                 signal.setArguments(QList<QVariant>{QVariant(info.uniqueName())});
0263                 QDBusConnection::sessionBus().asyncCall(signal);
0264                 return true;
0265             }
0266         }
0267     }
0268     return false;
0269 }