File indexing completed on 2024-04-14 15:33:38

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