1/* x-selection.m -- proxies between NSPasteboard and X11 selections
2 *
3 * Copyright (c) 2002-2012 Apple Inc. All rights reserved.
4 *
5 * Permission is hereby granted, free of charge, to any person
6 * obtaining a copy of this software and associated documentation files
7 * (the "Software"), to deal in the Software without restriction,
8 * including without limitation the rights to use, copy, modify, merge,
9 * publish, distribute, sublicense, and/or sell copies of the Software,
10 * and to permit persons to whom the Software is furnished to do so,
11 * subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be
14 * included in all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 * NONINFRINGEMENT.  IN NO EVENT SHALL THE ABOVE LISTED COPYRIGHT
20 * HOLDER(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23 * DEALINGS IN THE SOFTWARE.
24 *
25 * Except as contained in this notice, the name(s) of the above
26 * copyright holders shall not be used in advertising or otherwise to
27 * promote the sale, use or other dealings in this Software without
28 * prior written authorization.
29 */
30
31#import "x-selection.h"
32
33#include <stdio.h>
34#include <stdlib.h>
35#include <X11/Xatom.h>
36#include <X11/Xutil.h>
37#import <AppKit/NSGraphics.h>
38#import <AppKit/NSImage.h>
39#import <AppKit/NSBitmapImageRep.h>
40
41/*
42 * The basic design of the pbproxy code is as follows.
43 *
44 * When a client selects text, say from an xterm - we only copy it when the
45 * X11 Edit->Copy menu item is pressed or the shortcut activated.  In this
46 * case we take the PRIMARY selection, and set it as the NSPasteboard data.
47 *
48 * When an X11 client copies something to the CLIPBOARD, pbproxy greedily grabs
49 * the data, sets it as the NSPasteboard data, and finally sets itself as
50 * owner of the CLIPBOARD.
51 *
52 * When an X11 window is activated we check to see if the NSPasteboard has
53 * changed.  If the NSPasteboard has changed, then we set pbproxy as owner
54 * of the PRIMARY and CLIPBOARD and respond to requests for text and images.
55 *
56 * The behavior is now dynamic since the information above was written.
57 * The behavior is now dependent on the pbproxy_prefs below.
58 */
59
60/*
61 * TODO:
62 * 1. handle MULTIPLE - I need to study the ICCCM further, and find a test app.
63 * 2. Handle NSPasteboard updates immediately, not on active/inactive
64 *    - Open xterm, run 'cat readme.txt | pbcopy'
65 */
66
67static struct {
68    BOOL active;
69    BOOL primary_on_grab; /* This is provided as an option for people who
70                           * want it and has issues that won't ever be
71                           * addressed to make it *always* work.
72                           */
73    BOOL clipboard_to_pasteboard;
74    BOOL pasteboard_to_primary;
75    BOOL pasteboard_to_clipboard;
76} pbproxy_prefs = { YES, NO, YES, YES, YES };
77
78@implementation x_selection
79
80static struct propdata null_propdata = {
81    NULL, 0, 0
82};
83
84#ifdef DEBUG
85static void
86dump_prefs()
87{
88    ErrorF("pbproxy preferences:\n"
89           "\tactive %u\n"
90           "\tprimary_on_grab %u\n"
91           "\tclipboard_to_pasteboard %u\n"
92           "\tpasteboard_to_primary %u\n"
93           "\tpasteboard_to_clipboard %u\n",
94           pbproxy_prefs.active,
95           pbproxy_prefs.primary_on_grab,
96           pbproxy_prefs.clipboard_to_pasteboard,
97           pbproxy_prefs.pasteboard_to_primary,
98           pbproxy_prefs.pasteboard_to_clipboard);
99}
100#endif
101
102extern CFStringRef app_prefs_domain_cfstr;
103
104static BOOL
105prefs_get_bool(CFStringRef key, BOOL defaultValue)
106{
107    Boolean value, ok;
108
109    value = CFPreferencesGetAppBooleanValue(key, app_prefs_domain_cfstr, &ok);
110
111    return ok ? (BOOL)value : defaultValue;
112}
113
114static void
115init_propdata(struct propdata *pdata)
116{
117    *pdata = null_propdata;
118}
119
120static void
121free_propdata(struct propdata *pdata)
122{
123    free(pdata->data);
124    *pdata = null_propdata;
125}
126
127/*
128 * Return True if an error occurs.  Return False if pdata has data
129 * and we finished.
130 * The property is only deleted when bytesleft is 0 if delete is True.
131 */
132static Bool
133get_property(Window win, Atom property, struct propdata *pdata, Bool delete,
134             Atom *type)
135{
136    long offset = 0;
137    unsigned long numitems, bytesleft = 0;
138#ifdef TEST
139    /* This is used to test the growth handling. */
140    unsigned long length = 4UL;
141#else
142    unsigned long length = (100000UL + 3) / 4;
143#endif
144    unsigned char *buf = NULL, *chunk = NULL;
145    size_t buflen = 0, chunkbytesize = 0;
146    int format;
147
148    TRACE();
149
150    if (None == property)
151        return True;
152
153    do {
154        unsigned long newbuflen = 0;
155        unsigned char *newbuf = NULL;
156
157#ifdef TEST
158        ErrorF("bytesleft %lu\n", bytesleft);
159#endif
160
161        if (Success != XGetWindowProperty(xpbproxy_dpy, win, property,
162                                          offset, length, delete,
163                                          AnyPropertyType,
164                                          type, &format, &numitems,
165                                          &bytesleft, &chunk)) {
166            DebugF("Error while getting window property.\n");
167            *pdata = null_propdata;
168            free(buf);
169            return True;
170        }
171
172#ifdef TEST
173        ErrorF("format %d numitems %lu bytesleft %lu\n",
174               format, numitems, bytesleft);
175
176        ErrorF("type %s\n", XGetAtomName(xpbproxy_dpy, *type));
177#endif
178
179        /* Format is the number of bits. */
180        if (format == 8)
181            chunkbytesize = numitems;
182        else if (format == 16)
183            chunkbytesize = numitems * sizeof(short);
184        else if (format == 32)
185            chunkbytesize = numitems * sizeof(long);
186
187#ifdef TEST
188        ErrorF("chunkbytesize %zu\n", chunkbytesize);
189#endif
190        newbuflen = buflen + chunkbytesize;
191        if (newbuflen > 0) {
192            newbuf = realloc(buf, newbuflen);
193
194            if (NULL == newbuf) {
195                XFree(chunk);
196                free(buf);
197                return True;
198            }
199
200            memcpy(newbuf + buflen, chunk, chunkbytesize);
201            XFree(chunk);
202            buf = newbuf;
203            buflen = newbuflen;
204            /* offset is a multiple of 32 bits*/
205            offset += chunkbytesize / 4;
206        }
207        else {
208            if (chunk)
209                XFree(chunk);
210        }
211
212#ifdef TEST
213        ErrorF("bytesleft %lu\n", bytesleft);
214#endif
215    } while (bytesleft > 0);
216
217    pdata->data = buf;
218    pdata->length = buflen;
219    pdata->format = format;
220
221    return /*success*/ False;
222}
223
224/* Implementation methods */
225
226/* This finds the preferred type from a TARGETS list.*/
227- (Atom) find_preferred:(struct propdata *)pdata
228{
229    Atom a = None;
230    size_t i, step;
231    Bool png = False, jpeg = False, utf8 = False, string = False;
232
233    TRACE();
234
235    if (pdata->format != 32) {
236        ErrorF(
237            "Atom list is expected to be formatted as an array of 32bit values.\n");
238        return None;
239    }
240
241    for (i = 0, step = sizeof(long); i < pdata->length; i += step) {
242        a = (Atom) * (long *)(pdata->data + i);
243
244        if (a == atoms->image_png) {
245            png = True;
246        }
247        else if (a == atoms->image_jpeg) {
248            jpeg = True;
249        }
250        else if (a == atoms->utf8_string) {
251            utf8 = True;
252        }
253        else if (a == atoms->string) {
254            string = True;
255        }
256        else {
257            char *type = XGetAtomName(xpbproxy_dpy, a);
258            if (type) {
259                DebugF("Unhandled X11 mime type: %s", type);
260                XFree(type);
261            }
262        }
263    }
264
265    /*We prefer PNG over strings, and UTF8 over a Latin-1 string.*/
266    if (png)
267        return atoms->image_png;
268
269    if (jpeg)
270        return atoms->image_jpeg;
271
272    if (utf8)
273        return atoms->utf8_string;
274
275    if (string)
276        return atoms->string;
277
278    /* This is evidently something we don't know how to handle.*/
279    return None;
280}
281
282/* Return True if this is an INCR-style transfer. */
283- (Bool) is_incr_type:(XSelectionEvent *)e
284{
285    Atom seltype;
286    int format;
287    unsigned long numitems = 0UL, bytesleft = 0UL;
288    unsigned char *chunk;
289
290    TRACE();
291
292    if (Success != XGetWindowProperty(xpbproxy_dpy, e->requestor, e->property,
293                                      /*offset*/ 0L, /*length*/ 4UL,
294                                      /*Delete*/ False,
295                                      AnyPropertyType, &seltype, &format,
296                                      &numitems, &bytesleft, &chunk)) {
297        return False;
298    }
299
300    if (chunk)
301        XFree(chunk);
302
303    return (seltype == atoms->incr) ? True : False;
304}
305
306/*
307 * This should be called after a selection has been copied,
308 * or when the selection is unfinished before a transfer completes.
309 */
310- (void) release_pending
311{
312    TRACE();
313
314    free_propdata(&pending.propdata);
315    pending.requestor = None;
316    pending.selection = None;
317}
318
319/* Return True if an error occurs during an append.*/
320/* Return False if the append succeeds. */
321- (Bool) append_to_pending:(struct propdata *)pdata requestor:(Window)
322   requestor
323{
324    unsigned char *newdata;
325    size_t newlength;
326
327    TRACE();
328
329    if (requestor != pending.requestor) {
330        [self release_pending];
331        pending.requestor = requestor;
332    }
333
334    newlength = pending.propdata.length + pdata->length;
335    newdata = realloc(pending.propdata.data, newlength);
336
337    if (NULL == newdata) {
338        perror("realloc propdata");
339        [self release_pending];
340        return True;
341    }
342
343    memcpy(newdata + pending.propdata.length, pdata->data, pdata->length);
344    pending.propdata.data = newdata;
345    pending.propdata.length = newlength;
346
347    return False;
348}
349
350/* Called when X11 becomes active (i.e. has key focus) */
351- (void) x_active:(Time)timestamp
352{
353    static NSInteger changeCount;
354    NSInteger countNow;
355    NSPasteboard *pb;
356
357    TRACE();
358
359    pb = [NSPasteboard generalPasteboard];
360
361    if (nil == pb)
362        return;
363
364    countNow = [pb changeCount];
365
366    if (countNow != changeCount) {
367        DebugF("changed pasteboard!\n");
368        changeCount = countNow;
369
370        if (pbproxy_prefs.pasteboard_to_primary) {
371            XSetSelectionOwner(xpbproxy_dpy, atoms->primary,
372                               _selection_window,
373                               CurrentTime);
374        }
375
376        if (pbproxy_prefs.pasteboard_to_clipboard) {
377            [self own_clipboard];
378        }
379    }
380
381#if 0
382    /*gstaplin: we should perhaps investigate something like this branch above...*/
383    if ([_pasteboard availableTypeFromArray: _known_types] != nil) {
384        /* Pasteboard has data we should proxy; I think it makes
385           sense to put it on both CLIPBOARD and PRIMARY */
386
387        XSetSelectionOwner(xpbproxy_dpy, atoms->clipboard,
388                           _selection_window, timestamp);
389        XSetSelectionOwner(xpbproxy_dpy, atoms->primary,
390                           _selection_window, timestamp);
391    }
392#endif
393}
394
395/* Called when X11 loses key focus */
396- (void) x_inactive:(Time)timestamp
397{
398    TRACE();
399}
400
401/* This requests the TARGETS list from the PRIMARY selection owner. */
402- (void) x_copy_request_targets
403{
404    TRACE();
405
406    request_atom = atoms->targets;
407    XConvertSelection(xpbproxy_dpy, atoms->primary, atoms->targets,
408                      atoms->primary, _selection_window, CurrentTime);
409}
410
411/* Called when the Edit/Copy item on the main X11 menubar is selected
412 * and no appkit window claims it. */
413- (void) x_copy:(Time)timestamp
414{
415    Window w;
416
417    TRACE();
418
419    w = XGetSelectionOwner(xpbproxy_dpy, atoms->primary);
420
421    if (None != w) {
422        ++pending_copy;
423
424        if (1 == pending_copy) {
425            /*
426             * There are no other copy operations in progress, so we
427             * can proceed safely.  Otherwise the copy_completed method
428             * will see that the pending_copy is > 1, and do another copy.
429             */
430            [self x_copy_request_targets];
431        }
432    }
433}
434
435/* Set pbproxy as owner of the SELECTION_MANAGER selection.
436 * This prevents tools like xclipboard from causing havoc.
437 * Returns TRUE on success
438 */
439- (BOOL) set_clipboard_manager_status:(BOOL)value
440{
441    TRACE();
442
443    Window owner = XGetSelectionOwner(xpbproxy_dpy, atoms->clipboard_manager);
444
445    if (value) {
446        if (owner == _selection_window)
447            return TRUE;
448
449        if (owner != None) {
450            ErrorF(
451                "A clipboard manager using window 0x%lx already owns the clipboard selection.  "
452                "pbproxy will not sync clipboard to pasteboard.\n", owner);
453            return FALSE;
454        }
455
456        XSetSelectionOwner(xpbproxy_dpy, atoms->clipboard_manager,
457                           _selection_window,
458                           CurrentTime);
459        return (_selection_window ==
460                XGetSelectionOwner(xpbproxy_dpy, atoms->clipboard_manager));
461    }
462    else {
463        if (owner != _selection_window)
464            return TRUE;
465
466        XSetSelectionOwner(xpbproxy_dpy, atoms->clipboard_manager, None,
467                           CurrentTime);
468        return (None ==
469                XGetSelectionOwner(xpbproxy_dpy, atoms->clipboard_manager));
470    }
471
472    return FALSE;
473}
474
475/*
476 * This occurs when we previously owned a selection,
477 * and then lost it from another client.
478 */
479- (void) clear_event:(XSelectionClearEvent *)e
480{
481
482    TRACE();
483
484    DebugF("e->selection %s\n", XGetAtomName(xpbproxy_dpy, e->selection));
485
486    if (e->selection == atoms->clipboard) {
487        /*
488         * We lost ownership of the CLIPBOARD.
489         */
490        ++pending_clipboard;
491
492        if (1 == pending_clipboard) {
493            /* Claim the clipboard contents from the new owner. */
494            [self claim_clipboard];
495        }
496    }
497    else if (e->selection == atoms->clipboard_manager) {
498        if (pbproxy_prefs.clipboard_to_pasteboard) {
499            /* Another CLIPBOARD_MANAGER has set itself as owner.  Disable syncing
500             * to avoid a race.
501             */
502            ErrorF("Another clipboard manager was started!  "
503                   "xpbproxy is disabling syncing with clipboard.\n");
504            pbproxy_prefs.clipboard_to_pasteboard = NO;
505        }
506    }
507}
508
509/*
510 * We greedily acquire the clipboard after it changes, and on startup.
511 */
512- (void) claim_clipboard
513{
514    Window owner;
515
516    TRACE();
517
518    if (!pbproxy_prefs.clipboard_to_pasteboard)
519        return;
520
521    owner = XGetSelectionOwner(xpbproxy_dpy, atoms->clipboard);
522    if (None == owner) {
523        /*
524         * The owner probably died or we are just starting up pbproxy.
525         * Set pbproxy's _selection_window as the owner, and continue.
526         */
527        DebugF("No clipboard owner.\n");
528        [self copy_completed:atoms->clipboard];
529        return;
530    }
531    else if (owner == _selection_window) {
532        [self copy_completed:atoms->clipboard];
533        return;
534    }
535
536    DebugF("requesting targets\n");
537
538    request_atom = atoms->targets;
539    XConvertSelection(xpbproxy_dpy, atoms->clipboard, atoms->targets,
540                      atoms->clipboard, _selection_window, CurrentTime);
541    XFlush(xpbproxy_dpy);
542    /* Now we will get a SelectionNotify event in the future. */
543}
544
545/* Greedily acquire the clipboard. */
546- (void) own_clipboard
547{
548
549    TRACE();
550
551    /* We should perhaps have a boundary limit on the number of iterations... */
552    do {
553        XSetSelectionOwner(xpbproxy_dpy, atoms->clipboard, _selection_window,
554                           CurrentTime);
555    } while (_selection_window != XGetSelectionOwner(xpbproxy_dpy,
556                                                     atoms->clipboard));
557}
558
559- (void) init_reply:(XEvent *)reply request:(XSelectionRequestEvent *)e
560{
561    reply->xselection.type = SelectionNotify;
562    reply->xselection.selection = e->selection;
563    reply->xselection.target = e->target;
564    reply->xselection.requestor = e->requestor;
565    reply->xselection.time = e->time;
566    reply->xselection.property = None;
567}
568
569- (void) send_reply:(XEvent *)reply
570{
571    /*
572     * We are supposed to use an empty event mask, and not propagate
573     * the event, according to the ICCCM.
574     */
575    DebugF("reply->xselection.requestor 0x%lx\n", reply->xselection.requestor);
576
577    XSendEvent(xpbproxy_dpy, reply->xselection.requestor, False, 0, reply);
578    XFlush(xpbproxy_dpy);
579}
580
581/*
582 * This responds to a TARGETS request.
583 * The result is a list of a ATOMs that correspond to the types available
584 * for a selection.
585 * For instance an application might provide a UTF8_STRING and a STRING
586 * (in Latin-1 encoding).  The requestor can then make the choice based on
587 * the list.
588 */
589- (void) send_targets:(XSelectionRequestEvent *)e pasteboard:(NSPasteboard *)
590   pb
591{
592    XEvent reply;
593    NSArray *pbtypes;
594
595    [self init_reply:&reply request:e];
596
597    pbtypes = [pb types];
598    if (pbtypes) {
599        long list[7]; /* Don't forget to increase this if we handle more types! */
600        long count = 0;
601
602        /*
603         * I'm not sure if this is needed, but some toolkits/clients list
604         * TARGETS in response to targets.
605         */
606        list[count] = atoms->targets;
607        ++count;
608
609        if ([pbtypes containsObject:NSStringPboardType]) {
610            /* We have a string type that we can convert to UTF8, or Latin-1... */
611            DebugF("NSStringPboardType\n");
612            list[count] = atoms->utf8_string;
613            ++count;
614            list[count] = atoms->string;
615            ++count;
616            list[count] = atoms->compound_text;
617            ++count;
618        }
619
620        /* TODO add the NSPICTPboardType back again, once we have conversion
621         * functionality in send_image.
622         */
623#ifdef __clang__
624#pragma clang diagnostic push
625#pragma clang diagnostic ignored "-Wdeprecated-declarations" // NSPICTPboardType
626#endif
627
628        if ([pbtypes containsObject:NSPICTPboardType]
629            || [pbtypes containsObject:NSTIFFPboardType]) {
630            /* We can convert a TIFF to a PNG or JPEG. */
631            DebugF("NSTIFFPboardType\n");
632            list[count] = atoms->image_png;
633            ++count;
634            list[count] = atoms->image_jpeg;
635            ++count;
636        }
637
638#ifdef __clang__
639#pragma clang diagnostic pop
640#endif
641
642        if (count) {
643            /* We have a list of ATOMs to send. */
644            XChangeProperty(xpbproxy_dpy, e->requestor, e->property,
645                            atoms->atom, 32,
646                            PropModeReplace, (unsigned char *)list,
647                            count);
648
649            reply.xselection.property = e->property;
650        }
651    }
652
653    [self send_reply:&reply];
654}
655
656- (void) send_string:(XSelectionRequestEvent *)e utf8:(BOOL)utf8 pasteboard:(
657       NSPasteboard *)pb
658{
659    XEvent reply;
660    NSArray *pbtypes;
661    NSString *data;
662    const char *bytes;
663    NSUInteger length;
664
665    TRACE();
666
667    [self init_reply:&reply request:e];
668
669    pbtypes = [pb types];
670
671    if (![pbtypes containsObject:NSStringPboardType]) {
672        [self send_reply:&reply];
673        return;
674    }
675
676    DebugF("pbtypes retainCount after containsObject: %lu\n",
677           [pbtypes retainCount]);
678
679    data = [pb stringForType:NSStringPboardType];
680
681    if (nil == data) {
682        [self send_reply:&reply];
683        return;
684    }
685
686    if (utf8) {
687        bytes = [data UTF8String];
688        /*
689         * We don't want the UTF-8 string length here.
690         * We want the length in bytes.
691         */
692        length = strlen(bytes);
693
694        if (length < 50) {
695            DebugF("UTF-8: %s\n", bytes);
696            DebugF("UTF-8 length: %lu\n", length);
697        }
698    }
699    else {
700        DebugF("Latin-1\n");
701        bytes = [data cStringUsingEncoding:NSISOLatin1StringEncoding];
702        /*WARNING: bytes is not NUL-terminated. */
703        length = [data lengthOfBytesUsingEncoding:NSISOLatin1StringEncoding];
704    }
705
706    DebugF("e->target %s\n", XGetAtomName(xpbproxy_dpy, e->target));
707
708    XChangeProperty(xpbproxy_dpy, e->requestor, e->property, e->target,
709                    8, PropModeReplace, (unsigned char *)bytes, length);
710
711    reply.xselection.property = e->property;
712
713    [self send_reply:&reply];
714}
715
716- (void) send_compound_text:(XSelectionRequestEvent *)e pasteboard:(
717       NSPasteboard *)pb
718{
719    XEvent reply;
720    NSArray *pbtypes;
721
722    TRACE();
723
724    [self init_reply:&reply request:e];
725
726    pbtypes = [pb types];
727
728    if ([pbtypes containsObject: NSStringPboardType]) {
729        NSString *data = [pb stringForType:NSStringPboardType];
730        if (nil != data) {
731            /*
732             * Cast to (void *) to avoid a const warning.
733             * AFAIK Xutf8TextListToTextProperty does not modify the input memory.
734             */
735            void *utf8 = (void *)[data UTF8String];
736            char *list[] = { utf8, NULL };
737            XTextProperty textprop;
738
739            textprop.value = NULL;
740
741            if (Success == Xutf8TextListToTextProperty(xpbproxy_dpy, list, 1,
742                                                       XCompoundTextStyle,
743                                                       &textprop)) {
744
745                if (8 != textprop.format)
746                    DebugF(
747                        "textprop.format is unexpectedly not 8 - it's %d instead\n",
748                        textprop.format);
749
750                XChangeProperty(xpbproxy_dpy, e->requestor, e->property,
751                                atoms->compound_text, textprop.format,
752                                PropModeReplace, textprop.value,
753                                textprop.nitems);
754
755                reply.xselection.property = e->property;
756            }
757
758            if (textprop.value)
759                XFree(textprop.value);
760
761        }
762    }
763
764    [self send_reply:&reply];
765}
766
767/* Finding a test application that uses MULTIPLE has proven to be difficult. */
768- (void) send_multiple:(XSelectionRequestEvent *)e
769{
770    XEvent reply;
771
772    TRACE();
773
774    [self init_reply:&reply request:e];
775
776    if (None != e->property) {}
777
778    [self send_reply:&reply];
779}
780
781/* Return nil if an error occurred. */
782/* DO NOT retain the encdata for longer than the length of an event response.
783 * The autorelease pool will reuse/free it.
784 */
785- (NSData *) encode_image_data:(NSData *)data type:(NSBitmapImageFileType)
786   enctype
787{
788    NSBitmapImageRep *bmimage = nil;
789    NSData *encdata = nil;
790    NSDictionary *dict = nil;
791
792    bmimage = [[NSBitmapImageRep alloc] initWithData:data];
793
794    if (nil == bmimage)
795        return nil;
796
797    dict = [[NSDictionary alloc] init];
798    encdata = [bmimage representationUsingType:enctype properties:dict];
799
800    if (nil == encdata) {
801        [dict autorelease];
802        [bmimage autorelease];
803        return nil;
804    }
805
806    [dict autorelease];
807    [bmimage autorelease];
808
809    return encdata;
810}
811
812/* Return YES when an error has occurred when trying to send the PICT. */
813/* The caller should send a default response with a property of None when an error occurs. */
814- (BOOL) send_image_pict_reply:(XSelectionRequestEvent *)e
815                    pasteboard:(NSPasteboard *)pb
816                          type:(NSBitmapImageFileType)imagetype
817{
818    XEvent reply;
819    NSImage *img = nil;
820    NSData *data = nil, *encdata = nil;
821    NSUInteger length;
822    const void *bytes = NULL;
823
824    img = [[NSImage alloc] initWithPasteboard:pb];
825
826    if (nil == img) {
827        return YES;
828    }
829
830    data = [img TIFFRepresentation];
831
832    if (nil == data) {
833        [img autorelease];
834        ErrorF("unable to convert PICT to TIFF!\n");
835        return YES;
836    }
837
838    encdata = [self encode_image_data:data type:imagetype];
839    if (nil == encdata) {
840        [img autorelease];
841        return YES;
842    }
843
844    [self init_reply:&reply request:e];
845
846    length = [encdata length];
847    bytes = [encdata bytes];
848
849    XChangeProperty(xpbproxy_dpy, e->requestor, e->property, e->target,
850                    8, PropModeReplace, bytes, length);
851    reply.xselection.property = e->property;
852
853    [self send_reply:&reply];
854
855    [img autorelease];
856
857    return NO; /*no error*/
858}
859
860/* Return YES if an error occurred. */
861/* The caller should send a reply with a property of None when an error occurs. */
862- (BOOL) send_image_tiff_reply:(XSelectionRequestEvent *)e
863                    pasteboard:(NSPasteboard *)pb
864                          type:(NSBitmapImageFileType)imagetype
865{
866    XEvent reply;
867    NSData *data = nil;
868    NSData *encdata = nil;
869    NSUInteger length;
870    const void *bytes = NULL;
871
872    data = [pb dataForType:NSTIFFPboardType];
873
874    if (nil == data)
875        return YES;
876
877    encdata = [self encode_image_data:data type:imagetype];
878
879    if (nil == encdata)
880        return YES;
881
882    [self init_reply:&reply request:e];
883
884    length = [encdata length];
885    bytes = [encdata bytes];
886
887    XChangeProperty(xpbproxy_dpy, e->requestor, e->property, e->target,
888                    8, PropModeReplace, bytes, length);
889    reply.xselection.property = e->property;
890
891    [self send_reply:&reply];
892
893    return NO; /*no error*/
894}
895
896- (void) send_image:(XSelectionRequestEvent *)e pasteboard:(NSPasteboard *)pb
897{
898    NSArray *pbtypes = nil;
899    NSBitmapImageFileType imagetype = NSPNGFileType;
900
901    TRACE();
902
903    if (e->target == atoms->image_png)
904        imagetype = NSPNGFileType;
905    else if (e->target == atoms->image_jpeg)
906        imagetype = NSJPEGFileType;
907    else {
908        ErrorF(
909            "internal failure in xpbproxy!  imagetype being sent isn't PNG or JPEG.\n");
910    }
911
912    pbtypes = [pb types];
913
914    if (pbtypes) {
915        if ([pbtypes containsObject:NSTIFFPboardType]) {
916            if (NO ==
917                [self send_image_tiff_reply:e pasteboard:pb type:imagetype])
918                return;
919        }
920#ifdef __clang__
921#pragma clang diagnostic push
922#pragma clang diagnostic ignored "-Wdeprecated-declarations" // NSPICTPboardType
923#endif
924        else if ([pbtypes containsObject:NSPICTPboardType])
925#ifdef __clang__
926#pragma clang diagnostic pop
927#endif
928        {
929            if (NO ==
930                [self send_image_pict_reply:e pasteboard:pb type:imagetype])
931                return;
932
933            /* Fall through intentionally to the send_none: */
934        }
935    }
936
937    [self send_none:e];
938}
939
940- (void)send_none:(XSelectionRequestEvent *)e
941{
942    XEvent reply;
943
944    TRACE();
945
946    [self init_reply:&reply request:e];
947    [self send_reply:&reply];
948}
949
950/* Another client requested the data or targets of data available from the clipboard. */
951- (void)request_event:(XSelectionRequestEvent *)e
952{
953    NSPasteboard *pb;
954
955    TRACE();
956
957    /* TODO We should also keep track of the time of the selection, and
958     * according to the ICCCM "refuse the request" if the event timestamp
959     * is before we owned it.
960     * What should we base the time on?  How can we get the current time just
961     * before an XSetSelectionOwner?  Is it the server's time, or the clients?
962     * According to the XSelectionRequestEvent manual page, the Time value
963     * may be set to CurrentTime or a time, so that makes it a bit different.
964     * Perhaps we should just punt and ignore races.
965     */
966
967    /*TODO we need a COMPOUND_TEXT test app*/
968    /*TODO we need a MULTIPLE test app*/
969
970    pb = [NSPasteboard generalPasteboard];
971    if (nil == pb) {
972        [self send_none:e];
973        return;
974    }
975
976    if (None != e->target)
977        DebugF("e->target %s\n", XGetAtomName(xpbproxy_dpy, e->target));
978
979    if (e->target == atoms->targets) {
980        /* The paste requestor wants to know what TARGETS we support. */
981        [self send_targets:e pasteboard:pb];
982    }
983    else if (e->target == atoms->multiple) {
984        /*
985         * This isn't finished, and may never be, unless I can find
986         * a good test app.
987         */
988        [self send_multiple:e];
989    }
990    else if (e->target == atoms->utf8_string) {
991        [self send_string:e utf8:YES pasteboard:pb];
992    }
993    else if (e->target == atoms->string) {
994        [self send_string:e utf8:NO pasteboard:pb];
995    }
996    else if (e->target == atoms->compound_text) {
997        [self send_compound_text:e pasteboard:pb];
998    }
999    else if (e->target == atoms->multiple) {
1000        [self send_multiple:e];
1001    }
1002    else if (e->target == atoms->image_png || e->target ==
1003             atoms->image_jpeg) {
1004        [self send_image:e pasteboard:pb];
1005    }
1006    else {
1007        [self send_none:e];
1008    }
1009}
1010
1011/* This handles the events resulting from an XConvertSelection request. */
1012- (void) notify_event:(XSelectionEvent *)e
1013{
1014    Atom type;
1015    struct propdata pdata;
1016
1017    TRACE();
1018
1019    [self release_pending];
1020
1021    if (None == e->property) {
1022        DebugF("e->property is None.\n");
1023        [self copy_completed:e->selection];
1024        /* Nothing is selected. */
1025        return;
1026    }
1027
1028#if 0
1029    ErrorF("e->selection %s\n", XGetAtomName(xpbproxy_dpy, e->selection));
1030    ErrorF("e->property %s\n", XGetAtomName(xpbproxy_dpy, e->property));
1031#endif
1032
1033    if ([self is_incr_type:e]) {
1034        /*
1035         * This is an INCR-style transfer, which means that we
1036         * will get the data after a series of PropertyNotify events.
1037         */
1038        DebugF("is INCR\n");
1039
1040        if (get_property(e->requestor, e->property, &pdata, /*Delete*/ True,
1041                         &type)) {
1042            /*
1043             * An error occurred, so we should invoke the copy_completed:, but
1044             * not handle_selection:type:propdata:
1045             */
1046            [self copy_completed:e->selection];
1047            return;
1048        }
1049
1050        free_propdata(&pdata);
1051
1052        pending.requestor = e->requestor;
1053        pending.selection = e->selection;
1054
1055        DebugF("set pending.requestor to 0x%lx\n", pending.requestor);
1056    }
1057    else {
1058        if (get_property(e->requestor, e->property, &pdata, /*Delete*/ True,
1059                         &type)) {
1060            [self copy_completed:e->selection];
1061            return;
1062        }
1063
1064        /* We have the complete selection data.*/
1065        [self handle_selection:e->selection type:type propdata:&pdata];
1066
1067        DebugF("handled selection with the first notify_event\n");
1068    }
1069}
1070
1071/* This is used for INCR transfers.  See the ICCCM for the details. */
1072/* This is used to retrieve PRIMARY and CLIPBOARD selections. */
1073- (void) property_event:(XPropertyEvent *)e
1074{
1075    struct propdata pdata;
1076    Atom type;
1077
1078    TRACE();
1079
1080    if (None != e->atom) {
1081#ifdef DEBUG
1082        char *name = XGetAtomName(xpbproxy_dpy, e->atom);
1083
1084        if (name) {
1085            DebugF("e->atom %s\n", name);
1086            XFree(name);
1087        }
1088#endif
1089    }
1090
1091    if (None != pending.requestor && PropertyNewValue == e->state) {
1092        DebugF("pending.requestor 0x%lx\n", pending.requestor);
1093
1094        if (get_property(e->window, e->atom, &pdata, /*Delete*/ True,
1095                         &type)) {
1096            [self copy_completed:pending.selection];
1097            [self release_pending];
1098            return;
1099        }
1100
1101        if (0 == pdata.length) {
1102            /*
1103             * We completed the transfer.
1104             * handle_selection will call copy_completed: for us.
1105             */
1106            [self handle_selection:pending.selection type:type propdata:&
1107             pending.propdata];
1108            free_propdata(&pdata);
1109            pending.propdata = null_propdata;
1110            pending.requestor = None;
1111            pending.selection = None;
1112        }
1113        else {
1114            [self append_to_pending:&pdata requestor:e->window];
1115            free_propdata(&pdata);
1116        }
1117    }
1118}
1119
1120- (void) xfixes_selection_notify:(XFixesSelectionNotifyEvent *)e
1121{
1122    if (!pbproxy_prefs.active)
1123        return;
1124
1125    switch (e->subtype) {
1126    case XFixesSetSelectionOwnerNotify:
1127        if (e->selection == atoms->primary && pbproxy_prefs.primary_on_grab)
1128            [self x_copy:e->timestamp];
1129        break;
1130
1131    case XFixesSelectionWindowDestroyNotify:
1132    case XFixesSelectionClientCloseNotify:
1133    default:
1134        ErrorF("Unhandled XFixesSelectionNotifyEvent: subtype=%d\n",
1135               e->subtype);
1136        break;
1137    }
1138}
1139
1140- (void) handle_targets: (Atom)selection propdata:(struct propdata *)pdata
1141{
1142    /* Find a type we can handle and prefer from the list of ATOMs. */
1143    Atom preferred;
1144    char *name;
1145
1146    TRACE();
1147
1148    preferred = [self find_preferred:pdata];
1149
1150    if (None == preferred) {
1151        /*
1152         * This isn't required by the ICCCM, but some apps apparently
1153         * don't respond to TARGETS properly.
1154         */
1155        preferred = atoms->string;
1156    }
1157
1158    (void)name; /* Avoid a warning with non-debug compiles. */
1159#ifdef DEBUG
1160    name = XGetAtomName(xpbproxy_dpy, preferred);
1161
1162    if (name) {
1163        DebugF("requesting %s\n", name);
1164    }
1165#endif
1166    request_atom = preferred;
1167    XConvertSelection(xpbproxy_dpy, selection, preferred, selection,
1168                      _selection_window, CurrentTime);
1169}
1170
1171/* This handles the image type of selection (typically in CLIPBOARD). */
1172/* We convert to a TIFF, so that other applications can paste more easily. */
1173- (void) handle_image: (struct propdata *)pdata pasteboard:(NSPasteboard *)pb
1174{
1175    NSArray *pbtypes;
1176    NSUInteger length;
1177    NSData *data, *tiff;
1178    NSBitmapImageRep *bmimage;
1179
1180    TRACE();
1181
1182    length = pdata->length;
1183    data = [[NSData alloc] initWithBytes:pdata->data length:length];
1184
1185    if (nil == data) {
1186        DebugF("unable to create NSData object!\n");
1187        return;
1188    }
1189
1190    DebugF("data retainCount before NSBitmapImageRep initWithData: %lu\n",
1191           [data retainCount]);
1192
1193    bmimage = [[NSBitmapImageRep alloc] initWithData:data];
1194
1195    if (nil == bmimage) {
1196        [data autorelease];
1197        DebugF("unable to create NSBitmapImageRep!\n");
1198        return;
1199    }
1200
1201    DebugF("data retainCount after NSBitmapImageRep initWithData: %lu\n",
1202           [data retainCount]);
1203
1204    @try
1205    {
1206        tiff = [bmimage TIFFRepresentation];
1207    }
1208
1209    @catch (NSException *e)
1210    {
1211        DebugF("NSTIFFException!\n");
1212        [data autorelease];
1213        [bmimage autorelease];
1214        return;
1215    }
1216
1217    DebugF("bmimage retainCount after TIFFRepresentation %lu\n",
1218           [bmimage retainCount]);
1219
1220    pbtypes = [NSArray arrayWithObjects:NSTIFFPboardType, nil];
1221
1222    if (nil == pbtypes) {
1223        [data autorelease];
1224        [bmimage autorelease];
1225        return;
1226    }
1227
1228    [pb declareTypes:pbtypes owner:nil];
1229    if (YES != [pb setData:tiff forType:NSTIFFPboardType]) {
1230        DebugF("writing pasteboard data failed!\n");
1231    }
1232
1233    [data autorelease];
1234
1235    DebugF("bmimage retainCount before release %lu\n", [bmimage retainCount]);
1236
1237    [bmimage autorelease];
1238}
1239
1240/* This handles the UTF8_STRING type of selection. */
1241- (void) handle_utf8_string:(struct propdata *)pdata pasteboard:(NSPasteboard
1242                                                                 *)pb
1243{
1244    NSString *string;
1245    NSArray *pbtypes;
1246
1247    TRACE();
1248
1249    string =
1250        [[NSString alloc] initWithBytes:pdata->data length:pdata->length
1251         encoding:
1252         NSUTF8StringEncoding];
1253
1254    if (nil == string)
1255        return;
1256
1257    pbtypes = [NSArray arrayWithObjects:NSStringPboardType, nil];
1258
1259    if (nil == pbtypes) {
1260        [string autorelease];
1261        return;
1262    }
1263
1264    [pb declareTypes:pbtypes owner:nil];
1265
1266    if (YES != [pb setString:string forType:NSStringPboardType]) {
1267        ErrorF("pasteboard setString:forType: failed!\n");
1268    }
1269    [string autorelease];
1270    DebugF("done handling utf8 string\n");
1271}
1272
1273/* This handles the STRING type, which should be in Latin-1. */
1274- (void) handle_string: (struct propdata *)pdata pasteboard:(NSPasteboard *)
1275   pb
1276{
1277    NSString *string;
1278    NSArray *pbtypes;
1279
1280    TRACE();
1281
1282    string =
1283        [[NSString alloc] initWithBytes:pdata->data length:pdata->length
1284         encoding:
1285         NSISOLatin1StringEncoding];
1286
1287    if (nil == string)
1288        return;
1289
1290    pbtypes = [NSArray arrayWithObjects:NSStringPboardType, nil];
1291
1292    if (nil == pbtypes) {
1293        [string autorelease];
1294        return;
1295    }
1296
1297    [pb declareTypes:pbtypes owner:nil];
1298    if (YES != [pb setString:string forType:NSStringPboardType]) {
1299        ErrorF("pasteboard setString:forType failed in handle_string!\n");
1300    }
1301    [string autorelease];
1302}
1303
1304/* This is called when the selection is completely retrieved from another client. */
1305/* Warning: this frees the propdata. */
1306- (void) handle_selection:(Atom)selection type:(Atom)type propdata:(struct
1307                                                                    propdata
1308                                                                    *)pdata
1309{
1310    NSPasteboard *pb;
1311
1312    TRACE();
1313
1314    pb = [NSPasteboard generalPasteboard];
1315
1316    if (nil == pb) {
1317        [self copy_completed:selection];
1318        free_propdata(pdata);
1319        return;
1320    }
1321
1322    /*
1323     * Some apps it seems set the type to TARGETS instead of ATOM, such as Eterm.
1324     * These aren't ICCCM compliant apps, but we need these to work...
1325     */
1326    if (request_atom == atoms->targets
1327        && (type == atoms->atom || type == atoms->targets)) {
1328        [self handle_targets:selection propdata:pdata];
1329        free_propdata(pdata);
1330        return;
1331    }
1332    else if (type == atoms->image_png) {
1333        [self handle_image:pdata pasteboard:pb];
1334    }
1335    else if (type == atoms->image_jpeg) {
1336        [self handle_image:pdata pasteboard:pb];
1337    }
1338    else if (type == atoms->utf8_string) {
1339        [self handle_utf8_string:pdata pasteboard:pb];
1340    }
1341    else if (type == atoms->string) {
1342        [self handle_string:pdata pasteboard:pb];
1343    }
1344
1345    free_propdata(pdata);
1346
1347    [self copy_completed:selection];
1348}
1349
1350- (void) copy_completed:(Atom)selection
1351{
1352    TRACE();
1353    char *name;
1354
1355    (void)name; /* Avoid warning with non-debug compiles. */
1356#ifdef DEBUG
1357    name = XGetAtomName(xpbproxy_dpy, selection);
1358    if (name) {
1359        DebugF("copy_completed: %s\n", name);
1360        XFree(name);
1361    }
1362#endif
1363
1364    if (selection == atoms->primary && pending_copy > 0) {
1365        --pending_copy;
1366        if (pending_copy > 0) {
1367            /* Copy PRIMARY again. */
1368            [self x_copy_request_targets];
1369            return;
1370        }
1371    }
1372    else if (selection == atoms->clipboard && pending_clipboard > 0) {
1373        --pending_clipboard;
1374        if (pending_clipboard > 0) {
1375            /* Copy CLIPBOARD. */
1376            [self claim_clipboard];
1377            return;
1378        }
1379        else {
1380            /* We got the final data.  Now set pbproxy as the owner. */
1381            [self own_clipboard];
1382            return;
1383        }
1384    }
1385
1386    /*
1387     * We had 1 or more primary in progress, and the clipboard arrived
1388     * while we were busy.
1389     */
1390    if (pending_clipboard > 0) {
1391        [self claim_clipboard];
1392    }
1393}
1394
1395- (void) reload_preferences
1396{
1397    /*
1398     * It's uncertain how we could handle the synchronization failing, so cast to void.
1399     * The prefs_get_bool should fall back to defaults if the org.x.X11 plist doesn't exist or is invalid.
1400     */
1401    (void)CFPreferencesAppSynchronize(app_prefs_domain_cfstr);
1402#ifdef STANDALONE_XPBPROXY
1403    if (xpbproxy_is_standalone)
1404        pbproxy_prefs.active = YES;
1405    else
1406#endif
1407    pbproxy_prefs.active = prefs_get_bool(CFSTR(
1408                                              "sync_pasteboard"),
1409                                          pbproxy_prefs.active);
1410    pbproxy_prefs.primary_on_grab =
1411        prefs_get_bool(CFSTR(
1412                           "sync_primary_on_select"),
1413                       pbproxy_prefs.primary_on_grab);
1414    pbproxy_prefs.clipboard_to_pasteboard =
1415        prefs_get_bool(CFSTR(
1416                           "sync_clipboard_to_pasteboard"),
1417                       pbproxy_prefs.clipboard_to_pasteboard);
1418    pbproxy_prefs.pasteboard_to_primary =
1419        prefs_get_bool(CFSTR(
1420                           "sync_pasteboard_to_primary"),
1421                       pbproxy_prefs.pasteboard_to_primary);
1422    pbproxy_prefs.pasteboard_to_clipboard =
1423        prefs_get_bool(CFSTR(
1424                           "sync_pasteboard_to_clipboard"),
1425                       pbproxy_prefs.pasteboard_to_clipboard);
1426
1427    /* This is used for debugging. */
1428    //dump_prefs();
1429
1430    if (pbproxy_prefs.active && pbproxy_prefs.primary_on_grab &&
1431        !xpbproxy_have_xfixes) {
1432        ErrorF(
1433            "Disabling sync_primary_on_select functionality due to missing XFixes extension.\n");
1434        pbproxy_prefs.primary_on_grab = NO;
1435    }
1436
1437    /* Claim or release the CLIPBOARD_MANAGER atom */
1438    if (![self set_clipboard_manager_status:(pbproxy_prefs.active &&
1439                                             pbproxy_prefs.
1440                                             clipboard_to_pasteboard)])
1441        pbproxy_prefs.clipboard_to_pasteboard = NO;
1442
1443    if (pbproxy_prefs.active && pbproxy_prefs.clipboard_to_pasteboard)
1444        [self claim_clipboard];
1445}
1446
1447- (BOOL) is_active
1448{
1449    return pbproxy_prefs.active;
1450}
1451
1452/* NSPasteboard-required methods */
1453
1454- (void) paste:(id)sender
1455{
1456    TRACE();
1457}
1458
1459- (void) pasteboard:(NSPasteboard *)pb provideDataForType:(NSString *)type
1460{
1461    TRACE();
1462}
1463
1464- (void) pasteboardChangedOwner:(NSPasteboard *)pb
1465{
1466    TRACE();
1467
1468    /* Right now we don't care with this. */
1469}
1470
1471/* Allocation */
1472
1473- (id) init
1474{
1475    unsigned long pixel;
1476
1477    self = [super init];
1478    if (self == nil)
1479        return nil;
1480
1481    atoms->primary = XInternAtom(xpbproxy_dpy, "PRIMARY", False);
1482    atoms->clipboard = XInternAtom(xpbproxy_dpy, "CLIPBOARD", False);
1483    atoms->text = XInternAtom(xpbproxy_dpy, "TEXT", False);
1484    atoms->utf8_string = XInternAtom(xpbproxy_dpy, "UTF8_STRING", False);
1485    atoms->string = XInternAtom(xpbproxy_dpy, "STRING", False);
1486    atoms->targets = XInternAtom(xpbproxy_dpy, "TARGETS", False);
1487    atoms->multiple = XInternAtom(xpbproxy_dpy, "MULTIPLE", False);
1488    atoms->cstring = XInternAtom(xpbproxy_dpy, "CSTRING", False);
1489    atoms->image_png = XInternAtom(xpbproxy_dpy, "image/png", False);
1490    atoms->image_jpeg = XInternAtom(xpbproxy_dpy, "image/jpeg", False);
1491    atoms->incr = XInternAtom(xpbproxy_dpy, "INCR", False);
1492    atoms->atom = XInternAtom(xpbproxy_dpy, "ATOM", False);
1493    atoms->clipboard_manager = XInternAtom(xpbproxy_dpy, "CLIPBOARD_MANAGER",
1494                                           False);
1495    atoms->compound_text = XInternAtom(xpbproxy_dpy, "COMPOUND_TEXT", False);
1496    atoms->atom_pair = XInternAtom(xpbproxy_dpy, "ATOM_PAIR", False);
1497
1498    pixel = BlackPixel(xpbproxy_dpy, DefaultScreen(xpbproxy_dpy));
1499    _selection_window =
1500        XCreateSimpleWindow(xpbproxy_dpy, DefaultRootWindow(xpbproxy_dpy),
1501                            0, 0, 1, 1, 0, pixel, pixel);
1502
1503    /* This is used to get PropertyNotify events when doing INCR transfers. */
1504    XSelectInput(xpbproxy_dpy, _selection_window, PropertyChangeMask);
1505
1506    request_atom = None;
1507
1508    init_propdata(&pending.propdata);
1509    pending.requestor = None;
1510    pending.selection = None;
1511
1512    pending_copy = 0;
1513    pending_clipboard = 0;
1514
1515    if (xpbproxy_have_xfixes)
1516        XFixesSelectSelectionInput(xpbproxy_dpy, _selection_window,
1517                                   atoms->primary,
1518                                   XFixesSetSelectionOwnerNotifyMask);
1519
1520    [self reload_preferences];
1521
1522    return self;
1523}
1524
1525- (void) dealloc
1526{
1527    if (None != _selection_window) {
1528        XDestroyWindow(xpbproxy_dpy, _selection_window);
1529        _selection_window = None;
1530    }
1531
1532    free_propdata(&pending.propdata);
1533
1534    [super dealloc];
1535}
1536
1537@end
1538