File indexing completed on 2024-04-21 05:27:34

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