/* Copyright 2016-2019 Pierre Ossman for Cendio AB * * This 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 software 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 software; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, * USA. */ #ifdef HAVE_DIX_CONFIG_H #include #endif #include #include "propertyst.h" #include "scrnintstr.h" #include "selection.h" #include "windowstr.h" #include "xace.h" #include "xorg-version.h" #include "vncExtInit.h" #include "vncSelection.h" #include "RFBGlue.h" #define LOG_NAME "Selection" #define LOG_ERROR(...) vncLogError(LOG_NAME, __VA_ARGS__) #define LOG_STATUS(...) vncLogStatus(LOG_NAME, __VA_ARGS__) #define LOG_INFO(...) vncLogInfo(LOG_NAME, __VA_ARGS__) #define LOG_DEBUG(...) vncLogDebug(LOG_NAME, __VA_ARGS__) static Atom xaPRIMARY, xaCLIPBOARD; static Atom xaTARGETS, xaTIMESTAMP, xaSTRING, xaTEXT, xaUTF8_STRING; static Atom xaINCR; static Atom *xaBinclips; static unsigned xaHtmlIndex, xaPngIndex; static Bool htmlPngPresent; static unsigned *mimeIndexesFromClient; static unsigned numMimesFromClient; static Bool textFromClient; static WindowPtr pWindow; static Window wid; static Bool probing; static Atom activeSelection = None; struct VncDataTarget { ClientPtr client; Atom selection; Atom target; Atom property; Window requestor; CARD32 time; struct VncDataTarget* next; }; static struct VncDataTarget* vncDataTargetHead; static int vncCreateSelectionWindow(void); static int vncOwnSelection(Atom selection); static int vncConvertSelection(ClientPtr client, Atom selection, Atom target, Atom property, Window requestor, CARD32 time); static int vncProcConvertSelection(ClientPtr client); static void vncSelectionRequest(Atom selection, Atom target); static int vncProcSendEvent(ClientPtr client); static void vncSelectionCallback(CallbackListPtr *callbacks, void * data, void * args); static void vncClientStateCallback(CallbackListPtr * l, void * d, void * p); static int (*origProcConvertSelection)(ClientPtr); static int (*origProcSendEvent)(ClientPtr); void vncSelectionInit(void) { xaPRIMARY = MakeAtom("PRIMARY", 7, TRUE); xaCLIPBOARD = MakeAtom("CLIPBOARD", 9, TRUE); xaTARGETS = MakeAtom("TARGETS", 7, TRUE); xaTIMESTAMP = MakeAtom("TIMESTAMP", 9, TRUE); xaSTRING = MakeAtom("STRING", 6, TRUE); xaTEXT = MakeAtom("TEXT", 4, TRUE); xaUTF8_STRING = MakeAtom("UTF8_STRING", 11, TRUE); xaINCR = MakeAtom("INCR", 4, TRUE); unsigned i; mimeIndexesFromClient = calloc(dlp_num_mimetypes(), sizeof(unsigned)); numMimesFromClient = 0; textFromClient = FALSE; xaBinclips = calloc(dlp_num_mimetypes(), sizeof(Atom)); htmlPngPresent = FALSE; Bool htmlfound = FALSE, pngfound = FALSE; for (i = 0; i < dlp_num_mimetypes(); i++) { const char *cur = dlp_get_mimetype(i); xaBinclips[i] = MakeAtom(cur, strlen(cur), TRUE); if (!strcmp(cur, "text/html")) { xaHtmlIndex = i; htmlfound = TRUE; } if (!strcmp(cur, "image/png")) { xaPngIndex = i; pngfound = TRUE; } } if (htmlfound && pngfound) htmlPngPresent = TRUE; /* There are no hooks for when these are internal windows, so * override the relevant handlers. */ origProcConvertSelection = ProcVector[X_ConvertSelection]; ProcVector[X_ConvertSelection] = vncProcConvertSelection; origProcSendEvent = ProcVector[X_SendEvent]; ProcVector[X_SendEvent] = vncProcSendEvent; if (!AddCallback(&SelectionCallback, vncSelectionCallback, 0)) FatalError("Add VNC SelectionCallback failed\n"); if (!AddCallback(&ClientStateCallback, vncClientStateCallback, 0)) FatalError("Add VNC ClientStateCallback failed\n"); } static void vncHandleClipboardRequest(void) { if (activeSelection == None) { LOG_DEBUG("Got request for local clipboard although no clipboard is active"); return; } LOG_DEBUG("Got request for local clipboard, re-probing formats"); probing = FALSE; vncSelectionRequest(activeSelection, xaTARGETS); } void vncHandleClipboardAnnounce(int available) { if (available) { int rc; LOG_DEBUG("Remote clipboard announced, grabbing local ownership"); if (vncGetSetPrimary()) { rc = vncOwnSelection(xaPRIMARY); if (rc != Success) LOG_ERROR("Could not set PRIMARY selection"); } rc = vncOwnSelection(xaCLIPBOARD); if (rc != Success) LOG_ERROR("Could not set CLIPBOARD selection"); } else { struct VncDataTarget* next; if (pWindow == NULL) return; LOG_DEBUG("Remote clipboard lost, removing local ownership"); DeleteWindowFromAnySelections(pWindow); /* Abort any pending transfer */ while (vncDataTargetHead != NULL) { xEvent event; event.u.u.type = SelectionNotify; event.u.selectionNotify.time = vncDataTargetHead->time; event.u.selectionNotify.requestor = vncDataTargetHead->requestor; event.u.selectionNotify.selection = vncDataTargetHead->selection; event.u.selectionNotify.target = vncDataTargetHead->target; event.u.selectionNotify.property = None; WriteEventsToClient(vncDataTargetHead->client, 1, &event); next = vncDataTargetHead->next; free(vncDataTargetHead); vncDataTargetHead = next; } } } void vncHandleClipboardAnnounceBinary(const unsigned num, const char mimes[][32]) { if (num) { int rc; LOG_DEBUG("Remote binary clipboard announced, grabbing local ownership"); if (vncGetSetPrimary()) { rc = vncOwnSelection(xaPRIMARY); if (rc != Success) LOG_ERROR("Could not set PRIMARY selection"); } rc = vncOwnSelection(xaCLIPBOARD); if (rc != Success) LOG_ERROR("Could not set CLIPBOARD selection"); unsigned i, valid = 0; for (i = 0; i < num; i++) { unsigned j; for (j = 0; j < dlp_num_mimetypes(); j++) { if (!strcmp(dlp_get_mimetype(j), mimes[i])) { mimeIndexesFromClient[valid] = j; valid++; break; } } if (!strcmp(mimes[i], "text/plain")) textFromClient = TRUE; } numMimesFromClient = valid; LOG_DEBUG("Client sent %u mimes, %u were valid", num, valid); } else { struct VncDataTarget* next; numMimesFromClient = 0; textFromClient = FALSE; if (pWindow == NULL) return; LOG_DEBUG("Remote binary clipboard lost, removing local ownership"); DeleteWindowFromAnySelections(pWindow); /* Abort any pending transfer */ while (vncDataTargetHead != NULL) { xEvent event; event.u.u.type = SelectionNotify; event.u.selectionNotify.time = vncDataTargetHead->time; event.u.selectionNotify.requestor = vncDataTargetHead->requestor; event.u.selectionNotify.selection = vncDataTargetHead->selection; event.u.selectionNotify.target = vncDataTargetHead->target; event.u.selectionNotify.property = None; WriteEventsToClient(vncDataTargetHead->client, 1, &event); next = vncDataTargetHead->next; free(vncDataTargetHead); vncDataTargetHead = next; } } } static int vncCreateSelectionWindow(void) { ScreenPtr pScreen; int result; if (pWindow != NULL) return Success; pScreen = screenInfo.screens[0]; wid = FakeClientID(0); pWindow = CreateWindow(wid, pScreen->root, 0, 0, 100, 100, 0, InputOnly, 0, NULL, 0, serverClient, CopyFromParent, &result); if (!pWindow) return result; if (!AddResource(pWindow->drawable.id, RT_WINDOW, pWindow)) return BadAlloc; LOG_DEBUG("Created selection window"); return Success; } static int vncOwnSelection(Atom selection) { Selection *pSel; int rc; SelectionInfoRec info; rc = vncCreateSelectionWindow(); if (rc != Success) return rc; rc = dixLookupSelection(&pSel, selection, serverClient, DixSetAttrAccess); if (rc == Success) { if (pSel->client && (pSel->client != serverClient)) { xEvent event = { .u.selectionClear.time = currentTime.milliseconds, .u.selectionClear.window = pSel->window, .u.selectionClear.atom = pSel->selection }; event.u.u.type = SelectionClear; WriteEventsToClient(pSel->client, 1, &event); } } else if (rc == BadMatch) { pSel = dixAllocateObjectWithPrivates(Selection, PRIVATE_SELECTION); if (!pSel) return BadAlloc; pSel->selection = selection; rc = XaceHookSelectionAccess(serverClient, &pSel, DixCreateAccess | DixSetAttrAccess); if (rc != Success) { free(pSel); return rc; } pSel->next = CurrentSelections; CurrentSelections = pSel; } else return rc; pSel->lastTimeChanged = currentTime; pSel->window = wid; pSel->pWin = pWindow; pSel->client = serverClient; LOG_DEBUG("Grabbed %s selection", NameForAtom(selection)); info.selection = pSel; info.client = serverClient; info.kind = SelectionSetOwner; CallCallbacks(&SelectionCallback, &info); return Success; } static Bool clientHasBinaryAtom(const Atom target, unsigned *which) { unsigned i; for (i = 0; i < numMimesFromClient; i++) { if (xaBinclips[mimeIndexesFromClient[i]] == target) { *which = mimeIndexesFromClient[i]; return TRUE; } } return FALSE; } static int vncConvertSelection(ClientPtr client, Atom selection, Atom target, Atom property, Window requestor, CARD32 time) { Selection *pSel; WindowPtr pWin; int rc; Atom realProperty; xEvent event; LOG_DEBUG("Selection request for %s (type %s)", NameForAtom(selection), NameForAtom(target)); rc = dixLookupSelection(&pSel, selection, client, DixGetAttrAccess); if (rc != Success) return rc; /* We do not validate the time argument because neither does * dix/selection.c and some clients (e.g. Qt) relies on this */ rc = dixLookupWindow(&pWin, requestor, client, DixSetAttrAccess); if (rc != Success) return rc; if (property != None) realProperty = property; else realProperty = target; /* FIXME: MULTIPLE target */ unsigned binatomidx; if (target == xaTARGETS) { Atom targets[5 + numMimesFromClient]; targets[0] = xaTARGETS; targets[1] = xaTIMESTAMP; targets[2] = xaSTRING; targets[3] = xaTEXT; targets[4] = xaUTF8_STRING; unsigned i; for (i = 0; i < numMimesFromClient; i++) targets[5 + i] = xaBinclips[mimeIndexesFromClient[i]]; rc = dixChangeWindowProperty(serverClient, pWin, realProperty, XA_ATOM, 32, PropModeReplace, 5 + numMimesFromClient, targets, TRUE); if (rc != Success) return rc; } else if (target == xaTIMESTAMP) { rc = dixChangeWindowProperty(serverClient, pWin, realProperty, XA_INTEGER, 32, PropModeReplace, 1, &pSel->lastTimeChanged.milliseconds, TRUE); if (rc != Success) return rc; } else if (clientHasBinaryAtom(target, &binatomidx)) { const unsigned char *data; unsigned len; vncGetBinaryClipboardData(dlp_get_mimetype(binatomidx), &data, &len); rc = dixChangeWindowProperty(serverClient, pWin, realProperty, xaBinclips[binatomidx], 8, PropModeReplace, len, (unsigned char *) data, TRUE); if (rc != Success) return rc; } else if (textFromClient) { const unsigned char *data; unsigned len; vncGetBinaryClipboardData("text/plain", &data, &len); if ((target == xaSTRING) || (target == xaTEXT)) { char* latin1; latin1 = vncUTF8ToLatin1(data, (size_t)-1); if (latin1 == NULL) return BadAlloc; rc = dixChangeWindowProperty(serverClient, pWin, realProperty, XA_STRING, 8, PropModeReplace, len, latin1, TRUE); vncStrFree(latin1); if (rc != Success) return rc; } else if (target == xaUTF8_STRING) { rc = dixChangeWindowProperty(serverClient, pWin, realProperty, xaUTF8_STRING, 8, PropModeReplace, len, (char *) data, TRUE); if (rc != Success) return rc; } else { return BadMatch; } } else { LOG_ERROR("Text clipboard paste requested, but client sent no text"); return BadMatch; } event.u.u.type = SelectionNotify; event.u.selectionNotify.time = time; event.u.selectionNotify.requestor = requestor; event.u.selectionNotify.selection = selection; event.u.selectionNotify.target = target; event.u.selectionNotify.property = property; WriteEventsToClient(client, 1, &event); return Success; } static int vncProcConvertSelection(ClientPtr client) { Bool paramsOkay; WindowPtr pWin; Selection *pSel; int rc; REQUEST(xConvertSelectionReq); REQUEST_SIZE_MATCH(xConvertSelectionReq); rc = dixLookupWindow(&pWin, stuff->requestor, client, DixSetAttrAccess); if (rc != Success) return rc; paramsOkay = ValidAtom(stuff->selection) && ValidAtom(stuff->target); paramsOkay &= (stuff->property == None) || ValidAtom(stuff->property); if (!paramsOkay) { client->errorValue = stuff->property; return BadAtom; } rc = dixLookupSelection(&pSel, stuff->selection, client, DixReadAccess); if (rc == Success && pSel->client == serverClient && pSel->window == wid) { rc = vncConvertSelection(client, stuff->selection, stuff->target, stuff->property, stuff->requestor, stuff->time); if (rc != Success) { xEvent event; memset(&event, 0, sizeof(xEvent)); event.u.u.type = SelectionNotify; event.u.selectionNotify.time = stuff->time; event.u.selectionNotify.requestor = stuff->requestor; event.u.selectionNotify.selection = stuff->selection; event.u.selectionNotify.target = stuff->target; event.u.selectionNotify.property = None; WriteEventsToClient(client, 1, &event); } return Success; } return origProcConvertSelection(client); } static void vncSelectionRequest(Atom selection, Atom target) { Selection *pSel; xEvent event; int rc; rc = vncCreateSelectionWindow(); if (rc != Success) return; LOG_DEBUG("Requesting %s for %s selection", NameForAtom(target), NameForAtom(selection)); rc = dixLookupSelection(&pSel, selection, serverClient, DixGetAttrAccess); if (rc != Success) return; event.u.u.type = SelectionRequest; event.u.selectionRequest.owner = pSel->window; event.u.selectionRequest.time = currentTime.milliseconds; event.u.selectionRequest.requestor = wid; event.u.selectionRequest.selection = selection; event.u.selectionRequest.target = target; event.u.selectionRequest.property = target; WriteEventsToClient(pSel->client, 1, &event); } static Bool vncHasAtom(Atom atom, const Atom list[], size_t size) { size_t i; for (i = 0;i < size;i++) { if (list[i] == atom) return TRUE; } return FALSE; } static Bool vncHasBinaryClipboardAtom(const Atom list[], size_t size) { size_t i, b; const unsigned num = dlp_num_mimetypes(); for (i = 0;i < size;i++) { for (b = 0; b < num; b++) { if (list[i] == xaBinclips[b]) return TRUE; } } return FALSE; } static void vncHandleSelection(Atom selection, Atom target, Atom property, Atom requestor, TimeStamp time) { PropertyPtr prop; int rc; rc = dixLookupProperty(&prop, pWindow, property, serverClient, DixReadAccess); if (rc != Success) return; LOG_DEBUG("Selection notification for %s (target %s, property %s, type %s)", NameForAtom(selection), NameForAtom(target), NameForAtom(property), NameForAtom(prop->type)); if (target != property) return; if (prop->type == xaINCR) LOG_INFO("Incremental clipboard transfer denied, too large"); if (target == xaTARGETS) { if (prop->format != 32) return; if (prop->type != XA_ATOM) return; if (probing) { if (vncHasAtom(xaSTRING, (const Atom*)prop->data, prop->size) || vncHasAtom(xaUTF8_STRING, (const Atom*)prop->data, prop->size) || vncHasBinaryClipboardAtom((const Atom*)prop->data, prop->size)) { LOG_DEBUG("Compatible format found, notifying clients"); vncClearBinaryClipboardData(); activeSelection = selection; vncAnnounceClipboard(TRUE); vncHandleClipboardRequest(); } } else { if (vncHasAtom(xaUTF8_STRING, (const Atom*)prop->data, prop->size)) vncSelectionRequest(selection, xaUTF8_STRING); else if (vncHasAtom(xaSTRING, (const Atom*)prop->data, prop->size)) vncSelectionRequest(selection, xaSTRING); unsigned i; Bool skiphtml = FALSE; if (htmlPngPresent && vncHasAtom(xaBinclips[xaHtmlIndex], (const Atom*)prop->data, prop->size) && vncHasAtom(xaBinclips[xaPngIndex], (const Atom*)prop->data, prop->size)) skiphtml = TRUE; for (i = 0; i < dlp_num_mimetypes(); i++) { if (skiphtml && i == xaHtmlIndex) continue; if (vncHasAtom(xaBinclips[i], (const Atom*)prop->data, prop->size)) { vncSelectionRequest(selection, xaBinclips[i]); //break; } } } } else if (target == xaSTRING) { char* filtered; char* utf8; if (prop->format != 8) return; if (prop->type != xaSTRING) return; filtered = vncConvertLF(prop->data, prop->size); if (filtered == NULL) return; utf8 = vncLatin1ToUTF8(filtered, (size_t)-1); vncStrFree(filtered); if (utf8 == NULL) return; LOG_DEBUG("Sending text part of binary clipboard to clients (%d bytes)", (int)strlen(utf8)); vncSendBinaryClipboardData("text/plain", utf8, strlen(utf8)); vncStrFree(utf8); } else if (target == xaUTF8_STRING) { char *filtered; if (prop->format != 8) return; if (prop->type != xaUTF8_STRING) return; filtered = vncConvertLF(prop->data, prop->size); if (filtered == NULL) return; LOG_DEBUG("Sending text part of binary clipboard to clients (%d bytes)", (int)strlen(filtered)); vncSendBinaryClipboardData("text/plain", filtered, strlen(filtered)); vncStrFree(filtered); } else { unsigned i; if (prop->format != 8) return; for (i = 0; i < dlp_num_mimetypes(); i++) { if (target == xaBinclips[i]) { if (prop->type != xaBinclips[i]) return; LOG_DEBUG("Sending binary clipboard to clients (%d bytes)", prop->size); vncSendBinaryClipboardData(dlp_get_mimetype(i), prop->data, prop->size); break; } } } } #define SEND_EVENT_BIT 0x80 static int vncProcSendEvent(ClientPtr client) { REQUEST(xSendEventReq); REQUEST_SIZE_MATCH(xSendEventReq); stuff->event.u.u.type &= ~(SEND_EVENT_BIT); if (stuff->event.u.u.type == SelectionNotify && stuff->event.u.selectionNotify.requestor == wid) { TimeStamp time; time = ClientTimeToServerTime(stuff->event.u.selectionNotify.time); vncHandleSelection(stuff->event.u.selectionNotify.selection, stuff->event.u.selectionNotify.target, stuff->event.u.selectionNotify.property, stuff->event.u.selectionNotify.requestor, time); } return origProcSendEvent(client); } static void vncSelectionCallback(CallbackListPtr *callbacks, void * data, void * args) { SelectionInfoRec *info = (SelectionInfoRec *) args; if (info->selection->selection == activeSelection) { LOG_DEBUG("Local clipboard lost, notifying clients"); activeSelection = None; vncAnnounceClipboard(FALSE); } if (info->kind != SelectionSetOwner) return; if (info->client == serverClient) return; LOG_DEBUG("Selection owner change for %s", NameForAtom(info->selection->selection)); if ((info->selection->selection != xaPRIMARY) && (info->selection->selection != xaCLIPBOARD)) return; if ((info->selection->selection == xaPRIMARY) && !vncGetSendPrimary()) return; LOG_DEBUG("Got clipboard notification, probing for formats"); probing = TRUE; vncSelectionRequest(info->selection->selection, xaTARGETS); } static void vncClientStateCallback(CallbackListPtr * l, void * d, void * p) { ClientPtr client = ((NewClientInfoRec*)p)->client; if (client->clientState == ClientStateGone) { struct VncDataTarget** nextPtr = &vncDataTargetHead; for (struct VncDataTarget* cur = vncDataTargetHead; cur; cur = *nextPtr) { if (cur->client == client) { *nextPtr = cur->next; free(cur); continue; } nextPtr = &cur->next; } } } static void vncClearLocalClipboard(Atom selection) { SelectionInfoRec info; Selection *pSel; int rc; rc = dixLookupSelection(&pSel, selection, serverClient, DixSetAttrAccess); if (rc != Success) return; if (pSel->client && (pSel->client != serverClient)) { xEvent event = { .u.selectionClear.time = currentTime.milliseconds, .u.selectionClear.window = pSel->window, .u.selectionClear.atom = pSel->selection }; event.u.u.type = SelectionClear; WriteEventsToClient(pSel->client, 1, &event); } pSel->lastTimeChanged = currentTime; pSel->window = None; pSel->pWin = NULL; pSel->client = NullClient; LOG_DEBUG("Cleared %s selection", NameForAtom(selection)); info.selection = pSel; info.client = serverClient; info.kind = SelectionSetOwner; CallCallbacks(&SelectionCallback, &info); } void vncClearLocalClipboards() { vncClearLocalClipboard(xaPRIMARY); vncClearLocalClipboard(xaCLIPBOARD); }