1/*
2 * Copyright (C) 2010 Martin Robinson <mrobinson@webkit.org>
3 * Copyright (C) Igalia S.L.
4 * Copyright (C) 2013 Collabora Ltd.
5 * All rights reserved.
6 *
7 * This library is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Library General Public
9 * License as published by the Free Software Foundation; either
10 * version 2 of the License, or (at your option) any later version.
11 *
12 * This library is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 * Library General Public License for more details.
16 *
17 * You should have received a copy of the GNU Library General Public License
18 * along with this library; see the file COPYING.LIB.  If not, write to
19 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20 * Boston, MA 02110-1301, USA.
21 *
22 */
23#include "config.h"
24#include "PasteboardHelper.h"
25
26#include "Chrome.h"
27#include "DataObjectGtk.h"
28#include "Frame.h"
29#include "GRefPtrGtk.h"
30#include "GtkVersioning.h"
31#include "Page.h"
32#include "Pasteboard.h"
33#include "TextResourceDecoder.h"
34#include <gtk/gtk.h>
35#include <wtf/gobject/GUniquePtr.h>
36
37namespace WebCore {
38
39static GdkAtom textPlainAtom;
40static GdkAtom markupAtom;
41static GdkAtom netscapeURLAtom;
42static GdkAtom uriListAtom;
43static GdkAtom smartPasteAtom;
44static GdkAtom unknownAtom;
45
46static String gMarkupPrefix;
47
48static void removeMarkupPrefix(String& markup)
49{
50    // The markup prefix is not harmful, but we remove it from the string anyway, so that
51    // we can have consistent results with other ports during the layout tests.
52    if (markup.startsWith(gMarkupPrefix))
53        markup.remove(0, gMarkupPrefix.length());
54}
55
56static void initGdkAtoms()
57{
58    static gboolean initialized = FALSE;
59
60    if (initialized)
61        return;
62
63    initialized = TRUE;
64
65    textPlainAtom = gdk_atom_intern("text/plain;charset=utf-8", FALSE);
66    markupAtom = gdk_atom_intern("text/html", FALSE);
67    netscapeURLAtom = gdk_atom_intern("_NETSCAPE_URL", FALSE);
68    uriListAtom = gdk_atom_intern("text/uri-list", FALSE);
69    smartPasteAtom = gdk_atom_intern("application/vnd.webkitgtk.smartpaste", FALSE);
70    unknownAtom = gdk_atom_intern("application/vnd.webkitgtk.unknown", FALSE);
71    gMarkupPrefix = "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">";
72}
73
74PasteboardHelper* PasteboardHelper::defaultPasteboardHelper()
75{
76    DEPRECATED_DEFINE_STATIC_LOCAL(PasteboardHelper, defaultHelper, ());
77    return &defaultHelper;
78}
79
80PasteboardHelper::PasteboardHelper()
81    : m_targetList(gtk_target_list_new(0, 0))
82{
83    initGdkAtoms();
84
85    gtk_target_list_add_text_targets(m_targetList, PasteboardHelper::TargetTypeText);
86    gtk_target_list_add(m_targetList, markupAtom, 0, PasteboardHelper::TargetTypeMarkup);
87    gtk_target_list_add_uri_targets(m_targetList, PasteboardHelper::TargetTypeURIList);
88    gtk_target_list_add(m_targetList, netscapeURLAtom, 0, PasteboardHelper::TargetTypeNetscapeURL);
89    gtk_target_list_add_image_targets(m_targetList, PasteboardHelper::TargetTypeImage, TRUE);
90    gtk_target_list_add(m_targetList, unknownAtom, 0, PasteboardHelper::TargetTypeUnknown);
91}
92
93PasteboardHelper::~PasteboardHelper()
94{
95    gtk_target_list_unref(m_targetList);
96}
97
98static inline GdkDisplay* displayFromFrame(Frame* frame)
99{
100    ASSERT(frame);
101    Page* page = frame->page();
102    ASSERT(page);
103    PlatformPageClient client = page->chrome().platformPageClient();
104    return client ? gtk_widget_get_display(client) : gdk_display_get_default();
105}
106
107GtkClipboard* PasteboardHelper::getPrimarySelectionClipboard(Frame* frame) const
108{
109    return gtk_clipboard_get_for_display(displayFromFrame(frame), GDK_SELECTION_PRIMARY);
110}
111
112GtkTargetList* PasteboardHelper::targetList() const
113{
114    return m_targetList;
115}
116
117static String selectionDataToUTF8String(GtkSelectionData* data)
118{
119    // g_strndup guards against selection data that is not null-terminated.
120    GUniquePtr<gchar> markupString(g_strndup(reinterpret_cast<const char*>(gtk_selection_data_get_data(data)), gtk_selection_data_get_length(data)));
121    return String::fromUTF8(markupString.get());
122}
123
124void PasteboardHelper::getClipboardContents(GtkClipboard* clipboard)
125{
126    DataObjectGtk* dataObject = DataObjectGtk::forClipboard(clipboard);
127    ASSERT(dataObject);
128
129    if (gtk_clipboard_wait_is_text_available(clipboard)) {
130        GUniquePtr<gchar> textData(gtk_clipboard_wait_for_text(clipboard));
131        if (textData)
132            dataObject->setText(String::fromUTF8(textData.get()));
133    }
134
135    if (gtk_clipboard_wait_is_target_available(clipboard, markupAtom)) {
136        if (GtkSelectionData* data = gtk_clipboard_wait_for_contents(clipboard, markupAtom)) {
137            String markup(selectionDataToUTF8String(data));
138            removeMarkupPrefix(markup);
139            dataObject->setMarkup(markup);
140            gtk_selection_data_free(data);
141        }
142    }
143
144    if (gtk_clipboard_wait_is_target_available(clipboard, uriListAtom)) {
145        if (GtkSelectionData* data = gtk_clipboard_wait_for_contents(clipboard, uriListAtom)) {
146            dataObject->setURIList(selectionDataToUTF8String(data));
147            gtk_selection_data_free(data);
148        }
149    }
150}
151
152void PasteboardHelper::fillSelectionData(GtkSelectionData* selectionData, guint info, DataObjectGtk* dataObject)
153{
154    if (info == TargetTypeText)
155        gtk_selection_data_set_text(selectionData, dataObject->text().utf8().data(), -1);
156
157    else if (info == TargetTypeMarkup) {
158        // Some Linux applications refuse to accept pasted markup unless it is
159        // prefixed by a content-type meta tag.
160        CString markup = String(gMarkupPrefix + dataObject->markup()).utf8();
161        gtk_selection_data_set(selectionData, markupAtom, 8,
162            reinterpret_cast<const guchar*>(markup.data()), markup.length());
163
164    } else if (info == TargetTypeURIList) {
165        CString uriList = dataObject->uriList().utf8();
166        gtk_selection_data_set(selectionData, uriListAtom, 8,
167            reinterpret_cast<const guchar*>(uriList.data()), uriList.length());
168
169    } else if (info == TargetTypeNetscapeURL && dataObject->hasURL()) {
170        String url(dataObject->url());
171        String result(url);
172        result.append("\n");
173
174        if (dataObject->hasText())
175            result.append(dataObject->text());
176        else
177            result.append(url);
178
179        GUniquePtr<gchar> resultData(g_strdup(result.utf8().data()));
180        gtk_selection_data_set(selectionData, netscapeURLAtom, 8,
181            reinterpret_cast<const guchar*>(resultData.get()), strlen(resultData.get()));
182
183    } else if (info == TargetTypeImage)
184        gtk_selection_data_set_pixbuf(selectionData, dataObject->image());
185
186    else if (info == TargetTypeSmartPaste)
187        gtk_selection_data_set_text(selectionData, "", -1);
188
189    else if (info == TargetTypeUnknown) {
190        GVariantBuilder builder;
191        g_variant_builder_init(&builder, G_VARIANT_TYPE_ARRAY);
192
193        auto types = dataObject->unknownTypes();
194        auto end = types.end();
195        for (auto it = types.begin(); it != end; ++it) {
196            GUniquePtr<gchar> dictItem(g_strdup_printf("{'%s', '%s'}", it->key.utf8().data(), it->value.utf8().data()));
197            g_variant_builder_add_parsed(&builder, dictItem.get());
198        }
199
200        GRefPtr<GVariant> variant = g_variant_builder_end(&builder);
201        GUniquePtr<gchar> serializedVariant(g_variant_print(variant.get(), TRUE));
202        gtk_selection_data_set(selectionData, unknownAtom, 1, reinterpret_cast<const guchar*>(serializedVariant.get()), strlen(serializedVariant.get()));
203    }
204}
205
206GtkTargetList* PasteboardHelper::targetListForDataObject(DataObjectGtk* dataObject, SmartPasteInclusion shouldInludeSmartPaste)
207{
208    GtkTargetList* list = gtk_target_list_new(0, 0);
209
210    if (dataObject->hasText())
211        gtk_target_list_add_text_targets(list, TargetTypeText);
212
213    if (dataObject->hasMarkup())
214        gtk_target_list_add(list, markupAtom, 0, TargetTypeMarkup);
215
216    if (dataObject->hasURIList()) {
217        gtk_target_list_add_uri_targets(list, TargetTypeURIList);
218        gtk_target_list_add(list, netscapeURLAtom, 0, TargetTypeNetscapeURL);
219    }
220
221    if (dataObject->hasImage())
222        gtk_target_list_add_image_targets(list, TargetTypeImage, TRUE);
223
224    if (dataObject->hasUnknownTypeData())
225        gtk_target_list_add(list, unknownAtom, 0, TargetTypeUnknown);
226
227    if (shouldInludeSmartPaste == IncludeSmartPaste)
228        gtk_target_list_add(list, smartPasteAtom, 0, TargetTypeSmartPaste);
229
230    return list;
231}
232
233void PasteboardHelper::fillDataObjectFromDropData(GtkSelectionData* data, guint /* info */, DataObjectGtk* dataObject)
234{
235    if (!gtk_selection_data_get_data(data))
236        return;
237
238    GdkAtom target = gtk_selection_data_get_target(data);
239    if (target == textPlainAtom)
240        dataObject->setText(selectionDataToUTF8String(data));
241    else if (target == markupAtom) {
242        String markup(selectionDataToUTF8String(data));
243        removeMarkupPrefix(markup);
244        dataObject->setMarkup(markup);
245    } else if (target == uriListAtom) {
246        dataObject->setURIList(selectionDataToUTF8String(data));
247    } else if (target == netscapeURLAtom) {
248        String urlWithLabel(selectionDataToUTF8String(data));
249        Vector<String> pieces;
250        urlWithLabel.split("\n", pieces);
251
252        // Give preference to text/uri-list here, as it can hold more
253        // than one URI but still take  the label if there is one.
254        if (!dataObject->hasURIList())
255            dataObject->setURIList(pieces[0]);
256        if (pieces.size() > 1)
257            dataObject->setText(pieces[1]);
258    } else if (target == unknownAtom) {
259        GRefPtr<GVariant> variant = g_variant_new_parsed(reinterpret_cast<const char*>(gtk_selection_data_get_data(data)));
260
261        GUniqueOutPtr<gchar> key;
262        GUniqueOutPtr<gchar> value;
263        GVariantIter iter;
264
265        g_variant_iter_init(&iter, variant.get());
266        while (g_variant_iter_next(&iter, "{ss}", &key.outPtr(), &value.outPtr()))
267            dataObject->setUnknownTypeData(key.get(), value.get());
268    }
269}
270
271Vector<GdkAtom> PasteboardHelper::dropAtomsForContext(GtkWidget* widget, GdkDragContext* context)
272{
273    // Always search for these common atoms.
274    Vector<GdkAtom> dropAtoms;
275    dropAtoms.append(textPlainAtom);
276    dropAtoms.append(markupAtom);
277    dropAtoms.append(uriListAtom);
278    dropAtoms.append(netscapeURLAtom);
279    dropAtoms.append(unknownAtom);
280
281    // For images, try to find the most applicable image type.
282    GRefPtr<GtkTargetList> list = adoptGRef(gtk_target_list_new(0, 0));
283    gtk_target_list_add_image_targets(list.get(), TargetTypeImage, TRUE);
284    GdkAtom atom = gtk_drag_dest_find_target(widget, context, list.get());
285    if (atom != GDK_NONE)
286        dropAtoms.append(atom);
287
288    return dropAtoms;
289}
290
291static DataObjectGtk* settingClipboardDataObject = 0;
292
293static void getClipboardContentsCallback(GtkClipboard* clipboard, GtkSelectionData *selectionData, guint info, gpointer)
294{
295    DataObjectGtk* dataObject = DataObjectGtk::forClipboard(clipboard);
296    ASSERT(dataObject);
297    PasteboardHelper::defaultPasteboardHelper()->fillSelectionData(selectionData, info, dataObject);
298}
299
300static void clearClipboardContentsCallback(GtkClipboard* clipboard, gpointer data)
301{
302    DataObjectGtk* dataObject = DataObjectGtk::forClipboard(clipboard);
303    ASSERT(dataObject);
304
305    // Only clear the DataObject for this clipboard if we are not currently setting it.
306    if (dataObject != settingClipboardDataObject)
307        dataObject->clearAll();
308
309    if (!data)
310        return;
311
312    GClosure* callback = static_cast<GClosure*>(data);
313    GValue firstArgument = {0, {{0}}};
314    g_value_init(&firstArgument, G_TYPE_POINTER);
315    g_value_set_pointer(&firstArgument, clipboard);
316    g_closure_invoke(callback, 0, 1, &firstArgument, 0);
317    g_closure_unref(callback);
318}
319
320void PasteboardHelper::writeClipboardContents(GtkClipboard* clipboard, SmartPasteInclusion includeSmartPaste, GClosure* callback)
321{
322    DataObjectGtk* dataObject = DataObjectGtk::forClipboard(clipboard);
323    GtkTargetList* list = targetListForDataObject(dataObject, includeSmartPaste);
324
325    int numberOfTargets;
326    GtkTargetEntry* table = gtk_target_table_new_from_list(list, &numberOfTargets);
327
328    if (numberOfTargets > 0 && table) {
329        settingClipboardDataObject = dataObject;
330
331        gtk_clipboard_set_with_data(clipboard, table, numberOfTargets,
332            getClipboardContentsCallback, clearClipboardContentsCallback, callback);
333        gtk_clipboard_set_can_store(clipboard, 0, 0);
334
335        settingClipboardDataObject = 0;
336
337    } else
338        gtk_clipboard_clear(clipboard);
339
340    if (table)
341        gtk_target_table_free(table, numberOfTargets);
342    gtk_target_list_unref(list);
343}
344
345bool PasteboardHelper::clipboardContentSupportsSmartReplace(GtkClipboard* clipboard)
346{
347    return gtk_clipboard_wait_is_target_available(clipboard, smartPasteAtom);
348}
349
350}
351
352