kwin/xwl/drag_x.cpp
David Edmundson 26ce4869a0 [xwl] Fix crash if same data is requested twice
Summary:
m_dataRequests is a list of transfers for a given selection.
It exists primarily as a ref counting mechanism that matches up
transfers with selections.

If the same data is requested twice we would insert two items into the
list with matching timestamps for the original selection. This then
confuses the callback handling.

BUG: 417936

Test Plan:
kwin_wayland + chromium
drag and drop a URL onto desktopview. kwin used to crash here
Now a context menu now appears and I can add a link

Reviewers: #kwin

Subscribers: kwin

Tags: #kwin

Differential Revision: https://phabricator.kde.org/D28630
2020-04-06 23:22:08 +01:00

549 lines
17 KiB
C++

/********************************************************************
KWin - the KDE window manager
This file is part of the KDE project.
Copyright 2019 Roman Gilg <subdiff@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/
#include "drag_x.h"
#include "databridge.h"
#include "dnd.h"
#include "selection_source.h"
#include "xwayland.h"
#include "abstract_client.h"
#include "atoms.h"
#include "wayland_server.h"
#include "workspace.h"
#include <KWayland/Client/datadevice.h>
#include <KWayland/Client/datasource.h>
#include <KWayland/Server/datasource_interface.h>
#include <KWayland/Server/seat_interface.h>
#include <KWayland/Server/surface_interface.h>
#include <QMouseEvent>
#include <QTimer>
namespace KWin
{
namespace Xwl
{
static QStringList atomToMimeTypes(xcb_atom_t atom)
{
QStringList mimeTypes;
if (atom == atoms->utf8_string) {
mimeTypes << QString::fromLatin1("text/plain;charset=utf-8");
} else if (atom == atoms->text) {
mimeTypes << QString::fromLatin1("text/plain");
} else if (atom == atoms->uri_list || atom == atoms->netscape_url || atom == atoms->moz_url) {
// We identify netscape and moz format as less detailed formats text/uri-list,
// text/x-uri and accept the information loss.
mimeTypes << QString::fromLatin1("text/uri-list") << QString::fromLatin1("text/x-uri");
} else {
mimeTypes << Selection::atomName(atom);
}
return mimeTypes;
}
XToWlDrag::XToWlDrag(X11Source *source)
: m_source(source)
{
connect(DataBridge::self()->dnd(), &Dnd::transferFinished, this, [this](xcb_timestamp_t eventTime) {
// we use this mechanism, because the finished call is not
// reliable done by Wayland clients
auto it = std::find_if(m_dataRequests.begin(), m_dataRequests.end(), [eventTime](const QPair<xcb_timestamp_t, bool> &req) {
return req.first == eventTime && req.second == false;
});
if (it == m_dataRequests.end()) {
// transfer finished for a different drag
return;
}
(*it).second = true;
checkForFinished();
});
connect(source, &X11Source::transferReady, this, [this](xcb_atom_t target, qint32 fd) {
Q_UNUSED(target);
Q_UNUSED(fd);
m_dataRequests << QPair<xcb_timestamp_t, bool>(m_source->timestamp(), false);
});
auto *ddm = waylandServer()->internalDataDeviceManager();
m_dataSource = ddm->createDataSource(this);
connect(m_dataSource, &KWayland::Client::DataSource::dragAndDropPerformed, this, [this] {
m_performed = true;
if (m_visit) {
connect(m_visit, &WlVisit::finish, this, [this](WlVisit *visit) {
Q_UNUSED(visit);
checkForFinished();
});
QTimer::singleShot(2000, this, [this]{
if (!m_visit->entered() || !m_visit->dropHandled()) {
// X client timed out
Q_EMIT finish(this);
} else if (m_dataRequests.size() == 0) {
// Wl client timed out
m_visit->sendFinished();
Q_EMIT finish(this);
}
});
}
checkForFinished();
});
connect(m_dataSource, &KWayland::Client::DataSource::dragAndDropFinished, this, [this] {
// this call is not reliably initiated by Wayland clients
checkForFinished();
});
// source does _not_ take ownership of m_dataSource
source->setDataSource(m_dataSource);
auto *dc = new QMetaObject::Connection();
*dc = connect(waylandServer()->dataDeviceManager(), &KWayland::Server::DataDeviceManagerInterface::dataSourceCreated, this,
[this, dc](KWayland::Server::DataSourceInterface *dsi) {
Q_ASSERT(dsi);
if (dsi->client() != waylandServer()->internalConnection()) {
return;
}
QObject::disconnect(*dc);
delete dc;
connect(dsi, &KWayland::Server::DataSourceInterface::mimeTypeOffered, this, &XToWlDrag::offerCallback);
}
);
// Start drag with serial of last left pointer button press.
// This means X to Wl drags can only be executed with the left pointer button being pressed.
// For touch and (maybe) other pointer button drags we have to revisit this.
//
// Until then we accept the restriction for Xwayland clients.
DataBridge::self()->dataDevice()->startDrag(waylandServer()->seat()->pointerButtonSerial(Qt::LeftButton),
m_dataSource,
DataBridge::self()->dnd()->surface());
waylandServer()->dispatch();
}
XToWlDrag::~XToWlDrag()
{
delete m_dataSource;
m_dataSource = nullptr;
}
DragEventReply XToWlDrag::moveFilter(Toplevel *target, const QPoint &pos)
{
Q_UNUSED(pos);
auto *seat = waylandServer()->seat();
if (m_visit && m_visit->target() == target) {
// still same Wl target, wait for X events
return DragEventReply::Ignore;
}
if (m_visit) {
if (m_visit->leave()) {
delete m_visit;
} else {
connect(m_visit, &WlVisit::finish, this, [this](WlVisit *visit) {
m_oldVisits.removeOne(visit);
delete visit;
});
m_oldVisits << m_visit;
}
}
const bool hasCurrent = m_visit;
m_visit = nullptr;
if (!target || !target->surface() ||
target->surface()->client() == waylandServer()->xWaylandConnection()) {
// currently there is no target or target is an Xwayland window
// handled here and by X directly
if (AbstractClient *ac = qobject_cast<AbstractClient*>(target)) {
if (workspace()->activeClient() != ac) {
workspace()->activateClient(ac);
}
}
if (hasCurrent) {
// last received enter event is now void,
// wait for the next one
seat->setDragTarget(nullptr);
}
return DragEventReply::Ignore;
}
// new Wl native target
auto *ac = static_cast<AbstractClient*>(target);
m_visit = new WlVisit(ac, this);
connect(m_visit, &WlVisit::offersReceived, this, &XToWlDrag::setOffers);
return DragEventReply::Ignore;
}
bool XToWlDrag::handleClientMessage(xcb_client_message_event_t *event)
{
for (auto *visit : m_oldVisits) {
if (visit->handleClientMessage(event)) {
return true;
}
}
if (m_visit && m_visit->handleClientMessage(event)) {
return true;
}
return false;
}
void XToWlDrag::setDragAndDropAction(DnDAction action)
{
m_dataSource->setDragAndDropActions(action);
}
DnDAction XToWlDrag::selectedDragAndDropAction()
{
// Take the last received action only from before the drag was performed,
// because the action gets reset as soon as the drag is performed
// (this seems to be a bug in KWayland -> TODO).
if (!m_performed) {
m_lastSelectedDragAndDropAction = m_dataSource->selectedDragAndDropAction();
}
return m_lastSelectedDragAndDropAction;
}
void XToWlDrag::setOffers(const Mimes &offers)
{
m_source->setOffers(offers);
if (offers.isEmpty()) {
// There are no offers, so just directly set the drag target,
// no transfer possible anyways.
setDragTarget();
return;
}
if (m_offers == offers) {
// offers had been set already by a previous visit
// Wl side is already configured
setDragTarget();
return;
}
// TODO: make sure that offers are not changed in between visits
m_offersPending = m_offers = offers;
for (const auto mimePair : offers) {
m_dataSource->offer(mimePair.first);
}
}
using Mime = QPair<QString, xcb_atom_t>;
void XToWlDrag::offerCallback(const QString &mime)
{
m_offersPending.erase(std::remove_if(m_offersPending.begin(), m_offersPending.end(),
[mime](const Mime &m) { return m.first == mime; }));
if (m_offersPending.isEmpty() && m_visit && m_visit->entered()) {
setDragTarget();
}
}
void XToWlDrag::setDragTarget()
{
auto *ac = m_visit->target();
workspace()->activateClient(ac);
waylandServer()->seat()->setDragTarget(ac->surface(), ac->inputTransformation());
}
bool XToWlDrag::checkForFinished()
{
if (!m_visit) {
// not dropped above Wl native target
Q_EMIT finish(this);
return true;
}
if (!m_visit->finished()) {
return false;
}
if (m_dataRequests.size() == 0) {
// need to wait for first data request
return false;
}
const bool transfersFinished = std::all_of(m_dataRequests.begin(), m_dataRequests.end(),
[](QPair<xcb_timestamp_t, bool> req) { return req.second; });
if (transfersFinished) {
m_visit->sendFinished();
Q_EMIT finish(this);
}
return transfersFinished;
}
WlVisit::WlVisit(AbstractClient *target, XToWlDrag *drag)
: QObject(drag),
m_target(target),
m_drag(drag)
{
xcb_connection_t *xcbConn = kwinApp()->x11Connection();
m_window = xcb_generate_id(xcbConn);
DataBridge::self()->dnd()->overwriteRequestorWindow(m_window);
const uint32_t dndValues[] = { XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY |
XCB_EVENT_MASK_PROPERTY_CHANGE };
xcb_create_window(xcbConn,
XCB_COPY_FROM_PARENT,
m_window,
kwinApp()->x11RootWindow(),
0, 0,
8192, 8192, // TODO: get current screen size and connect to changes
0,
XCB_WINDOW_CLASS_INPUT_OUTPUT,
Xwayland::self()->xcbScreen()->root_visual,
XCB_CW_EVENT_MASK,
dndValues);
uint32_t version = Dnd::version();
xcb_change_property(xcbConn,
XCB_PROP_MODE_REPLACE,
m_window,
atoms->xdnd_aware,
XCB_ATOM_ATOM,
32, 1, &version);
xcb_map_window(xcbConn, m_window);
workspace()->addManualOverlay(m_window);
workspace()->updateStackingOrder(true);
xcb_flush(xcbConn);
m_mapped = true;
}
WlVisit::~WlVisit()
{
xcb_connection_t *xcbConn = kwinApp()->x11Connection();
xcb_destroy_window(xcbConn, m_window);
xcb_flush(xcbConn);
}
bool WlVisit::leave()
{
DataBridge::self()->dnd()->overwriteRequestorWindow(XCB_WINDOW_NONE);
unmapProxyWindow();
return m_finished;
}
bool WlVisit::handleClientMessage(xcb_client_message_event_t *event)
{
if (event->window != m_window) {
// different window
return false;
}
if (event->type == atoms->xdnd_enter) {
return handleEnter(event);
} else if (event->type == atoms->xdnd_position) {
return handlePosition(event);
} else if (event->type == atoms->xdnd_drop) {
return handleDrop(event);
} else if (event->type == atoms->xdnd_leave) {
return handleLeave(event);
}
return false;
}
static bool hasMimeName(const Mimes &mimes, const QString &name)
{
return std::any_of(mimes.begin(), mimes.end(),
[name](const Mime &m) { return m.first == name; });
}
bool WlVisit::handleEnter(xcb_client_message_event_t *event)
{
if (m_entered) {
// a drag already entered
return true;
}
m_entered = true;
xcb_client_message_data_t *data = &event->data;
m_srcWindow = data->data32[0];
m_version = data->data32[1] >> 24;
// get types
Mimes offers;
if (!(data->data32[1] & 1)) {
// message has only max 3 types (which are directly in data)
for (size_t i = 0; i < 3; i++) {
xcb_atom_t mimeAtom = data->data32[2 + i];
const auto mimeStrings = atomToMimeTypes(mimeAtom);
for (const auto mime : mimeStrings ) {
if (!hasMimeName(offers, mime)) {
offers << Mime(mime, mimeAtom);
}
}
}
} else {
// more than 3 types -> in window property
getMimesFromWinProperty(offers);
}
Q_EMIT offersReceived(offers);
return true;
}
void WlVisit::getMimesFromWinProperty(Mimes &offers)
{
xcb_connection_t *xcbConn = kwinApp()->x11Connection();
auto cookie = xcb_get_property(xcbConn,
0,
m_srcWindow,
atoms->xdnd_type_list,
XCB_GET_PROPERTY_TYPE_ANY,
0, 0x1fffffff);
auto *reply = xcb_get_property_reply(xcbConn, cookie, nullptr);
if (reply == nullptr) {
return;
}
if (reply->type != XCB_ATOM_ATOM || reply->value_len == 0) {
// invalid reply value
free(reply);
return;
}
xcb_atom_t *mimeAtoms = static_cast<xcb_atom_t *>(xcb_get_property_value(reply));
for (size_t i = 0; i < reply->value_len; ++i) {
const auto mimeStrings = atomToMimeTypes(mimeAtoms[i]);
for (const auto mime : mimeStrings) {
if (!hasMimeName(offers, mime)) {
offers << Mime(mime, mimeAtoms[i]);
}
}
}
free(reply);
}
bool WlVisit::handlePosition(xcb_client_message_event_t *event)
{
xcb_client_message_data_t *data = &event->data;
m_srcWindow = data->data32[0];
if (!m_target) {
// not over Wl window at the moment
m_action = DnDAction::None;
m_actionAtom = XCB_ATOM_NONE;
sendStatus();
return true;
}
const uint32_t pos = data->data32[2];
Q_UNUSED(pos);
const xcb_timestamp_t timestamp = data->data32[3];
m_drag->x11Source()->setTimestamp(timestamp);
xcb_atom_t actionAtom = m_version > 1 ? data->data32[4] :
atoms->xdnd_action_copy;
auto action = Drag::atomToClientAction(actionAtom);
if (action == DnDAction::None) {
// copy action is always possible in XDND
action = DnDAction::Copy;
actionAtom = atoms->xdnd_action_copy;
}
if (m_action != action) {
m_action = action;
m_actionAtom = actionAtom;
m_drag->setDragAndDropAction(m_action);
}
sendStatus();
return true;
}
bool WlVisit::handleDrop(xcb_client_message_event_t *event)
{
m_dropHandled = true;
xcb_client_message_data_t *data = &event->data;
m_srcWindow = data->data32[0];
const xcb_timestamp_t timestamp = data->data32[2];
m_drag->x11Source()->setTimestamp(timestamp);
// we do nothing more here, the drop is being processed
// through the X11Source object
doFinish();
return true;
}
void WlVisit::doFinish()
{
m_finished = true;
unmapProxyWindow();
Q_EMIT finish(this);
}
bool WlVisit::handleLeave(xcb_client_message_event_t *event)
{
m_entered = false;
xcb_client_message_data_t *data = &event->data;
m_srcWindow = data->data32[0];
doFinish();
return true;
}
void WlVisit::sendStatus()
{
// receive position events
uint32_t flags = 1 << 1;
if (targetAcceptsAction()) {
// accept the drop
flags |= (1 << 0);
}
xcb_client_message_data_t data = {0};
data.data32[0] = m_window;
data.data32[1] = flags;
data.data32[4] = flags & (1 << 0) ? m_actionAtom : static_cast<uint32_t>(XCB_ATOM_NONE);
Drag::sendClientMessage(m_srcWindow, atoms->xdnd_status, &data);
}
void WlVisit::sendFinished()
{
const bool accepted = m_entered && m_action != DnDAction::None;
xcb_client_message_data_t data = {0};
data.data32[0] = m_window;
data.data32[1] = accepted;
data.data32[2] = accepted ? m_actionAtom : static_cast<uint32_t>(XCB_ATOM_NONE);
Drag::sendClientMessage(m_srcWindow, atoms->xdnd_finished, &data);
}
bool WlVisit::targetAcceptsAction() const
{
if (m_action == DnDAction::None) {
return false;
}
const auto selAction = m_drag->selectedDragAndDropAction();
return selAction == m_action || selAction == DnDAction::Copy;
}
void WlVisit::unmapProxyWindow()
{
if (!m_mapped) {
return;
}
xcb_connection_t *xcbConn = kwinApp()->x11Connection();
xcb_unmap_window(xcbConn, m_window);
workspace()->removeManualOverlay(m_window);
workspace()->updateStackingOrder(true);
xcb_flush(xcbConn);
m_mapped = false;
}
} // namespace Xwl
} // namespace KWin