FreeRDP
Loading...
Searching...
No Matches
sdl_clip.cpp
1
21#include <memory>
22#include <string>
23#include <sstream>
24#include <mutex>
25#include <iterator>
26#include <algorithm>
27#include <utility>
28#include <cstring>
29
30#include <winpr/wlog.h>
31#include <winpr/image.h>
32
33#include <string_view>
34#include "sdl_clip.hpp"
35#include "sdl_context.hpp"
36
37#if defined(__EMSCRIPTEN__)
38#include <mutex>
39class sdlClip;
40/* The clipboard the page talks to. s_web_lock keeps uninit() from tearing it down while an
41 * offer from the page is being sent. */
42static std::mutex s_web_lock;
43static sdlClip* s_web_clip = nullptr;
44#include <emscripten.h>
45#include <emscripten/proxying.h>
46#include <emscripten/threading.h>
47#include <winpr/string.h>
48#include <pthread.h>
49#endif
50
51#define TAG CLIENT_TAG("sdl.cliprdr")
52
53#define mime_text_plain "text/plain"
54const char mime_text_utf8[] = "text/plain;charset=utf-8";
55
56static const char* first_announcement[] = {
57 "sdl-clipboard-monitor-ready-4b8c7d9b-6d5c-4dcc-add1-21573ca564b0"
58};
59
60[[nodiscard]] static const std::vector<const char*>& s_mime_text()
61{
62 static std::vector<const char*> values;
63 if (values.empty())
64 {
65 values = std::vector<const char*>(
66 { mime_text_utf8, "UTF8_STRING", mime_text_plain, "COMPOUND_TEXT", "TEXT", "STRING" });
67 }
68 return values;
69}
70
71static const char s_mime_jxl[] = "image/jxl";
72static const char s_mime_avif[] = "image/avif";
73static const char s_mime_png[] = "image/png";
74static const char s_mime_webp[] = "image/webp";
75static const char s_mime_jpg[] = "image/jpeg";
76
77static const char s_mime_tiff[] = "image/tiff";
78static const char s_mime_uri_list[] = "text/uri-list";
79/* SDL reports the native pasteboard types on macOS, Finder files have no MIME type */
80static const char s_type_file_url[] = "public.file-url";
81static const char s_mime_html[] = "text/html";
82
83#define BMP_MIME_LIST "image/bmp", "image/x-bmp", "image/x-MS-bmp", "image/x-win-bitmap"
84
85[[nodiscard]] static const std::vector<const char*>& s_mime_bitmap()
86{
87 static std::vector<const char*> values;
88 if (values.empty())
89 {
90 values = std::vector<const char*>({ BMP_MIME_LIST });
91 }
92 return values;
93}
94
95[[nodiscard]] static const std::vector<const char*>& s_mime_image()
96{
97 static std::vector<const char*> values;
98 if (values.empty())
99 {
100 if (winpr_image_format_is_supported(WINPR_IMAGE_WEBP))
101 values.push_back(s_mime_webp);
102
103 if (winpr_image_format_is_supported(WINPR_IMAGE_PNG))
104 values.push_back(s_mime_png);
105
106 if (winpr_image_format_is_supported(WINPR_IMAGE_JPEG))
107 values.push_back(s_mime_jpg);
108
109 auto bmp = std::vector<const char*>({ BMP_MIME_LIST });
110 values.insert(values.end(), bmp.begin(), bmp.end());
111 }
112 return values;
113}
114
115/* Local image types winpr can turn into a CF_DIB, in order of preference: BMP only needs its
116 * file header stripped, the others need the matching winpr image support compiled in. */
117[[nodiscard]] static const std::vector<const char*>& s_mime_dib_sources()
118{
119 static const std::vector<const char*> values = []()
120 {
121 std::vector<const char*> v({ BMP_MIME_LIST });
122 if (winpr_image_format_is_supported(WINPR_IMAGE_PNG))
123 v.push_back(s_mime_png);
124 if (winpr_image_format_is_supported(WINPR_IMAGE_WEBP))
125 v.push_back(s_mime_webp);
126 if (winpr_image_format_is_supported(WINPR_IMAGE_JPEG))
127 v.push_back(s_mime_jpg);
128 return v;
129 }();
130 return values;
131}
132
133/* Local image types an "HTML Format" request may be answered from */
134[[nodiscard]] static const std::vector<const char*>& s_mime_html_image_sources()
135{
136 static const std::vector<const char*> values({ s_mime_jpg, s_mime_png, s_mime_webp, s_mime_avif,
137 s_mime_jxl, s_mime_tiff, BMP_MIME_LIST });
138 return values;
139}
140
141/* The mime types SDL has cached for the current clipboard owner. Unlike SDL_HasClipboardData
142 * this does no clipboard I/O: on X11, SDL_HasClipboardData transfers the data, and calls our
143 * own ClipDataCb when we are the owner. */
144[[nodiscard]] static std::vector<std::string> localMimeTypes()
145{
146 std::vector<std::string> list;
147 size_t count = 0;
148 auto mimes = SDL_GetClipboardMimeTypes(&count);
149 for (size_t x = 0; mimes && (x < count); x++)
150 {
151 if (mimes[x])
152 list.emplace_back(mimes[x]);
153 }
154 SDL_free(static_cast<void*>(mimes));
155 return list;
156}
157
158[[nodiscard]] static bool hasMime(const std::vector<std::string>& list, const char* mime)
159{
160 return std::find(list.begin(), list.end(), mime) != list.end();
161}
162
163[[nodiscard]] static const char* firstOffered(const std::vector<std::string>& offered,
164 const std::vector<const char*>& candidates)
165{
166 for (const auto& m : candidates)
167 {
168 if (hasMime(offered, m))
169 return m;
170 }
171 return nullptr;
172}
173
174/* WinPR synthesizes text with its terminating '\0' included in the size, as the RDP
175 * wire formats require. Local applications expect the bare string, so cut it at the
176 * first '\0' (as the X11 client does). */
177[[nodiscard]] static uint32_t textLength(const void* data, uint32_t size)
178{
179 const auto end = memchr(data, '\0', size);
180 if (!end)
181 return size;
182 return WINPR_ASSERTING_INT_CAST(uint32_t,
183 static_cast<const char*>(end) - static_cast<const char*>(data));
184}
185
186static const char s_mime_gnome_copied_files[] = "x-special/gnome-copied-files";
187static const char s_mime_mate_copied_files[] = "x-special/mate-copied-files";
188
189static const char s_mime_freerdp_update[] = "x-special/freerdp-clipboard-update";
190
191static const char* s_type_HtmlFormat = "HTML Format";
192static const char* s_type_FileGroupDescriptorW = "FileGroupDescriptorW";
193
194class ClipboardLockGuard
195{
196 public:
197 explicit ClipboardLockGuard(wClipboard* clipboard) : _clipboard(clipboard)
198 {
199 ClipboardLock(_clipboard);
200 }
201 ClipboardLockGuard(const ClipboardLockGuard& other) = delete;
202 ClipboardLockGuard(ClipboardLockGuard&& other) = delete;
203
204 ClipboardLockGuard& operator=(const ClipboardLockGuard& rhs) = delete;
205 ClipboardLockGuard& operator=(ClipboardLockGuard&& rhs) = delete;
206
207 ~ClipboardLockGuard()
208 {
209 ClipboardUnlock(_clipboard);
210 }
211
212 private:
213 wClipboard* _clipboard;
214};
215
216static bool operator<(const CLIPRDR_FORMAT& lhs, const CLIPRDR_FORMAT& rhs)
217{
218 return (lhs.formatId < rhs.formatId);
219}
220
221static bool operator==(const CLIPRDR_FORMAT& lhs, const CLIPRDR_FORMAT& rhs)
222{
223 return (lhs.formatId == rhs.formatId);
224}
225
226sdlClip::sdlClip(SdlContext* sdl)
227 : _sdl(sdl), _file(cliprdr_file_context_new(this)), _log(WLog_Get(TAG)),
228 _system(ClipboardCreate()), _event(CreateEventA(nullptr, TRUE, FALSE, nullptr)),
229 _uuid(sdl::utils::generate_uuid_v4())
230{
231 WINPR_ASSERT(sdl);
232
233 std::stringstream ss;
234 ss << s_mime_freerdp_update << "-" << _uuid;
235 _mime_uuid = ss.str();
236
237 std::ignore = cliprdr_file_context_set_locally_available(_file, TRUE);
238}
239
240sdlClip::~sdlClip()
241{
242#if defined(__EMSCRIPTEN__)
243 {
244 std::scoped_lock web(s_web_lock);
245 if (s_web_clip == this)
246 s_web_clip = nullptr;
247 }
248#endif
249 cliprdr_file_context_free(_file);
250 ClipboardDestroy(_system);
251 std::ignore = CloseHandle(_event);
252}
253
254bool sdlClip::init(CliprdrClientContext* clip)
255{
256 WINPR_ASSERT(clip);
257 ClipboardLockGuard systemlock(_system);
258 std::scoped_lock lock(_lock);
259 _ctx = clip;
260 clip->custom = this;
261 _ctx->MonitorReady = sdlClip::MonitorReady;
262 _ctx->ServerCapabilities = sdlClip::ReceiveServerCapabilities;
263 _ctx->ServerFormatList = sdlClip::ReceiveServerFormatList;
264 _ctx->ServerFormatListResponse = sdlClip::ReceiveFormatListResponse;
265 _ctx->ServerFormatDataRequest = sdlClip::ReceiveFormatDataRequest;
266 _ctx->ServerFormatDataResponse = sdlClip::ReceiveFormatDataResponse;
267#if defined(__EMSCRIPTEN__)
268 {
269 std::scoped_lock web(s_web_lock);
270 s_web_clip = this;
271 }
272#endif
273
274 return cliprdr_file_context_init(_file, _ctx);
275}
276
277bool sdlClip::uninit(CliprdrClientContext* clip)
278{
279 WINPR_ASSERT(clip);
280#if defined(__EMSCRIPTEN__)
281 {
282 std::scoped_lock web(s_web_lock);
283 if (s_web_clip == this)
284 s_web_clip = nullptr;
285 }
286#endif
287 ClipboardLockGuard systemlock(_system);
288 std::scoped_lock lock(_lock);
289 if (!cliprdr_file_context_uninit(_file, _ctx))
290 return false;
291 _ctx = nullptr;
292 clip->custom = nullptr;
293 _server_requests.clear();
294 return true;
295}
296
297bool sdlClip::contains(const char** mime_types, Sint32 count)
298{
299 for (Sint32 x = 0; x < count; x++)
300 {
301 const auto mime = mime_types[x];
302 if (mime && (strcmp(_mime_uuid.c_str(), mime) == 0))
303 return true;
304 }
305 return false;
306}
307
308bool sdlClip::ownsClipboard() const
309{
310 return hasMime(localMimeTypes(), _mime_uuid.c_str());
311}
312
313bool sdlClip::handleEvent(const SDL_ClipboardEvent& ev)
314{
315 std::unique_ptr<char*, decltype(&SDL_free)> current(nullptr, SDL_free);
316 const char** mime_types = ev.mime_types;
317 size_t nformats = WINPR_ASSERTING_INT_CAST(size_t, ev.num_mime_types);
318 bool connected = false;
319 {
320 std::scoped_lock lock(_lock);
321 connected = (_ctx != nullptr);
322 }
323 if (connected && _ctx && _sync && ev.owner && (ev.num_mime_types == 1))
324 {
325 if (strcmp(first_announcement[0], ev.mime_types[0]) == 0)
326 {
327 current.reset(SDL_GetClipboardMimeTypes(&nformats));
328 mime_types = const_cast<const char**>(current.get());
329 if (!mime_types)
330 nformats = 0;
331 }
332 }
333 else if (!connected || !_ctx || !_sync || ev.owner)
334 {
335 _last_timestamp = ev.timestamp;
336
337 /* The channel thread replaces _current_mimetypes on every server format list, so take
338 * them under the lock instead of trusting pointers carried in the event. */
339 std::vector<std::string> mimes;
340 {
341 ClipboardLockGuard systemlock(_system);
342 std::scoped_lock lock(_lock);
343 mimes.swap(_current_mimetypes);
344 if (!mimes.empty())
345 _cache_data.clear();
346 }
347 if (mimes.empty())
348 return true;
349
350 std::vector<const char*> cmimes;
351 cmimes.reserve(mimes.size());
352 for (const auto& m : mimes)
353 cmimes.push_back(m.c_str());
354
355 /* Server files need native clipboard items on some platforms (macOS), which only the
356 * file context can offer. It requests the data with ClipDataCb like SDL does. */
357 if (cliprdr_file_context_set_local_clipboard_data(_file, sdlClip::ClipDataCb, this,
358 cmimes.data(), cmimes.size()))
359 return true;
360
361 /* SDL copies the mime type strings */
362 return SDL_SetClipboardData(sdlClip::ClipDataCb, sdlClip::ClipCleanCb, this, cmimes.data(),
363 cmimes.size());
364 }
365
366 if (ev.timestamp == _last_timestamp)
367 return true;
368
369 if (contains(mime_types, WINPR_ASSERTING_INT_CAST(Sint32, nformats)))
370 return true;
371
372 /* SDL holds this session's server data right now, so this update was queued before our
373 * later SDL_SetClipboardData took effect: typically the selection a compositor re-sends on
374 * focus-in, racing a server format list. Announcing it would replace the fresh server
375 * clipboard with the stale local one (and the server would then ask us for data we no
376 * longer own). A genuine local change cancels our data before its update is queued. */
377 if (ownsClipboard())
378 {
379 WLog_Print(_log, WLOG_DEBUG, "ignoring stale local clipboard update");
380 return true;
381 }
382
383 {
384 ClipboardLockGuard systemlock(_system);
385 std::scoped_lock lock(_lock);
386 clearServerFormats();
387 }
388
389 const std::string mime_html = s_mime_html;
390
391 const std::vector<std::string> mime_bitmap = { BMP_MIME_LIST };
392 const std::string mime_webp = s_mime_webp;
393 const std::string mime_png = s_mime_png;
394 const std::string mime_jpeg = s_mime_jpg;
395 const std::string mime_tiff = s_mime_tiff;
396 const std::vector<std::string> mime_images = { mime_webp, mime_png, mime_jpeg,
397 mime_tiff, s_mime_avif, s_mime_jxl };
398
399 std::vector<std::string> clientFormatNames;
400 std::vector<CLIPRDR_FORMAT> clientFormats;
401
402 const char** clipboard_mime_formats = mime_types;
403
404 WLog_Print(_log, WLOG_TRACE, "SDL has %" PRIuz " formats", nformats);
405
406 bool textPushed = false;
407 bool imgPushed = false;
408 bool filePushed = false;
409
410 for (size_t i = 0; i < nformats; i++)
411 {
412 std::string local_mime = clipboard_mime_formats[i];
413 WLog_Print(_log, WLOG_TRACE, " - %s", local_mime.c_str());
414
415 if (std::find(s_mime_text().begin(), s_mime_text().end(), local_mime) !=
416 s_mime_text().end())
417 {
418 /* text formats */
419 if (!textPushed)
420 {
421 clientFormats.push_back({ CF_TEXT, nullptr });
422 clientFormats.push_back({ CF_OEMTEXT, nullptr });
423 clientFormats.push_back({ CF_UNICODETEXT, nullptr });
424 textPushed = true;
425 }
426 }
427 else if (local_mime == mime_html)
428 /* html */
429 clientFormatNames.emplace_back(s_type_HtmlFormat);
430 else if ((std::find(mime_bitmap.begin(), mime_bitmap.end(), local_mime) !=
431 mime_bitmap.end()) ||
432 (std::find(mime_images.begin(), mime_images.end(), local_mime) !=
433 mime_images.end()))
434 {
435 /* image formats */
436 if (!imgPushed)
437 {
438 clientFormats.push_back({ CF_DIB, nullptr });
439#if defined(WINPR_UTILS_IMAGE_DIBv5)
440 clientFormats.push_back({ CF_DIBV5, nullptr });
441#endif
442
443 if (winpr_image_format_is_supported(WINPR_IMAGE_BITMAP))
444 {
445 for (auto& bmp : mime_bitmap)
446 clientFormatNames.emplace_back(bmp);
447 }
448
449 if (winpr_image_format_is_supported(WINPR_IMAGE_JPEG))
450 clientFormatNames.emplace_back(mime_jpeg);
451 if (winpr_image_format_is_supported(WINPR_IMAGE_WEBP))
452 clientFormatNames.emplace_back(mime_webp);
453 if (winpr_image_format_is_supported(WINPR_IMAGE_PNG))
454 clientFormatNames.emplace_back(mime_png);
455
456 clientFormatNames.emplace_back(s_type_HtmlFormat);
457 imgPushed = true;
458 }
459 clientFormatNames.emplace_back(local_mime);
460 }
461 else if (mime_is_file(local_mime) || (local_mime == s_type_file_url))
462 {
463 if (!filePushed)
464 {
465 clientFormatNames.emplace_back(s_type_FileGroupDescriptorW);
466 filePushed = true;
467 }
468 }
469 }
470
471 std::sort(clientFormatNames.begin(), clientFormatNames.end());
472 clientFormatNames.erase(std::unique(clientFormatNames.begin(), clientFormatNames.end()),
473 clientFormatNames.end());
474
475 for (auto& name : clientFormatNames)
476 {
477 clientFormats.push_back({ ClipboardRegisterFormat(_system, name.c_str()), name.data() });
478 }
479
480 std::sort(clientFormats.begin(), clientFormats.end(),
481 [](const auto& a, const auto& b) { return a < b; });
482 auto u = std::unique(clientFormats.begin(), clientFormats.end());
483 clientFormats.erase(u, clientFormats.end());
484
485 const CLIPRDR_FORMAT_LIST formatList = {
486 { CB_FORMAT_LIST, 0, 0 },
487 static_cast<UINT32>(clientFormats.size()),
488 clientFormats.data(),
489 };
490
491 WLog_Print(_log, WLOG_TRACE,
492 "-------------- client format list [%" PRIu32 "] ------------------",
493 formatList.numFormats);
494 for (UINT32 x = 0; x < formatList.numFormats; x++)
495 {
496 auto format = &formatList.formats[x];
497 WLog_Print(_log, WLOG_TRACE, "client announces %" PRIu32 " [%s][%s]", format->formatId,
498 ClipboardGetFormatIdString(format->formatId), format->formatName);
499 }
500
501 /* The RDP thread may have closed the channel since the check above (see uninit) */
502 ClipboardLockGuard systemlock(_system);
503 std::scoped_lock lock(_lock);
504 if (!_ctx)
505 return true;
506
507 if (cliprdr_file_context_notify_new_client_format_list(_file) != CHANNEL_RC_OK)
508 return false;
509
510 WINPR_ASSERT(_ctx);
511 WINPR_ASSERT(_ctx->ClientFormatList);
512 return _ctx->ClientFormatList(_ctx, &formatList) == CHANNEL_RC_OK;
513}
514
515UINT sdlClip::MonitorReady(CliprdrClientContext* context, const CLIPRDR_MONITOR_READY* monitorReady)
516{
517 WINPR_UNUSED(monitorReady);
518 WINPR_ASSERT(context);
519 WINPR_ASSERT(monitorReady);
520
521 auto clipboard = static_cast<sdlClip*>(
522 cliprdr_file_context_get_context(static_cast<CliprdrFileContext*>(context->custom)));
523 WINPR_ASSERT(clipboard);
524
525 auto ret = clipboard->SendClientCapabilities();
526 if (ret != CHANNEL_RC_OK)
527 return ret;
528
529 clipboard->_sync = true;
530 SDL_Event ev = {};
531 ev.clipboard.type = SDL_EVENT_CLIPBOARD_UPDATE;
532 ev.clipboard.owner = true;
533 ev.clipboard.timestamp = SDL_GetTicksNS();
534 ev.clipboard.num_mime_types = ARRAYSIZE(first_announcement);
535 ev.clipboard.mime_types = first_announcement;
536
537 auto rc = SDL_PushEvent(&ev);
538 if (rc != 1)
539 return ERROR_INTERNAL_ERROR;
540
541 return CHANNEL_RC_OK;
542}
543
544UINT sdlClip::SendClientCapabilities()
545{
546 CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet = {
547 CB_CAPSTYPE_GENERAL, 12, CB_CAPS_VERSION_2,
548 CB_USE_LONG_FORMAT_NAMES | cliprdr_file_context_current_flags(_file)
549 };
550 const CLIPRDR_CAPABILITIES capabilities = {
551 { CB_TYPE_NONE, 0, 0 }, 1, reinterpret_cast<CLIPRDR_CAPABILITY_SET*>(&generalCapabilitySet)
552 };
553
554 WINPR_ASSERT(_ctx);
555 WINPR_ASSERT(_ctx->ClientCapabilities);
556 return _ctx->ClientCapabilities(_ctx, &capabilities);
557}
558
559void sdlClip::clearServerFormats()
560{
561 {
562 ClipboardLockGuard systemlock(_system);
563 std::scoped_lock lock(_lock);
564 _serverFormats.clear();
565 _cache_data.clear();
566 /* The WinPR clipboard may still hold local data converted for an earlier server request.
567 * ClipDataCb() would hand that out instead of requesting the new server data. */
568 ClipboardEmpty(_system);
569 }
570 cliprdr_file_context_clear(_file);
571}
572
573UINT sdlClip::SendFormatListResponse(BOOL status)
574{
575 const CLIPRDR_FORMAT_LIST_RESPONSE formatListResponse = {
576 { CB_FORMAT_LIST_RESPONSE, static_cast<UINT16>(status ? CB_RESPONSE_OK : CB_RESPONSE_FAIL),
577 0 }
578 };
579 WINPR_ASSERT(_ctx);
580 WINPR_ASSERT(_ctx->ClientFormatListResponse);
581 return _ctx->ClientFormatListResponse(_ctx, &formatListResponse);
582}
583
584UINT sdlClip::SendDataResponse(const BYTE* data, size_t size)
585{
586 CLIPRDR_FORMAT_DATA_RESPONSE response = {};
587
588 if (size > UINT32_MAX)
589 return ERROR_INVALID_PARAMETER;
590
591 response.common.msgFlags = (data) ? CB_RESPONSE_OK : CB_RESPONSE_FAIL;
592 response.common.dataLen = static_cast<UINT32>(size);
593 response.requestedFormatData = data;
594
595 WINPR_ASSERT(_ctx);
596 WINPR_ASSERT(_ctx->ClientFormatDataResponse);
597 return _ctx->ClientFormatDataResponse(_ctx, &response);
598}
599
600UINT sdlClip::SendDataRequest(uint32_t formatID, const std::string& mime)
601{
602 const CLIPRDR_FORMAT_DATA_REQUEST request = { { CB_TYPE_NONE, 0, 0 }, formatID };
603
604 _request_queue.emplace(formatID, mime);
605
606 WINPR_ASSERT(_ctx);
607 WINPR_ASSERT(_ctx->ClientFormatDataRequest);
608 UINT ret = _ctx->ClientFormatDataRequest(_ctx, &request);
609 if (ret != CHANNEL_RC_OK)
610 {
611 WLog_Print(_log, WLOG_ERROR, "error sending ClientFormatDataRequest, cancelling request");
612 _request_queue.pop();
613 }
614
615 return ret;
616}
617
618std::string sdlClip::getServerFormat(uint32_t id)
619{
620 for (auto& fmt : _serverFormats)
621 {
622 if (fmt.formatId() == id)
623 {
624 if (fmt.formatName())
625 return fmt.formatName();
626 break;
627 }
628 }
629
630 return "";
631}
632
633uint32_t sdlClip::serverIdForMime(const std::string& mime)
634{
635 std::string cmp = mime;
636 if (mime_is_html(mime))
637 cmp = s_type_HtmlFormat;
638 if (mime_is_file(mime))
639 cmp = s_type_FileGroupDescriptorW;
640
641 for (auto& format : _serverFormats)
642 {
643 if (!format.formatName())
644 continue;
645 if (cmp == format.formatName())
646 return format.formatId();
647 }
648
649 if (mime_is_image(mime) || mime_is_bmp(mime))
650 {
651 /* not offered under this name: CF_DIB, or any other image format of the server; the
652 * WinPR clipboard converts it */
653 uint32_t named = 0;
654 for (auto& format : _serverFormats)
655 {
656 if ((format.formatId() == CF_DIB) || (format.formatId() == CF_DIBV5))
657 return format.formatId();
658 if (!named && format.formatName() &&
659 (mime_is_image(format.formatName()) || mime_is_bmp(format.formatName())))
660 named = format.formatId();
661 }
662 return named ? named : CF_DIB;
663 }
664 if (mime_is_text(mime))
665 return CF_UNICODETEXT;
666
667 return 0;
668}
669
670UINT sdlClip::ReceiveServerCapabilities(CliprdrClientContext* context,
671 const CLIPRDR_CAPABILITIES* capabilities)
672{
673 WINPR_ASSERT(context);
674 WINPR_ASSERT(capabilities);
675
676 auto capsPtr = reinterpret_cast<const BYTE*>(capabilities->capabilitySets);
677 WINPR_ASSERT(capsPtr);
678
679 auto clipboard = static_cast<sdlClip*>(
680 cliprdr_file_context_get_context(static_cast<CliprdrFileContext*>(context->custom)));
681 WINPR_ASSERT(clipboard);
682
683 if (!cliprdr_file_context_remote_set_flags(clipboard->_file, 0))
684 return ERROR_INTERNAL_ERROR;
685
686 for (UINT32 i = 0; i < capabilities->cCapabilitiesSets; i++)
687 {
688 auto caps = reinterpret_cast<const CLIPRDR_CAPABILITY_SET*>(capsPtr);
689
690 if (caps->capabilitySetType == CB_CAPSTYPE_GENERAL)
691 {
692 auto generalCaps = reinterpret_cast<const CLIPRDR_GENERAL_CAPABILITY_SET*>(caps);
693
694 if (!cliprdr_file_context_remote_set_flags(clipboard->_file, generalCaps->generalFlags))
695 return ERROR_INTERNAL_ERROR;
696 }
697
698 capsPtr += caps->capabilitySetLength;
699 }
700
701 return CHANNEL_RC_OK;
702}
703
704UINT sdlClip::ReceiveServerFormatList(CliprdrClientContext* context,
705 const CLIPRDR_FORMAT_LIST* formatList)
706{
707 BOOL html = FALSE;
708 BOOL text = FALSE;
709 BOOL image = FALSE;
710 BOOL file = FALSE;
711
712 if (!context || !context->custom)
713 return ERROR_INVALID_PARAMETER;
714
715 auto filecontext = static_cast<CliprdrFileContext*>(context->custom);
716 auto clipboard = static_cast<sdlClip*>(cliprdr_file_context_get_context(filecontext));
717 WINPR_ASSERT(clipboard);
718
719 ClipboardLockGuard systemlock(clipboard->_system);
720 std::scoped_lock lock(clipboard->_lock);
721
722 clipboard->clearServerFormats();
723 /* _system may still hold local clipboard data converted for an earlier server request.
724 * ClipDataCb converts from _system before asking the server, so left in place it would
725 * answer the next local paste with that stale local data instead of the new server data. */
726 ClipboardEmpty(clipboard->_system);
727
728 for (UINT32 i = 0; i < formatList->numFormats; i++)
729 {
730 const CLIPRDR_FORMAT* format = &formatList->formats[i];
731
732 clipboard->_serverFormats.emplace_back(format->formatId, format->formatName);
733
734 if (format->formatName)
735 {
736 if (strcmp(format->formatName, s_type_HtmlFormat) == 0)
737 {
738 text = TRUE;
739 html = TRUE;
740 }
741 else if (strcmp(format->formatName, s_type_FileGroupDescriptorW) == 0)
742 {
743 file = TRUE;
744 text = TRUE;
745 }
746 else if (mime_is_image(format->formatName) || mime_is_bmp(format->formatName))
747 {
748 /* e.g. GNOME Remote Desktop offers image/png alone for PNG-only sources */
749 image = TRUE;
750 }
751 }
752 else
753 {
754 switch (format->formatId)
755 {
756 case CF_TEXT:
757 case CF_OEMTEXT:
758 case CF_UNICODETEXT:
759 text = TRUE;
760 break;
761
762 case CF_DIB:
763 image = TRUE;
764 break;
765
766 default:
767 break;
768 }
769 }
770 }
771
772 clipboard->_current_mimetypes.clear();
773
774 {
775 auto res = cliprdr_file_context_notify_new_server_format_list(filecontext);
776 if (res != CHANNEL_RC_OK)
777 return res;
778 }
779
780 if (text)
781 {
782 clipboard->_current_mimetypes.insert(clipboard->_current_mimetypes.end(),
783 s_mime_text().begin(), s_mime_text().end());
784 }
785 if (image)
786 {
787 clipboard->_current_mimetypes.insert(clipboard->_current_mimetypes.end(),
788 s_mime_bitmap().begin(), s_mime_bitmap().end());
789 clipboard->_current_mimetypes.insert(clipboard->_current_mimetypes.end(),
790 s_mime_image().begin(), s_mime_image().end());
791 }
792 if (html)
793 {
794 clipboard->_current_mimetypes.emplace_back(s_mime_html);
795 }
796 if (file)
797 {
798 clipboard->_current_mimetypes.emplace_back(s_mime_uri_list);
799 clipboard->_current_mimetypes.emplace_back(s_mime_gnome_copied_files);
800 clipboard->_current_mimetypes.emplace_back(s_mime_mate_copied_files);
801 }
802 clipboard->_current_mimetypes.emplace_back(clipboard->_mime_uuid.c_str());
803
804 auto& mime = clipboard->_current_mimetypes;
805 std::sort(mime.begin(), mime.end());
806 mime.erase(std::unique(mime.begin(), mime.end()), mime.end());
807
808 WLog_Print(clipboard->_log, WLOG_TRACE,
809 "-------------- server mime types [%" PRIuz "] ------------------", mime.size());
810 for (const auto& m : mime)
811 {
812 WLog_Print(clipboard->_log, WLOG_TRACE, "server announces %s]", m.c_str());
813 }
814
815 /* handleEvent picks the mime types up from _current_mimetypes. Set every field: an
816 * initializer list only initializes the union's first member (type). */
817 SDL_Event ev = {};
818 ev.clipboard.type = SDL_EVENT_CLIPBOARD_UPDATE;
819 ev.clipboard.owner = true;
820 ev.clipboard.timestamp = SDL_GetTicksNS();
821 ev.clipboard.num_mime_types = 0;
822 ev.clipboard.mime_types = nullptr;
823
824 auto rc = (SDL_PushEvent(&ev) == 1);
825 const UINT ret = clipboard->SendFormatListResponse(rc);
826#if defined(__EMSCRIPTEN__)
827 if (ret == CHANNEL_RC_OK)
828 std::ignore = clipboard->webRequestServerText();
829#endif
830 return ret;
831}
832
833UINT sdlClip::ReceiveFormatListResponse(WINPR_ATTR_UNUSED CliprdrClientContext* context,
834 const CLIPRDR_FORMAT_LIST_RESPONSE* formatListResponse)
835{
836 WINPR_ASSERT(context);
837 WINPR_ASSERT(formatListResponse);
838
839 if (formatListResponse->common.msgFlags & CB_RESPONSE_FAIL)
840 WLog_WARN(TAG, "format list update failed");
841 return CHANNEL_RC_OK;
842}
843
844std::shared_ptr<BYTE> sdlClip::getLocalData(uint32_t formatId, uint32_t& len)
845{
846 len = 0;
847
848 const auto offered = localMimeTypes();
849
850 /* The local clipboard still holds what this session received from the server (the
851 * server asked before our format list reached it). Reading it through SDL would call
852 * ClipDataCb and ask the server for its own data, so fail the request instead. */
853 if (hasMime(offered, _mime_uuid.c_str()))
854 {
855 WLog_Print(_log, WLOG_DEBUG, "local clipboard holds server data, not answering request");
856 return {};
857 }
858
859 UINT32 fileFormatId = 0;
860 UINT32 htmlFormatId = 0;
861 {
862 ClipboardLockGuard systemlock(_system);
863 std::scoped_lock lock(_lock);
864 WLog_Print(_log, WLOG_DEBUG, "Requesting format %s [0x%08" PRIx32 "] [%s]",
865 ClipboardGetFormatIdString(formatId), formatId,
866 ClipboardGetFormatName(_system, formatId));
867 fileFormatId = ClipboardGetFormatId(_system, s_type_FileGroupDescriptorW);
868 htmlFormatId = ClipboardGetFormatId(_system, s_type_HtmlFormat);
869 }
870
871 /* Local mime types to try, in order, and the _system format each is stored as for winpr to
872 * convert: its registered format, the requested one, or a newly registered one. */
873 enum class Store
874 {
875 Lookup,
876 Requested,
877 Register
878 };
879 struct Candidate
880 {
881 const char* mime;
882 Store store;
883 };
884 std::vector<Candidate> candidates;
885
886 switch (formatId)
887 {
888 case CF_TEXT:
889 case CF_OEMTEXT:
890 case CF_UNICODETEXT:
891 {
892 const char* mime = firstOffered(offered, s_mime_text());
893 if (mime)
894 candidates.push_back({ mime, Store::Lookup });
895 }
896 break;
897
898 case CF_DIB:
899 case CF_DIBV5:
900 /* Every convertible type on offer (screenshot tools offer image/png only, never
901 * image/bmp), in case a decoder rejects one of them. */
902 for (const auto& mime : s_mime_dib_sources())
903 {
904 if (hasMime(offered, mime))
905 candidates.push_back({ mime, Store::Register });
906 }
907 break;
908
909 case CF_TIFF:
910 candidates.push_back({ s_mime_tiff, Store::Requested });
911 break;
912
913 default:
914 if (formatId == fileFormatId)
915 candidates.push_back({ s_mime_uri_list, Store::Lookup });
916 else if (formatId == htmlFormatId)
917 {
918 /* In case HTML format was requested but we only have images in local clipboard */
919 const char* mime = hasMime(offered, s_mime_html)
920 ? s_mime_html
921 : firstOffered(offered, s_mime_html_image_sources());
922 if (mime)
923 candidates.push_back({ mime, Store::Lookup });
924 }
925 else
926 {
927 ClipboardLockGuard systemlock(_system);
928 std::scoped_lock lock(_lock);
929 const char* formatName = ClipboardGetFormatName(_system, formatId);
930 if (formatName && hasMime(offered, formatName))
931 {
932 /* the name is owned by _system and outlives this function */
933 candidates.push_back({ formatName, Store::Lookup });
934 }
935 else if (formatName && (mime_is_image(formatName) || mime_is_bmp(formatName)))
936 {
937 /* We announce every image format WinPR can convert to, but the SDL backend
938 * may only provide some of them (Windows: image/bmp). Take the ones on
939 * offer and let the WinPR clipboard convert to the requested format. */
940 for (const auto& mime : s_mime_dib_sources())
941 {
942 if (hasMime(offered, mime))
943 candidates.push_back({ mime, Store::Register });
944 }
945 }
946 }
947 break;
948 }
949
950 std::shared_ptr<BYTE> data;
951 for (const auto& candidate : candidates)
952 {
953 /* Read without holding our locks: on X11 this pumps events and can take a while, and
954 * the channel thread needs the locks to deliver server data. */
955 size_t size = 0;
956 std::shared_ptr<void> sdldata;
957 if (formatId == fileFormatId)
958 sdldata =
959 std::shared_ptr<void>(cliprdr_file_context_get_local_uri_list(_file, &size), free);
960 if (!sdldata)
961 sdldata = std::shared_ptr<void>(SDL_GetClipboardData(candidate.mime, &size), SDL_free);
962 if (!sdldata || (size > UINT32_MAX))
963 continue;
964
965 ClipboardLockGuard systemlock(_system);
966 std::scoped_lock lock(_lock);
967
968 if (formatId == fileFormatId)
969 {
970 auto bdata = static_cast<const char*>(sdldata.get());
971 if (!cliprdr_file_context_update_client_data(_file, bdata, size))
972 continue;
973 }
974
975 UINT32 storeId = formatId;
976 if (candidate.store == Store::Lookup)
977 storeId = ClipboardGetFormatId(_system, candidate.mime);
978 else if (candidate.store == Store::Register)
979 storeId = ClipboardRegisterFormat(_system, candidate.mime);
980
981 uint32_t ptrlen = 0;
982 BYTE* ptr = nullptr;
983 if (ClipboardSetData(_system, storeId, sdldata.get(), static_cast<uint32_t>(size)))
984 ptr = static_cast<BYTE*>(ClipboardGetData(_system, formatId, &ptrlen));
985
986 /* _system is also where ClipDataCb looks for server data: leave no local data behind,
987 * or a later local paste would be answered with it. */
988 ClipboardEmpty(_system);
989
990 if (!ptr)
991 {
992 WLog_Print(_log, WLOG_DEBUG, "could not convert local %s", candidate.mime);
993 continue;
994 }
995
996 data = std::shared_ptr<BYTE>(ptr, free);
997 len = ptrlen;
998
999 if (formatId == fileFormatId)
1000 {
1001 BYTE* ddata = nullptr;
1002 UINT32 dsize = 0;
1003 const UINT32 flags = cliprdr_file_context_remote_get_flags(_file);
1004 const UINT32 error = cliprdr_serialize_file_list_ex(
1005 flags, reinterpret_cast<const FILEDESCRIPTORW*>(data.get()),
1006 ptrlen / sizeof(FILEDESCRIPTORW), &ddata, &dsize);
1007 data = std::shared_ptr<BYTE>(ddata, free);
1008 len = dsize;
1009 if (error)
1010 {
1011 data.reset();
1012 len = 0;
1013 }
1014 }
1015 break;
1016 }
1017 return data;
1018}
1019
1020UINT sdlClip::ReceiveFormatDataRequest(CliprdrClientContext* context,
1021 const CLIPRDR_FORMAT_DATA_REQUEST* formatDataRequest)
1022{
1023 WINPR_ASSERT(context);
1024 WINPR_ASSERT(formatDataRequest);
1025
1026 auto clipboard = static_cast<sdlClip*>(
1027 cliprdr_file_context_get_context(static_cast<CliprdrFileContext*>(context->custom)));
1028 WINPR_ASSERT(clipboard);
1029
1030#if defined(__EMSCRIPTEN__)
1031 /* In the browser, never fall through to the SDL path below: SDL has no browser clipboard,
1032 * its internal one ends in ClipDataCb(), which requests the data from the server and waits
1033 * for the reply on this very channel thread, so nothing can arrive (the server times out and
1034 * all clipboard traffic stalls). Answer from the browser data, or fail at once. */
1035 if (!clipboard->webAnswerDataRequest(formatDataRequest))
1036 return clipboard->SendDataResponse(nullptr, 0);
1037 return CHANNEL_RC_OK;
1038#endif
1039 {
1040 std::scoped_lock lock(clipboard->_lock);
1041 const UINT32 mask = freerdp_settings_get_uint32(context->rdpcontext->settings,
1042 FreeRDP_ClipboardFeatureMask);
1043
1044 const UINT32 fileId =
1045 ClipboardRegisterFormat(clipboard->_system, s_type_FileGroupDescriptorW);
1046 if (fileId == formatDataRequest->requestedFormatId)
1047 {
1048 if ((mask & CLIPRDR_FLAG_LOCAL_TO_REMOTE_FILES) == 0)
1049 return clipboard->SendDataResponse(nullptr, 0);
1050 }
1051 else
1052 {
1053 if ((mask & CLIPRDR_FLAG_LOCAL_TO_REMOTE) == 0)
1054 return clipboard->SendDataResponse(nullptr, 0);
1055 }
1056
1057 /* ClipDataCb is waiting on the main thread for the server: a local application is
1058 * reading our offer of server data, so the local clipboard holds no client data to
1059 * give. Answer now. Queued for the main thread, the request would wait for the end of
1060 * that read while the server may hold its reply until we answer (seen with Windows:
1061 * a 9 s freeze until ClipDataCb timed out). */
1062 if (clipboard->_reading_server)
1063 {
1064 WLog_Print(clipboard->_log, WLOG_DEBUG,
1065 "local read of server data in progress, failing server request");
1066 return clipboard->SendDataResponse(nullptr, 0);
1067 }
1068 clipboard->_server_requests.push_back(formatDataRequest->requestedFormatId);
1069 }
1070
1071 /* This runs on the channel thread, but the SDL clipboard API is main-thread only: on
1072 * Wayland, reading the selection offer here races the main thread replacing it. */
1073 if (!SDL_RunOnMainThread([](void* userdata)
1074 { static_cast<sdlClip*>(userdata)->handleDataRequests(); }, clipboard,
1075 false))
1076 {
1077 std::scoped_lock lock(clipboard->_lock);
1078 if (!clipboard->_server_requests.empty())
1079 clipboard->_server_requests.pop_back();
1080 return clipboard->SendDataResponse(nullptr, 0);
1081 }
1082 return CHANNEL_RC_OK;
1083}
1084
1085void sdlClip::handleDataRequests()
1086{
1087 for (;;)
1088 {
1089 uint32_t formatId = 0;
1090 {
1091 std::scoped_lock lock(_lock);
1092 if (_server_requests.empty())
1093 return;
1094 formatId = _server_requests.front();
1095 _server_requests.pop_front();
1096 if (!_ctx)
1097 continue;
1098 }
1099
1100 uint32_t len = 0;
1101 auto data = getLocalData(formatId, len);
1102 std::scoped_lock lock(_lock);
1103 if (!_ctx)
1104 continue;
1105 if (SendDataResponse(data.get(), len) != CHANNEL_RC_OK)
1106 WLog_Print(_log, WLOG_WARN, "failed to send clipboard data response");
1107 }
1108}
1109
1110UINT sdlClip::ReceiveFormatDataResponse(CliprdrClientContext* context,
1111 const CLIPRDR_FORMAT_DATA_RESPONSE* formatDataResponse)
1112{
1113 WINPR_ASSERT(context);
1114 WINPR_ASSERT(formatDataResponse);
1115
1116 const UINT32 size = formatDataResponse->common.dataLen;
1117 const BYTE* data = formatDataResponse->requestedFormatData;
1118
1119 auto clipboard = static_cast<sdlClip*>(
1120 cliprdr_file_context_get_context(static_cast<CliprdrFileContext*>(context->custom)));
1121 WINPR_ASSERT(clipboard);
1122
1123#if defined(__EMSCRIPTEN__)
1124 if (clipboard->webTakeDataResponse(formatDataResponse))
1125 return CHANNEL_RC_OK;
1126#endif
1127 ClipboardLockGuard systemlock(clipboard->_system);
1128 std::scoped_lock lock(clipboard->_lock);
1129 if (clipboard->_request_queue.empty())
1130 {
1131 /* The matching request already timed out (see the wait below) and was
1132 * popped, so this is a late reply on a slow/high-latency link. Dropping it
1133 * is harmless; returning an error here would kill the cliprdr channel thread
1134 * and tear down the whole session. Warn and continue instead. */
1135 WLog_Print(clipboard->_log, WLOG_WARN,
1136 "format data response with no pending request (late reply?), ignoring");
1137 return CHANNEL_RC_OK;
1138 }
1139
1140 do
1141 {
1142 UINT32 srcFormatId = 0;
1143 auto& request = clipboard->_request_queue.front();
1144 bool success = (formatDataResponse->common.msgFlags & CB_RESPONSE_OK) &&
1145 !(formatDataResponse->common.msgFlags & CB_RESPONSE_FAIL);
1146 request.setSuccess(success);
1147
1148 if (!success)
1149 {
1150 WLog_Print(clipboard->_log, WLOG_WARN,
1151 "clipboard data request for format %" PRIu32 " [%s], mime %s failed",
1152 request.format(), request.formatstr().c_str(), request.mime().c_str());
1153 break;
1154 }
1155
1156 switch (request.format())
1157 {
1158 case CF_TEXT:
1159 case CF_OEMTEXT:
1160 case CF_UNICODETEXT:
1161 srcFormatId = request.format();
1162 break;
1163
1164 case CF_DIB:
1165 case CF_DIBV5:
1166 srcFormatId = request.format();
1167 break;
1168
1169 default:
1170 {
1171 auto name = clipboard->getServerFormat(request.format());
1172 if (!name.empty())
1173 {
1174 if (name == s_type_FileGroupDescriptorW)
1175 {
1176 srcFormatId =
1177 ClipboardGetFormatId(clipboard->_system, s_type_FileGroupDescriptorW);
1178
1179 if (cliprdr_file_context_has_local_support(clipboard->_file))
1180 {
1181 if (!cliprdr_file_context_update_server_data(
1182 clipboard->_file, clipboard->_system, data, size))
1183 {
1184 WLog_Print(clipboard->_log, WLOG_WARN,
1185 "File clipboard failed to update");
1186 }
1187 }
1188 }
1189 else if (name == s_type_HtmlFormat)
1190 {
1191 srcFormatId = ClipboardGetFormatId(clipboard->_system, s_type_HtmlFormat);
1192 }
1193 else if (mime_is_image(name) || mime_is_bmp(name))
1194 {
1195 srcFormatId = ClipboardGetFormatId(clipboard->_system, name.c_str());
1196 }
1197 }
1198 }
1199 break;
1200 }
1201
1202 if (!ClipboardSetData(clipboard->_system, srcFormatId, data, size))
1203 {
1204 /* Fail this request only: an error return here would end the clipboard channel
1205 * and, without auto-reconnect, the session. */
1206 WLog_Print(clipboard->_log, WLOG_ERROR, "error when setting clipboard data");
1207 request.setSuccess(false);
1208 break;
1209 }
1210 WLog_Print(clipboard->_log, WLOG_DEBUG, "updated clipboard data %s [0x%08" PRIx32 "]",
1211 ClipboardGetFormatName(clipboard->_system, srcFormatId), srcFormatId);
1212 } while (false);
1213
1214 if (!SetEvent(clipboard->_event))
1215 {
1216 WLog_Print(clipboard->_log, WLOG_ERROR, "error when setting clipboard event");
1217 return ERROR_INTERNAL_ERROR;
1218 }
1219
1220 return CHANNEL_RC_OK;
1221}
1222
1223const void* sdlClip::ClipDataCb(void* userdata, const char* mime_type, size_t* size)
1224{
1225 auto clip = static_cast<sdlClip*>(userdata);
1226 WINPR_ASSERT(clip);
1227 WINPR_ASSERT(size);
1228 WINPR_ASSERT(mime_type);
1229
1230 *size = 0;
1231 uint32_t len = 0;
1232
1233 if (mime_is_text(mime_type))
1234 {
1235 if (mime_is_utf8(mime_type))
1236 mime_type = mime_text_utf8;
1237 else
1238 mime_type = mime_text_plain;
1239 }
1240
1241 {
1242 ClipboardLockGuard systemlock(clip->_system);
1243 std::scoped_lock lock(clip->_lock);
1244
1245 if (!clip->_ctx)
1246 return nullptr;
1247
1248 /* check if we already used this mime type */
1249 auto cache = clip->_cache_data.find(mime_type);
1250 if (cache != clip->_cache_data.end())
1251 {
1252 *size = cache->second.size;
1253 return cache->second.ptr.get();
1254 }
1255
1256 auto formatID = clip->serverIdForMime(mime_type);
1257
1258 /* Can we convert the data from existing formats in the clibpard? */
1259 uint32_t fsize = 0;
1260 auto mimeFormatID = ClipboardRegisterFormat(clip->_system, mime_type);
1261 auto fptr = ClipboardGetData(clip->_system, mimeFormatID, &fsize);
1262 if (fptr)
1263 {
1264 auto ptr = std::shared_ptr<void>(fptr, free);
1265 if (mime_is_text(mime_type))
1266 fsize = textLength(fptr, fsize);
1267 clip->_cache_data.emplace(mime_type, cache_entry{ fsize, ptr });
1268
1269 auto fcache = clip->_cache_data.find(mime_type);
1270 if (fcache != clip->_cache_data.end())
1271 {
1272 *size = fcache->second.size;
1273 return fcache->second.ptr.get();
1274 }
1275 }
1276
1277 WLog_Print(clip->_log, WLOG_DEBUG, "requesting format %s [%s 0x%08" PRIx32 "]", mime_type,
1278 ClipboardGetFormatName(clip->_system, formatID), formatID);
1279
1280 /* From here until the reply, server data requests are answered on arrival (see
1281 * ReceiveFormatDataRequest). Those already queued get the same answer now: a local
1282 * application is reading our offer, so there is no client data to give. */
1283 clip->_reading_server = true;
1284 while (!clip->_server_requests.empty())
1285 {
1286 clip->_server_requests.pop_front();
1287 WLog_Print(clip->_log, WLOG_DEBUG,
1288 "local read of server data in progress, failing queued server request");
1289 std::ignore = clip->SendDataResponse(nullptr, 0);
1290 }
1291
1292 if (clip->SendDataRequest(formatID, mime_type))
1293 {
1294 clip->_reading_server = false;
1295 return nullptr;
1296 }
1297 }
1298 {
1299 HANDLE hdl[2] = { freerdp_abort_event(clip->_sdl->context()), clip->_event };
1300
1301 const UINT32 timeout =
1302 freerdp_settings_get_uint32(clip->_sdl->context()->settings, FreeRDP_TcpAckTimeout);
1303 DWORD status = WaitForMultipleObjects(ARRAYSIZE(hdl), hdl, FALSE, timeout);
1304
1305 if (status != WAIT_OBJECT_0 + 1)
1306 {
1307 std::scoped_lock lock(clip->_lock);
1308 clip->_reading_server = false;
1309 if (!clip->_request_queue.empty())
1310 clip->_request_queue.pop();
1311
1312 if (status == WAIT_TIMEOUT)
1313 WLog_Print(clip->_log, WLOG_ERROR,
1314 "no reply in 10 seconds, returning empty content");
1315
1316 return nullptr;
1317 }
1318 }
1319
1320 {
1321 ClipboardLockGuard systemlock(clip->_system);
1322 std::scoped_lock lock(clip->_lock);
1323 clip->_reading_server = false;
1324 if (clip->_request_queue.empty())
1325 return nullptr;
1326 /* take the request out of the queue: pop() destroys the queued one */
1327 auto request = std::move(clip->_request_queue.front());
1328 clip->_request_queue.pop();
1329
1330 if (clip->_request_queue.empty())
1331 std::ignore = ResetEvent(clip->_event);
1332
1333 if (request.success())
1334 {
1335 auto formatID = ClipboardRegisterFormat(clip->_system, mime_type);
1336 auto data = ClipboardGetData(clip->_system, formatID, &len);
1337 if (!data)
1338 {
1339 WLog_Print(clip->_log, WLOG_ERROR, "error retrieving clipboard data");
1340 return nullptr;
1341 }
1342
1343 auto ptr = std::shared_ptr<void>(data, free);
1344 if (mime_is_text(mime_type))
1345 len = textLength(data, len);
1346 clip->_cache_data.emplace(mime_type, cache_entry{ len, ptr });
1347 *size = len;
1348 return ptr.get();
1349 }
1350
1351 return nullptr;
1352 }
1353}
1354
1355void sdlClip::ClipCleanCb(void* userdata)
1356{
1357 auto clip = static_cast<sdlClip*>(userdata);
1358 WINPR_ASSERT(clip);
1359 ClipboardLockGuard give_me_a_name(clip->_system);
1360 std::scoped_lock lock(clip->_lock);
1361 ClipboardEmpty(clip->_system);
1362}
1363
1364bool sdlClip::mime_is_file(const std::string& mime)
1365{
1366 if (strncmp(s_mime_uri_list, mime.c_str(), sizeof(s_mime_uri_list)) == 0)
1367 return true;
1368 if (strncmp(s_mime_gnome_copied_files, mime.c_str(), sizeof(s_mime_gnome_copied_files)) == 0)
1369 return true;
1370 if (strncmp(s_mime_mate_copied_files, mime.c_str(), sizeof(s_mime_mate_copied_files)) == 0)
1371 return true;
1372 return false;
1373}
1374
1375bool sdlClip::mime_is_text(const std::string& mime)
1376{
1377 for (const auto& tmime : s_mime_text())
1378 {
1379 assert(tmime != nullptr);
1380 if (mime == tmime)
1381 return true;
1382 }
1383
1384 return false;
1385}
1386
1387bool sdlClip::mime_is_utf8(const std::string& mime)
1388{
1389 if (mime == std::string(mime_text_utf8))
1390 return true;
1391
1392 return mime == std::string("UTF8_STRING");
1393}
1394
1395bool sdlClip::mime_is_image(const std::string& mime)
1396{
1397 for (const auto& imime : s_mime_image())
1398 {
1399 assert(imime != nullptr);
1400 if (mime == imime)
1401 return true;
1402 }
1403
1404 return false;
1405}
1406
1407bool sdlClip::mime_is_bmp(const std::string& mime)
1408{
1409 for (const auto& imime : s_mime_bitmap())
1410 {
1411 assert(imime != nullptr);
1412 if (mime == imime)
1413 return true;
1414 }
1415
1416 return false;
1417}
1418
1419bool sdlClip::mime_is_html(const std::string& mime)
1420{
1421 return mime.compare(s_mime_html) == 0;
1422}
1423
1424ClipRequest::ClipRequest(UINT32 format, const std::string& mime)
1425 : _format(format), _mime(mime), _success(false)
1426{
1427}
1428
1429uint32_t ClipRequest::format() const
1430{
1431 return _format;
1432}
1433
1434std::string ClipRequest::formatstr() const
1435{
1436 return ClipboardGetFormatIdString(_format);
1437}
1438
1439std::string ClipRequest::mime() const
1440{
1441 return _mime;
1442}
1443
1444bool ClipRequest::success() const
1445{
1446 return _success;
1447}
1448
1449void ClipRequest::setSuccess(bool status)
1450{
1451 _success = status;
1452}
1453
1454CliprdrFormat::CliprdrFormat(uint32_t formatID, const char* formatName) : _formatID(formatID)
1455{
1456 if (formatName)
1457 _formatName = formatName;
1458}
1459
1460uint32_t CliprdrFormat::formatId() const
1461{
1462 return _formatID;
1463}
1464
1465const char* CliprdrFormat::formatName() const
1466{
1467 if (_formatName.empty())
1468 return nullptr;
1469 return _formatName.c_str();
1470}
1471
1472#if defined(__EMSCRIPTEN__)
1473/* Browser clipboard (text and images). The page calls freerdp_web_clipboard_offer() with what
1474 * the browser clipboard holds (UTF-8 text and/or an image converted to CF_DIB); we announce the
1475 * formats and answer the server's requests from that data. When the server announces text or a
1476 * CF_DIB image, we fetch it at once and hand it to the page (Module.freerdpClipboardWrite /
1477 * freerdpClipboardWriteImage), which writes it with the async Clipboard API (images as PNG). */
1478static const char s_web_mime_text[] = "x-freerdp-web-text";
1479static const char s_web_mime_dib[] = "x-freerdp-web-dib";
1480static const char s_web_mime_png[] = "x-freerdp-web-png";
1481
1482/* in sdl_clip_emscripten.js */
1483extern "C" void freerdp_web_clipboard_write_js(const char* utf8);
1484extern "C" void freerdp_web_clipboard_write_image_js(const BYTE* data, size_t size, int png);
1485
1486typedef struct
1487{
1488 char* text;
1489 BYTE* dib;
1490 size_t size;
1491 bool png;
1492} web_write_job;
1493
1494static void web_write_on_main(void* arg)
1495{
1496 auto job = static_cast<web_write_job*>(arg);
1497 if (job->text)
1498 freerdp_web_clipboard_write_js(job->text);
1499 if (job->dib)
1500 freerdp_web_clipboard_write_image_js(job->dib, job->size, job->png ? 1 : 0);
1501 free(job->text);
1502 free(job->dib);
1503 free(job);
1504}
1505
1506static void web_write(char* text, BYTE* dib, size_t size, bool png = false)
1507{
1508 auto job = static_cast<web_write_job*>(calloc(1, sizeof(web_write_job)));
1509 if (!job)
1510 {
1511 free(text);
1512 free(dib);
1513 return;
1514 }
1515 job->text = text;
1516 job->dib = dib;
1517 job->size = size;
1518 job->png = png;
1519 if (emscripten_is_main_browser_thread())
1520 web_write_on_main(job);
1521 else if (!emscripten_proxy_async(emscripten_proxy_get_system_queue(),
1522 emscripten_main_runtime_thread_id(), web_write_on_main, job))
1523 {
1524 free(text);
1525 free(dib);
1526 free(job);
1527 }
1528}
1529
1530bool sdlClip::webOffer(const char* utf8, const BYTE* dib, size_t dibSize)
1531{
1532 std::scoped_lock web(s_web_lock);
1533 auto clip = s_web_clip;
1534 if (!clip || !clip->_ctx || !clip->_ctx->ClientFormatList)
1535 return false;
1536 std::vector<CLIPRDR_FORMAT> formats;
1537 {
1538 std::scoped_lock lock(clip->_lock);
1539 clip->_web_local = utf8 ? utf8 : "";
1540 clip->_web_local_dib.assign(dib, dib ? dib + dibSize : dib);
1541 /* remember what we offered: the server may announce it back much later */
1542 if (!clip->_web_local.empty())
1543 clip->_web_recent.push_back(std::hash<std::string>{}(clip->_web_local));
1544 if (dib && dibSize)
1545 clip->_web_recent.push_back(std::hash<std::string_view>{}(
1546 std::string_view(reinterpret_cast<const char*>(dib), dibSize)));
1547 while (clip->_web_recent.size() > 16)
1548 clip->_web_recent.pop_front();
1549 if (!clip->_web_local.empty())
1550 {
1551 formats.push_back({ CF_UNICODETEXT, nullptr });
1552 formats.push_back({ CF_TEXT, nullptr });
1553 }
1554 if (!clip->_web_local_dib.empty())
1555 formats.push_back({ CF_DIB, nullptr });
1556 }
1557 if (formats.empty())
1558 return false;
1559 CLIPRDR_FORMAT_LIST list = {};
1560 list.common.msgType = CB_FORMAT_LIST;
1561 list.numFormats = static_cast<UINT32>(formats.size());
1562 list.formats = formats.data();
1563 return clip->_ctx->ClientFormatList(clip->_ctx, &list) == CHANNEL_RC_OK;
1564}
1565
1566bool sdlClip::webRequestServerText()
1567{
1568 bool has_text = false;
1569 bool has_dib = false;
1570 UINT32 png_id = 0;
1571 std::scoped_lock lock(_lock);
1572 for (const auto& f : _serverFormats)
1573 {
1574 has_text |= (f.formatId() == CF_UNICODETEXT);
1575 has_dib |= (f.formatId() == CF_DIB);
1576 /* GNOME Remote Desktop announces a PNG-only clipboard as "image/png" alone */
1577 if (f.formatName() && (strcmp(f.formatName(), s_mime_png) == 0))
1578 png_id = f.formatId();
1579 }
1580 if (!has_text && !has_dib && !png_id)
1581 return false;
1582 /* Keep the browser data for now: servers often announce our own offer back (an echo, e.g.
1583 * GNOME's clipboard handling). Clearing it here left nothing to answer the following data
1584 * request with. webTakeDataResponse() compares and only then hands over ownership. */
1585 bool ok = true;
1586 if (has_text)
1587 ok &= (SendDataRequest(CF_UNICODETEXT, s_web_mime_text) == CHANNEL_RC_OK);
1588 if (has_dib)
1589 ok &= (SendDataRequest(CF_DIB, s_web_mime_dib) == CHANNEL_RC_OK);
1590 else if (png_id) /* browsers write PNG anyway: take it as it is */
1591 ok &= (SendDataRequest(png_id, s_web_mime_png) == CHANNEL_RC_OK);
1592 return ok;
1593}
1594
1595bool sdlClip::webTakeDataResponse(const CLIPRDR_FORMAT_DATA_RESPONSE* response)
1596{
1597 std::string kind;
1598 {
1599 std::scoped_lock lock(_lock);
1600 if (_request_queue.empty())
1601 return false;
1602 kind = _request_queue.front().mime();
1603 if ((kind != s_web_mime_text) && (kind != s_web_mime_dib) && (kind != s_web_mime_png))
1604 return false;
1605 _request_queue.pop();
1606 }
1607
1608 if (!(response->common.msgFlags & CB_RESPONSE_OK) || !response->requestedFormatData)
1609 return true;
1610 const BYTE* data = response->requestedFormatData;
1611 const size_t size = response->common.dataLen;
1612 if (kind == s_web_mime_text)
1613 {
1614 char* utf8 = ConvertWCharNToUtf8Alloc(reinterpret_cast<const WCHAR*>(data),
1615 size / sizeof(WCHAR), nullptr);
1616 if (!utf8)
1617 return true;
1618 {
1619 std::scoped_lock lock(_lock);
1620 const auto hv = std::hash<std::string>{}(std::string(utf8));
1621 if (std::find(_web_recent.begin(), _web_recent.end(), hv) != _web_recent.end())
1622 {
1623 free(utf8); /* one of our own offers announced back: keep serving ours */
1624 return true;
1625 }
1626 _web_local.clear(); /* new content from the server: it owns the clipboard now */
1627 _web_local_dib.clear();
1628 }
1629 web_write(utf8, nullptr, 0);
1630 }
1631 else if ((kind == s_web_mime_png) ? (size > 8)
1632 : (size > 40)) /* PNG signature, BITMAPINFOHEADER */
1633 {
1634 {
1635 std::scoped_lock lock(_lock);
1636 const auto hv = std::hash<std::string_view>{}(
1637 std::string_view(reinterpret_cast<const char*>(data), size));
1638 if (std::find(_web_recent.begin(), _web_recent.end(), hv) != _web_recent.end())
1639 return true; /* one of our own images announced back */
1640 _web_local.clear();
1641 _web_local_dib.clear();
1642 }
1643 auto copy = static_cast<BYTE*>(malloc(size));
1644 if (copy)
1645 {
1646 memcpy(copy, data, size);
1647 web_write(nullptr, copy, size, kind == s_web_mime_png);
1648 }
1649 }
1650 return true;
1651}
1652
1653bool sdlClip::webAnswerDataRequest(const CLIPRDR_FORMAT_DATA_REQUEST* request)
1654{
1655 std::string text;
1656 std::vector<BYTE> dib;
1657 {
1658 std::scoped_lock lock(_lock);
1659 if (_web_local.empty() && _web_local_dib.empty())
1660 return false;
1661 text = _web_local;
1662 dib = _web_local_dib;
1663 }
1664 switch (request->requestedFormatId)
1665 {
1666 case CF_UNICODETEXT:
1667 {
1668 if (text.empty())
1669 return false;
1670 WCHAR* wstr = ConvertUtf8NToWCharAlloc(text.c_str(), text.size() + 1, nullptr);
1671 if (!wstr)
1672 return false;
1673 /* UTF-16 including the terminating NUL, as CF_UNICODETEXT requires */
1674 std::ignore = SendDataResponse(reinterpret_cast<const BYTE*>(wstr),
1675 (_wcslen(wstr) + 1) * sizeof(WCHAR));
1676 free(wstr);
1677 return true;
1678 }
1679 case CF_TEXT:
1680 if (text.empty())
1681 return false;
1682 std::ignore =
1683 SendDataResponse(reinterpret_cast<const BYTE*>(text.c_str()), text.size() + 1);
1684 return true;
1685 case CF_DIB:
1686 if (dib.empty())
1687 return false;
1688 std::ignore = SendDataResponse(dib.data(), dib.size());
1689 return true;
1690 default:
1691 return false;
1692 }
1693}
1694
1695/* Called by the page on the main browser thread. That thread also runs FreeRDP's main loop
1696 * under Asyncify: when it is suspended in the middle of a transport write (e.g. waiting inside
1697 * a TLS write), calling into the channel from here would re-enter the same TLS connection on the
1698 * same thread; the transport lock does not stop that (same owner), the records interleave and
1699 * the server fails the connection with "bad record mac". So copy the data and send the format
1700 * list from a separate thread. */
1701typedef struct
1702{
1703 char* text;
1704 BYTE* dib;
1705 size_t size;
1706} web_offer_job;
1707
1708static void* web_offer_thread(void* arg)
1709{
1710 auto job = static_cast<web_offer_job*>(arg);
1711 std::ignore = sdlClip::webOffer(job->text, job->dib, job->size);
1712 free(job->text);
1713 free(job->dib);
1714 free(job);
1715 return nullptr;
1716}
1717
1718extern "C" EMSCRIPTEN_KEEPALIVE int freerdp_web_clipboard_offer(const char* utf8, const BYTE* dib,
1719 size_t dibSize)
1720{
1721 {
1722 std::scoped_lock web(s_web_lock);
1723 if (!s_web_clip)
1724 return 0;
1725 }
1726 auto job = static_cast<web_offer_job*>(calloc(1, sizeof(web_offer_job)));
1727 if (!job)
1728 return 0;
1729 job->text = utf8 ? _strdup(utf8) : nullptr;
1730 if (dib && dibSize)
1731 {
1732 job->dib = static_cast<BYTE*>(malloc(dibSize));
1733 if (job->dib)
1734 {
1735 memcpy(job->dib, dib, dibSize);
1736 job->size = dibSize;
1737 }
1738 }
1739 pthread_t tid = {};
1740 if (pthread_create(&tid, nullptr, web_offer_thread, job) != 0)
1741 {
1742 free(job->text);
1743 free(job->dib);
1744 free(job);
1745 return 0;
1746 }
1747 pthread_detach(tid);
1748 return 1;
1749}
1750#endif
object that handles clipboard context for the SDL3 client
Definition sdl_clip.hpp:77
WINPR_ATTR_NODISCARD FREERDP_API UINT32 freerdp_settings_get_uint32(const rdpSettings *settings, FreeRDP_Settings_Keys_UInt32 id)
Returns a UINT32 settings value.