File indexing completed on 2022-06-16 18:36:48

0001 /********************************************************************
0002  KSld - the KDE Screenlocker Daemon
0003  This file is part of the KDE project.
0004 
0005 Copyright (C) 1999 Martin R. Jones <mjones@kde.org>
0006 Copyright (C) 2002 Luboš Luňák <l.lunak@kde.org>
0007 Copyright (C) 2003 Oswald Buddenhagen <ossi@kde.org>
0008 Copyright (C) 2008 Chani Armitage <chanika@gmail.com>
0009 Copyright (C) 2011 Martin Gräßlin <mgraesslin@kde.org>
0010 
0011 This program is free software; you can redistribute it and/or modify
0012 it under the terms of the GNU General Public License as published by
0013 the Free Software Foundation; either version 2 of the License, or
0014 (at your option) any later version.
0015 
0016 This program is distributed in the hope that it will be useful,
0017 but WITHOUT ANY WARRANTY; without even the implied warranty of
0018 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0019 GNU General Public License for more details.
0020 
0021 You should have received a copy of the GNU General Public License
0022 along with this program.  If not, see <http://www.gnu.org/licenses/>.
0023 *********************************************************************/
0024 #include "x11locker.h"
0025 #include "globalaccel.h"
0026 // KDE
0027 // Qt
0028 #include <QApplication>
0029 #include <QScreen>
0030 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0031 #include <private/qtx11extras_p.h>
0032 #else
0033 #include <QDesktopWidget>
0034 #include <QX11Info>
0035 #endif
0036 // X11
0037 #include <X11/Xatom.h>
0038 #include <xcb/xcb.h>
0039 
0040 #include <kscreenlocker_logging.h>
0041 
0042 static Window gVRoot = 0;
0043 static Window gVRootData = 0;
0044 static Atom gXA_VROOT;
0045 static Atom gXA_SCREENSAVER_VERSION;
0046 
0047 namespace ScreenLocker
0048 {
0049 X11Locker::X11Locker(QObject *parent)
0050     : AbstractLocker(parent)
0051     , QAbstractNativeEventFilter()
0052     , m_focusedLockWindow(XCB_WINDOW_NONE)
0053 {
0054     initialize();
0055 }
0056 
0057 X11Locker::~X11Locker()
0058 {
0059     qApp->removeNativeEventFilter(this);
0060 }
0061 
0062 void X11Locker::initialize()
0063 {
0064     qApp->installNativeEventFilter(this);
0065 
0066     XWindowAttributes rootAttr;
0067     XGetWindowAttributes(QX11Info::display(), QX11Info::appRootWindow(), &rootAttr);
0068     XSelectInput(QX11Info::display(), QX11Info::appRootWindow(), SubstructureNotifyMask | rootAttr.your_event_mask);
0069     // Get root window size
0070     updateGeo();
0071 
0072     // virtual root property
0073     gXA_VROOT = XInternAtom(QX11Info::display(), "__SWM_VROOT", False);
0074     gXA_SCREENSAVER_VERSION = XInternAtom(QX11Info::display(), "_SCREENSAVER_VERSION", False);
0075 
0076     // read the initial information about all toplevel windows
0077     Window r, p;
0078     Window *real;
0079     unsigned nreal;
0080     if (XQueryTree(QX11Info::display(), QX11Info::appRootWindow(), &r, &p, &real, &nreal) && real != nullptr) {
0081         for (unsigned i = 0; i < nreal; ++i) {
0082             XWindowAttributes winAttr;
0083             if (XGetWindowAttributes(QX11Info::display(), real[i], &winAttr)) {
0084                 WindowInfo info;
0085                 info.window = real[i];
0086                 info.viewable = (winAttr.map_state == IsViewable);
0087                 m_windowInfo.append(info); // ordered bottom to top
0088             }
0089         }
0090         XFree(real);
0091     }
0092 
0093     // monitor for screen geometry changes
0094     connect(qGuiApp, &QGuiApplication::screenAdded, this, [this](QScreen *screen) {
0095         connect(screen, &QScreen::geometryChanged, this, &X11Locker::updateGeo);
0096         updateGeo();
0097     });
0098     connect(qGuiApp, &QGuiApplication::screenRemoved, this, &X11Locker::updateGeo);
0099     const auto screens = QGuiApplication::screens();
0100     for (auto *screen : screens) {
0101         connect(screen, &QScreen::geometryChanged, this, &X11Locker::updateGeo);
0102     }
0103 }
0104 
0105 void X11Locker::showLockWindow()
0106 {
0107     m_background->hide();
0108 
0109     // Some xscreensaver hacks check for this property
0110     const char *version = "KDE 4.0";
0111 
0112     XChangeProperty(QX11Info::display(),
0113                     m_background->winId(),
0114                     gXA_SCREENSAVER_VERSION,
0115                     XA_STRING,
0116                     8,
0117                     PropModeReplace,
0118                     (unsigned char *)version,
0119                     strlen(version));
0120 
0121     qCDebug(KSCREENLOCKER) << "Lock window Id: " << m_background->winId();
0122 
0123     m_background->setPosition(0, 0);
0124     XSync(QX11Info::display(), False);
0125 
0126     setVRoot(m_background->winId(), m_background->winId());
0127 }
0128 
0129 //---------------------------------------------------------------------------
0130 //
0131 // Hide the screen locker window
0132 //
0133 void X11Locker::hideLockWindow()
0134 {
0135     Q_EMIT userActivity();
0136     m_background->hide();
0137     m_background->lower();
0138     removeVRoot(m_background->winId());
0139     XDeleteProperty(QX11Info::display(), m_background->winId(), gXA_SCREENSAVER_VERSION);
0140     if (gVRoot) {
0141         unsigned long vroot_data[1] = {gVRootData};
0142         XChangeProperty(QX11Info::display(), gVRoot, gXA_VROOT, XA_WINDOW, 32, PropModeReplace, (unsigned char *)vroot_data, 1);
0143         gVRoot = 0;
0144     }
0145     XSync(QX11Info::display(), False);
0146     m_allowedWindows.clear();
0147 }
0148 
0149 //---------------------------------------------------------------------------
0150 static int ignoreXError(Display *, XErrorEvent *)
0151 {
0152     return 0;
0153 }
0154 
0155 //---------------------------------------------------------------------------
0156 //
0157 // Save the current virtual root window
0158 //
0159 void X11Locker::saveVRoot()
0160 {
0161     Window rootReturn, parentReturn, *children;
0162     unsigned int numChildren;
0163     Window root = QX11Info::appRootWindow();
0164 
0165     gVRoot = 0;
0166     gVRootData = 0;
0167 
0168     int (*oldHandler)(Display *, XErrorEvent *);
0169     oldHandler = XSetErrorHandler(ignoreXError);
0170 
0171     if (XQueryTree(QX11Info::display(), root, &rootReturn, &parentReturn, &children, &numChildren)) {
0172         for (unsigned int i = 0; i < numChildren; i++) {
0173             Atom actual_type;
0174             int actual_format;
0175             unsigned long nitems, bytesafter;
0176             unsigned char *newRoot = nullptr;
0177 
0178             if ((XGetWindowProperty(QX11Info::display(),
0179                                     children[i],
0180                                     gXA_VROOT,
0181                                     0,
0182                                     1,
0183                                     False,
0184                                     XA_WINDOW,
0185                                     &actual_type,
0186                                     &actual_format,
0187                                     &nitems,
0188                                     &bytesafter,
0189                                     &newRoot)
0190                  == Success)
0191                 && newRoot) {
0192                 gVRoot = children[i];
0193                 Window *dummy = (Window *)newRoot;
0194                 gVRootData = *dummy;
0195                 XFree((char *)newRoot);
0196                 break;
0197             }
0198         }
0199         if (children) {
0200             XFree((char *)children);
0201         }
0202     }
0203 
0204     XSetErrorHandler(oldHandler);
0205 }
0206 
0207 //---------------------------------------------------------------------------
0208 //
0209 // Set the virtual root property
0210 //
0211 void X11Locker::setVRoot(Window win, Window vr)
0212 {
0213     if (gVRoot) {
0214         removeVRoot(gVRoot);
0215     }
0216 
0217     unsigned long rw = QX11Info::appRootWindow();
0218     unsigned long vroot_data[1] = {vr};
0219 
0220     Window rootReturn, parentReturn, *children;
0221     unsigned int numChildren;
0222     Window top = win;
0223     while (1) {
0224         if (!XQueryTree(QX11Info::display(), top, &rootReturn, &parentReturn, &children, &numChildren)) {
0225             return;
0226         }
0227         if (children) {
0228             XFree((char *)children);
0229         }
0230         if (parentReturn == rw) {
0231             break;
0232         } else {
0233             top = parentReturn;
0234         }
0235     }
0236 
0237     XChangeProperty(QX11Info::display(), top, gXA_VROOT, XA_WINDOW, 32, PropModeReplace, (unsigned char *)vroot_data, 1);
0238 }
0239 
0240 //---------------------------------------------------------------------------
0241 //
0242 // Remove the virtual root property
0243 //
0244 void X11Locker::removeVRoot(Window win)
0245 {
0246     XDeleteProperty(QX11Info::display(), win, gXA_VROOT);
0247 }
0248 
0249 void X11Locker::fakeFocusIn(WId window)
0250 {
0251     if (window == m_focusedLockWindow) {
0252         return;
0253     }
0254 
0255     // We have keyboard grab, so this application will
0256     // get keyboard events even without having focus.
0257     // Fake FocusIn to make Qt realize it has the active
0258     // window, so that it will correctly show cursor in the dialog.
0259     XEvent ev;
0260     memset(&ev, 0, sizeof(ev));
0261     ev.xfocus.display = QX11Info::display();
0262     ev.xfocus.type = FocusIn;
0263     ev.xfocus.window = window;
0264     ev.xfocus.mode = NotifyNormal;
0265     ev.xfocus.detail = NotifyAncestor;
0266     XSendEvent(QX11Info::display(), window, False, NoEventMask, &ev);
0267     XFlush(QX11Info::display());
0268 
0269     m_focusedLockWindow = window;
0270 }
0271 
0272 template<typename T>
0273 void coordFromEvent(xcb_generic_event_t *event, int *x, int *y)
0274 {
0275     T *e = reinterpret_cast<T *>(event);
0276     *x = e->event_x;
0277     *y = e->event_y;
0278 }
0279 
0280 template<typename T>
0281 void sendEvent(xcb_generic_event_t *event, xcb_window_t target, int x, int y)
0282 {
0283     T e = *(reinterpret_cast<T *>(event));
0284     e.event = target;
0285     e.child = target;
0286     e.event_x = x;
0287     e.event_y = y;
0288     xcb_send_event(QX11Info::connection(), false, target, XCB_EVENT_MASK_NO_EVENT, reinterpret_cast<const char *>(&e));
0289 }
0290 
0291 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0292 bool X11Locker::nativeEventFilter(const QByteArray &eventType, void *message, long int *)
0293 #else
0294 bool X11Locker::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *)
0295 #endif
0296 {
0297     if (eventType != QByteArrayLiteral("xcb_generic_event_t")) {
0298         return false;
0299     }
0300     xcb_generic_event_t *event = reinterpret_cast<xcb_generic_event_t *>(message);
0301     const uint8_t responseType = event->response_type & ~0x80;
0302     if (globalAccel() && responseType == XCB_KEY_PRESS) {
0303         if (globalAccel()->checkKeyPress(reinterpret_cast<xcb_key_press_event_t *>(event))) {
0304             Q_EMIT userActivity();
0305             return true;
0306         }
0307     }
0308     bool ret = false;
0309     switch (responseType) {
0310     case XCB_BUTTON_PRESS:
0311     case XCB_BUTTON_RELEASE:
0312     case XCB_KEY_PRESS:
0313     case XCB_KEY_RELEASE:
0314     case XCB_MOTION_NOTIFY:
0315         Q_EMIT userActivity();
0316         if (!m_lockWindows.isEmpty()) {
0317             int x = 0;
0318             int y = 0;
0319             if (responseType == XCB_KEY_PRESS || responseType == XCB_KEY_RELEASE) {
0320                 coordFromEvent<xcb_key_press_event_t>(event, &x, &y);
0321             } else if (responseType == XCB_BUTTON_PRESS || responseType == XCB_BUTTON_RELEASE) {
0322                 coordFromEvent<xcb_button_press_event_t>(event, &x, &y);
0323             } else if (responseType == XCB_MOTION_NOTIFY) {
0324                 coordFromEvent<xcb_motion_notify_event_t>(event, &x, &y);
0325             }
0326             Window root_return;
0327             int x_return, y_return;
0328             unsigned int width_return, height_return, border_width_return, depth_return;
0329             for (WId window : qAsConst(m_lockWindows)) {
0330                 if (XGetGeometry(QX11Info::display(),
0331                                  window,
0332                                  &root_return,
0333                                  &x_return,
0334                                  &y_return,
0335                                  &width_return,
0336                                  &height_return,
0337                                  &border_width_return,
0338                                  &depth_return)
0339                     && (x >= x_return && x <= x_return + (int)width_return) && (y >= y_return && y <= y_return + (int)height_return)) {
0340                     // We need to do our own focus handling (see comment in fakeFocusIn).
0341                     // For now: Focus on clicks inside the window
0342                     if (responseType == XCB_BUTTON_PRESS) {
0343                         fakeFocusIn(window);
0344                     }
0345                     const int targetX = x - x_return;
0346                     const int targetY = y - y_return;
0347                     if (responseType == XCB_KEY_PRESS || responseType == XCB_KEY_RELEASE) {
0348                         sendEvent<xcb_key_press_event_t>(event, window, targetX, targetY);
0349                     } else if (responseType == XCB_BUTTON_PRESS || responseType == XCB_BUTTON_RELEASE) {
0350                         sendEvent<xcb_button_press_event_t>(event, window, targetX, targetY);
0351                     } else if (responseType == XCB_MOTION_NOTIFY) {
0352                         sendEvent<xcb_motion_notify_event_t>(event, window, targetX, targetY);
0353                     }
0354                     break;
0355                 }
0356             }
0357             ret = true;
0358         }
0359         break;
0360     case XCB_CONFIGURE_NOTIFY: { // from SubstructureNotifyMask on the root window
0361         xcb_configure_notify_event_t *xc = reinterpret_cast<xcb_configure_notify_event_t *>(event);
0362         if (xc->event == QX11Info::appRootWindow()) {
0363             int index = findWindowInfo(xc->window);
0364             if (index >= 0) {
0365                 int index2 = xc->above_sibling ? findWindowInfo(xc->above_sibling) : 0;
0366                 if (index2 < 0) {
0367                     qCDebug(KSCREENLOCKER) << "Unknown above for ConfigureNotify";
0368                 } else { // move just above the other window
0369                     if (index2 < index) {
0370                         ++index2;
0371                     }
0372                     m_windowInfo.move(index, index2);
0373                 }
0374             } else {
0375                 qCDebug(KSCREENLOCKER) << "Unknown toplevel for ConfigureNotify";
0376             }
0377             // kDebug() << "ConfigureNotify:";
0378             // the stacking order changed, so let's change the stacking order again to what we want
0379             stayOnTop();
0380             ret = true;
0381         }
0382         break;
0383     }
0384     case XCB_MAP_NOTIFY: { // from SubstructureNotifyMask on the root window
0385         xcb_map_notify_event_t *xm = reinterpret_cast<xcb_map_notify_event_t *>(event);
0386         if (xm->event == QX11Info::appRootWindow()) {
0387             qCDebug(KSCREENLOCKER) << "MapNotify:" << xm->window;
0388             int index = findWindowInfo(xm->window);
0389             if (index >= 0) {
0390                 m_windowInfo[index].viewable = true;
0391             } else {
0392                 qCDebug(KSCREENLOCKER) << "Unknown toplevel for MapNotify";
0393             }
0394             if (m_allowedWindows.contains(xm->window)) {
0395                 if (m_lockWindows.contains(xm->window)) {
0396                     qCDebug(KSCREENLOCKER) << "uhoh! duplicate!";
0397                 } else {
0398                     if (!m_background->isVisible()) {
0399                         // not yet shown and we have a lock window, so we show our own window
0400                         m_background->show();
0401                     }
0402                     m_lockWindows.prepend(xm->window);
0403                     fakeFocusIn(xm->window);
0404                 }
0405             }
0406             if (xm->window == m_background->winId()) {
0407                 m_background->update();
0408                 Q_EMIT lockWindowShown();
0409                 return false;
0410             }
0411             stayOnTop();
0412             ret = true;
0413         }
0414         break;
0415     }
0416     case XCB_UNMAP_NOTIFY: {
0417         xcb_unmap_notify_event_t *xu = reinterpret_cast<xcb_unmap_notify_event_t *>(event);
0418         if (xu->event == QX11Info::appRootWindow()) {
0419             qCDebug(KSCREENLOCKER) << "UnmapNotify:" << xu->window;
0420             int index = findWindowInfo(xu->window);
0421             if (index >= 0) {
0422                 m_windowInfo[index].viewable = false;
0423             } else {
0424                 qCDebug(KSCREENLOCKER) << "Unknown toplevel for MapNotify";
0425             }
0426             m_lockWindows.removeAll(xu->window);
0427             if (m_focusedLockWindow == xu->event && !m_lockWindows.empty()) {
0428                 // The currently focused window vanished, just focus the first one in the list
0429                 fakeFocusIn(m_lockWindows[0]);
0430             }
0431             ret = true;
0432         }
0433         break;
0434     }
0435     case XCB_CREATE_NOTIFY: {
0436         xcb_create_notify_event_t *xc = reinterpret_cast<xcb_create_notify_event_t *>(event);
0437         if (xc->parent == QX11Info::appRootWindow()) {
0438             qCDebug(KSCREENLOCKER) << "CreateNotify:" << xc->window;
0439             int index = findWindowInfo(xc->window);
0440             if (index >= 0) {
0441                 qCDebug(KSCREENLOCKER) << "Already existing toplevel for CreateNotify";
0442             } else {
0443                 WindowInfo info;
0444                 info.window = xc->window;
0445                 info.viewable = false;
0446                 m_windowInfo.append(info);
0447             }
0448             ret = true;
0449         }
0450         break;
0451     }
0452     case XCB_DESTROY_NOTIFY: {
0453         xcb_destroy_notify_event_t *xd = reinterpret_cast<xcb_destroy_notify_event_t *>(event);
0454         if (xd->event == QX11Info::appRootWindow()) {
0455             int index = findWindowInfo(xd->window);
0456             if (index >= 0) {
0457                 m_windowInfo.removeAt(index);
0458             } else {
0459                 qCDebug(KSCREENLOCKER) << "Unknown toplevel for DestroyNotify";
0460             }
0461             ret = true;
0462         }
0463         break;
0464     }
0465     case XCB_REPARENT_NOTIFY: {
0466         xcb_reparent_notify_event_t *xr = reinterpret_cast<xcb_reparent_notify_event_t *>(event);
0467         if (xr->event == QX11Info::appRootWindow() && xr->parent != QX11Info::appRootWindow()) {
0468             int index = findWindowInfo(xr->window);
0469             if (index >= 0) {
0470                 m_windowInfo.removeAt(index);
0471             } else {
0472                 qCDebug(KSCREENLOCKER) << "Unknown toplevel for ReparentNotify away";
0473             }
0474         } else if (xr->parent == QX11Info::appRootWindow()) {
0475             int index = findWindowInfo(xr->window);
0476             if (index >= 0) {
0477                 qCDebug(KSCREENLOCKER) << "Already existing toplevel for ReparentNotify";
0478             } else {
0479                 WindowInfo info;
0480                 info.window = xr->window;
0481                 info.viewable = false;
0482                 m_windowInfo.append(info);
0483             }
0484         }
0485         break;
0486     }
0487     case XCB_CIRCULATE_NOTIFY: {
0488         xcb_circulate_notify_event_t *xc = reinterpret_cast<xcb_circulate_notify_event_t *>(event);
0489         if (xc->event == QX11Info::appRootWindow()) {
0490             int index = findWindowInfo(xc->window);
0491             if (index >= 0) {
0492                 m_windowInfo.move(index, xc->place == PlaceOnTop ? m_windowInfo.size() - 1 : 0);
0493             } else {
0494                 qCDebug(KSCREENLOCKER) << "Unknown toplevel for CirculateNotify";
0495             }
0496         }
0497         break;
0498     }
0499     }
0500     return ret;
0501 }
0502 
0503 int X11Locker::findWindowInfo(Window w)
0504 {
0505     for (int i = 0; i < m_windowInfo.size(); ++i) {
0506         if (m_windowInfo[i].window == w) {
0507             return i;
0508         }
0509     }
0510     return -1;
0511 }
0512 
0513 void X11Locker::stayOnTop()
0514 {
0515     // this restacking is written in a way so that
0516     // if the stacking positions actually don't change,
0517     // all restacking operations will be no-op,
0518     // and no ConfigureNotify will be generated,
0519     // thus avoiding possible infinite loops
0520     QVector<Window> stack(m_lockWindows.count() + 1);
0521     int count = 0;
0522     for (WId w : qAsConst(m_lockWindows)) {
0523         stack[count++] = w;
0524     }
0525     // finally, the lock window
0526     stack[count++] = m_background->winId();
0527     // do the actual restacking if needed
0528     XRaiseWindow(QX11Info::display(), stack[0]);
0529     if (count > 1) {
0530         XRestackWindows(QX11Info::display(), stack.data(), count);
0531     }
0532     XFlush(QX11Info::display());
0533 }
0534 
0535 void X11Locker::updateGeo()
0536 {
0537     QRect geometry;
0538     const auto screens = QGuiApplication::screens();
0539     for (auto *screen : screens) {
0540         geometry |= screen->geometry();
0541     }
0542     m_background->setGeometry(geometry);
0543     m_background->update();
0544 }
0545 
0546 void X11Locker::addAllowedWindow(quint32 window)
0547 {
0548     m_allowedWindows << window;
0549     // test whether it's to show
0550     const int index = findWindowInfo(window);
0551     if (index == -1 || !m_windowInfo[index].viewable) {
0552         return;
0553     }
0554     if (m_lockWindows.contains(window)) {
0555         qCDebug(KSCREENLOCKER) << "uhoh! duplicate!";
0556     } else {
0557         if (!m_background->isVisible()) {
0558             // not yet shown and we have a lock window, so we show our own window
0559             m_background->show();
0560         }
0561 
0562         if (m_lockWindows.empty()) {
0563             // Make sure to focus the first window
0564             m_focusedLockWindow = XCB_WINDOW_NONE;
0565             fakeFocusIn(window);
0566         }
0567 
0568         m_lockWindows.prepend(window);
0569         stayOnTop();
0570     }
0571 }
0572 
0573 }