mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-10-24 06:04:08 +00:00
Drag and drop protocol: deny requests for drag data made before the user has actually dropped something onto the window
This commit is contained in:
parent
9475a58f0b
commit
026d11f517
4 changed files with 108 additions and 0 deletions
|
|
@ -251,6 +251,8 @@ Detailed list of changes
|
|||
|
||||
- IME preedit text: use underline rather than reverse video (:pull:`10386`)
|
||||
|
||||
- Drag and drop protocol: deny requests for drag data made before the user has actually dropped something onto the window
|
||||
|
||||
|
||||
0.48.2 [2026-07-30]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
|||
|
|
@ -104,6 +104,15 @@ mandatory, terminals must send the full list of MIME types available in
|
|||
the drop. The client program can now request data for the MIME types
|
||||
it is interested in.
|
||||
|
||||
Data may only be requested after a drop has actually occurred, receiving
|
||||
movement events is not sufficient. Terminals must reject any data request
|
||||
received before the drop event with an ``EPERM`` error. Since no drop is in
|
||||
progress, this particular error does not terminate anything. Additionally,
|
||||
when a drag leaves the window without a drop having occurred, terminals must
|
||||
discard all data and resources associated with the drag session, such as
|
||||
previously fetched URI lists, in-progress file transfers and directory
|
||||
handles. Requests referring to them must fail.
|
||||
|
||||
Requesting data is done by sending an escape code of the form::
|
||||
|
||||
OSC _dnd_code ; t=r:x=idx ST
|
||||
|
|
|
|||
33
kitty/dnd.c
33
kitty/dnd.c
|
|
@ -1367,6 +1367,23 @@ drop_enqueue_request(Window *w, int32_t cell_x, int32_t cell_y, int32_t pixel_y,
|
|||
return;
|
||||
}
|
||||
|
||||
if (!w->drop.dropped) {
|
||||
/* The user has not actually dropped anything on this window, so the
|
||||
* client is not allowed to read any drag data. Movement events are
|
||||
* informational only; consent to transfer data is given by the drop. */
|
||||
int32_t saved_x = w->drop.current_request_x;
|
||||
int32_t saved_y = w->drop.current_request_y;
|
||||
int32_t saved_Y = w->drop.current_request_Y;
|
||||
w->drop.current_request_x = cell_x;
|
||||
w->drop.current_request_y = cell_y;
|
||||
w->drop.current_request_Y = pixel_y;
|
||||
drop_send_error(w, EPERM, "drop data can only be requested after a drop");
|
||||
w->drop.current_request_x = saved_x;
|
||||
w->drop.current_request_y = saved_y;
|
||||
w->drop.current_request_Y = saved_Y;
|
||||
return;
|
||||
}
|
||||
|
||||
if (w->drop.num_data_requests >= arraysz(w->drop.data_requests)) {
|
||||
/* Queue full: deny with EMFILE and end the drop */
|
||||
int32_t saved_x = w->drop.current_request_x;
|
||||
|
|
@ -1397,6 +1414,18 @@ drop_left_child(Window *w) {
|
|||
w->drop.hovered = false;
|
||||
w->drop.dropped = false;
|
||||
drop_free_offered_mimes(w);
|
||||
/* The drag session no longer involves this window, so discard everything
|
||||
* obtained from it: otherwise a previously fetched URI list, open
|
||||
* directory handles and an in-flight file transfer would let the client
|
||||
* keep reading files with no drag in progress. */
|
||||
drop_close_file_fd(w);
|
||||
drop_free_request_queue(w);
|
||||
drop_free_dir_handles(w);
|
||||
free(w->drop.uri_list);
|
||||
w->drop.uri_list = NULL;
|
||||
w->drop.uri_list_sz = 0;
|
||||
free(w->drop.getting_data_for_mime);
|
||||
w->drop.getting_data_for_mime = NULL;
|
||||
if (w->drop.wanted) {
|
||||
char buf[128];
|
||||
int header_size = snprintf(buf, sizeof(buf), "\x1b]%d;t=m:x=-1:y=-1", DND_CODE);
|
||||
|
|
@ -2893,6 +2922,10 @@ dnd_test_probe_state(PyObject *self UNUSED, PyObject *args) {
|
|||
return ans;
|
||||
}
|
||||
if (strcmp(q, "drop_getting_data_for_mime") == 0) { return PyUnicode_FromString(w->drop.getting_data_for_mime ? w->drop.getting_data_for_mime : ""); }
|
||||
if (strcmp(q, "drop_dropped") == 0) { return Py_NewRef(w->drop.dropped ? Py_True : Py_False); }
|
||||
if (strcmp(q, "drop_num_dir_handles") == 0) { return PyLong_FromSize_t(w->drop.num_dir_handles); }
|
||||
if (strcmp(q, "drop_uri_list_sz") == 0) { return PyLong_FromSize_t(w->drop.uri_list_sz); }
|
||||
if (strcmp(q, "drop_file_fd_plus_one") == 0) { return PyLong_FromLong((long)w->drop.file_fd_plus_one); }
|
||||
if (strcmp(q, "can_offer") == 0) { return Py_NewRef(w->drag_source.can_offer ? Py_True : Py_False); }
|
||||
if (strcmp(q, "drag_operations") == 0) { return PyLong_FromLong((long)w->drag_source.allowed_operations); }
|
||||
if (strcmp(q, "drag_mimes") == 0) {
|
||||
|
|
|
|||
|
|
@ -696,6 +696,70 @@ class TestDnDProtocol(BaseTest):
|
|||
self.assertEqual(len(r_events), 1, raw)
|
||||
self.ae(r_events[0]['payload'], b'')
|
||||
|
||||
def test_data_request_before_drop_denied(self) -> None:
|
||||
"""Data requests sent while the drag is merely hovering are rejected with EPERM."""
|
||||
with dnd_test_window() as (screen, cap):
|
||||
self._register_for_drops(screen, cap, 'text/plain text/uri-list')
|
||||
dnd_test_set_mouse_pos(cap.window_id, 0, 0, 0, 0)
|
||||
# Only a move event so far, no drop.
|
||||
dnd_test_fake_drop_event(cap.window_id, False, ['text/plain', 'text/uri-list'])
|
||||
cap.consume()
|
||||
|
||||
# All three request forms must be denied before the drop.
|
||||
for req in (client_request_data(1), client_request_uri_data(2, 1), client_dir_read(2, 1)):
|
||||
parse_bytes(screen, req)
|
||||
events = self._get_events(cap)
|
||||
self.assertEqual(len(events), 1, events)
|
||||
self.ae(events[0]['type'], 'R')
|
||||
self.ae(events[0]['payload'].strip().partition(b':')[0], b'EPERM')
|
||||
|
||||
# The denial must not have terminated the drag session: after an
|
||||
# actual drop, requests work as usual.
|
||||
dnd_test_fake_drop_event(cap.window_id, True, ['text/plain', 'text/uri-list'])
|
||||
cap.consume()
|
||||
parse_bytes(screen, client_request_data(1))
|
||||
dnd_test_fake_drop_data(cap.window_id, 'text/plain', b'hello')
|
||||
combined = b''.join(e['payload'] for e in parse_escape_codes_b64(cap.consume()) if e['type'] == 'r')
|
||||
self.ae(combined, b'hello')
|
||||
|
||||
def test_drag_leave_discards_drag_session_data(self) -> None:
|
||||
"""When the drag leaves the window all data obtained from it is discarded."""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
with open(os.path.join(root, 'secret.txt'), 'wb') as f:
|
||||
f.write(b'secret data')
|
||||
uri_list = f'file://{root}\r\n'.encode()
|
||||
with dnd_test_window() as (screen, cap):
|
||||
self._setup_uri_drop(screen, cap, uri_list)
|
||||
# Get a directory handle for root.
|
||||
parse_bytes(screen, client_request_uri_data(2, 1))
|
||||
events = parse_escape_codes_b64(cap.consume())
|
||||
d_events = [e for e in events if e['type'] == 'r' and is_dir_event(e)]
|
||||
self.assertTrue(d_events, 'expected directory listing for root')
|
||||
handle_id = dir_handle(d_events[0])
|
||||
self.assertGreater(dnd_test_probe_state(cap.window_id, 'drop_num_dir_handles'), 0)
|
||||
self.assertGreater(dnd_test_probe_state(cap.window_id, 'drop_uri_list_sz'), 0)
|
||||
|
||||
# Drag leaves the window.
|
||||
dnd_test_fake_drop_event(cap.window_id, False, None)
|
||||
cap.consume() # discard the leave event
|
||||
|
||||
# The URI list, directory handles and any in-flight file
|
||||
# transfer must be gone.
|
||||
self.ae(dnd_test_probe_state(cap.window_id, 'drop_num_dir_handles'), 0)
|
||||
self.ae(dnd_test_probe_state(cap.window_id, 'drop_uri_list_sz'), 0)
|
||||
self.ae(dnd_test_probe_state(cap.window_id, 'drop_file_fd_plus_one'), 0)
|
||||
self.assertFalse(dnd_test_probe_state(cap.window_id, 'drop_dropped'))
|
||||
|
||||
# Requests using the stale handle must fail.
|
||||
parse_bytes(screen, client_dir_read(handle_id, 1))
|
||||
events = self._get_events(cap)
|
||||
self.assertEqual(len(events), 1, events)
|
||||
self.ae(events[0]['type'], 'R')
|
||||
self.ae(events[0]['payload'].strip().partition(b':')[0], b'EPERM')
|
||||
|
||||
# ---- remote file/directory transfer tests ----------------
|
||||
|
||||
def _setup_uri_drop(self, screen, cap, uri_list_data: bytes, mimes=None):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue