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 "BitmapImage.h"
27#include "GtkVersioning.h"
28#include "SelectionData.h"
29#include <gtk/gtk.h>
30#include <wtf/SetForScope.h>
31#include <wtf/glib/GUniquePtr.h>
32#include <wtf/text/CString.h>
33
34namespace WebCore {
35
36static GdkAtom textPlainAtom;
37static GdkAtom markupAtom;
38static GdkAtom netscapeURLAtom;
39static GdkAtom uriListAtom;
40static GdkAtom smartPasteAtom;
41static GdkAtom unknownAtom;
42
43static const String& markupPrefix()
44{
45 static NeverDestroyed<const String> prefix(MAKE_STATIC_STRING_IMPL("<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">"));
46 return prefix.get();
47}
48
49static void removeMarkupPrefix(String& markup)
50{
51 // The markup prefix is not harmful, but we remove it from the string anyway, so that
52 // we can have consistent results with other ports during the layout tests.
53 if (markup.startsWith(markupPrefix()))
54 markup.remove(0, markupPrefix().length());
55}
56
57PasteboardHelper& PasteboardHelper::singleton()
58{
59 static PasteboardHelper helper;
60 return helper;
61}
62
63PasteboardHelper::PasteboardHelper()
64 : m_targetList(adoptGRef(gtk_target_list_new(nullptr, 0)))
65{
66 textPlainAtom = gdk_atom_intern_static_string("text/plain;charset=utf-8");
67 markupAtom = gdk_atom_intern_static_string("text/html");
68 netscapeURLAtom = gdk_atom_intern_static_string("_NETSCAPE_URL");
69 uriListAtom = gdk_atom_intern_static_string("text/uri-list");
70 smartPasteAtom = gdk_atom_intern_static_string("application/vnd.webkitgtk.smartpaste");
71 unknownAtom = gdk_atom_intern_static_string("application/vnd.webkitgtk.unknown");
72
73 gtk_target_list_add_text_targets(m_targetList.get(), PasteboardHelper::TargetTypeText);
74 gtk_target_list_add(m_targetList.get(), markupAtom, 0, PasteboardHelper::TargetTypeMarkup);
75 gtk_target_list_add_uri_targets(m_targetList.get(), PasteboardHelper::TargetTypeURIList);
76 gtk_target_list_add(m_targetList.get(), netscapeURLAtom, 0, PasteboardHelper::TargetTypeNetscapeURL);
77 gtk_target_list_add_image_targets(m_targetList.get(), PasteboardHelper::TargetTypeImage, TRUE);
78 gtk_target_list_add(m_targetList.get(), unknownAtom, 0, PasteboardHelper::TargetTypeUnknown);
79}
80
81PasteboardHelper::~PasteboardHelper() = default;
82
83GtkTargetList* PasteboardHelper::targetList() const
84{
85 return m_targetList.get();
86}
87
88static String selectionDataToUTF8String(GtkSelectionData* data)
89{
90 if (!gtk_selection_data_get_length(data))
91 return String();
92
93 // g_strndup guards against selection data that is not null-terminated.
94 GUniquePtr<gchar> markupString(g_strndup(reinterpret_cast<const char*>(gtk_selection_data_get_data(data)), gtk_selection_data_get_length(data)));
95 return String::fromUTF8(markupString.get());
96}
97
98void PasteboardHelper::getClipboardContents(GtkClipboard* clipboard, SelectionData& selection)
99{
100 if (gtk_clipboard_wait_is_text_available(clipboard)) {
101 GUniquePtr<gchar> textData(gtk_clipboard_wait_for_text(clipboard));
102 if (textData)
103 selection.setText(String::fromUTF8(textData.get()));
104 }
105
106 if (gtk_clipboard_wait_is_target_available(clipboard, markupAtom)) {
107 if (GtkSelectionData* data = gtk_clipboard_wait_for_contents(clipboard, markupAtom)) {
108 String markup(selectionDataToUTF8String(data));
109 removeMarkupPrefix(markup);
110 selection.setMarkup(markup);
111 gtk_selection_data_free(data);
112 }
113 }
114
115 if (gtk_clipboard_wait_is_target_available(clipboard, uriListAtom)) {
116 if (GtkSelectionData* data = gtk_clipboard_wait_for_contents(clipboard, uriListAtom)) {
117 selection.setURIList(selectionDataToUTF8String(data));
118 gtk_selection_data_free(data);
119 }
120 }
121
122#ifndef GTK_API_VERSION_2
123 if (gtk_clipboard_wait_is_image_available(clipboard)) {
124 if (GRefPtr<GdkPixbuf> pixbuf = adoptGRef(gtk_clipboard_wait_for_image(clipboard))) {
125 RefPtr<cairo_surface_t> surface = adoptRef(gdk_cairo_surface_create_from_pixbuf(pixbuf.get(), 1, nullptr));
126 Ref<Image> image = BitmapImage::create(WTFMove(surface));
127 selection.setImage(image.ptr());
128 }
129 }
130#endif
131
132 selection.setCanSmartReplace(gtk_clipboard_wait_is_target_available(clipboard, smartPasteAtom));
133}
134
135GRefPtr<GtkTargetList> PasteboardHelper::targetListForSelectionData(const SelectionData& selection)
136{
137 GRefPtr<GtkTargetList> list = adoptGRef(gtk_target_list_new(nullptr, 0));
138
139 if (selection.hasText())
140 gtk_target_list_add_text_targets(list.get(), TargetTypeText);
141
142 if (selection.hasMarkup())
143 gtk_target_list_add(list.get(), markupAtom, 0, TargetTypeMarkup);
144
145 if (selection.hasURIList()) {
146 gtk_target_list_add_uri_targets(list.get(), TargetTypeURIList);
147 gtk_target_list_add(list.get(), netscapeURLAtom, 0, TargetTypeNetscapeURL);
148 }
149
150 if (selection.hasImage())
151 gtk_target_list_add_image_targets(list.get(), TargetTypeImage, TRUE);
152
153 if (selection.hasUnknownTypeData())
154 gtk_target_list_add(list.get(), unknownAtom, 0, TargetTypeUnknown);
155
156 if (selection.canSmartReplace())
157 gtk_target_list_add(list.get(), smartPasteAtom, 0, TargetTypeSmartPaste);
158
159 return list;
160}
161
162void PasteboardHelper::fillSelectionData(const SelectionData& selection, unsigned info, GtkSelectionData* selectionData)
163{
164 if (info == TargetTypeText)
165 gtk_selection_data_set_text(selectionData, selection.text().utf8().data(), -1);
166
167 else if (info == TargetTypeMarkup) {
168 // Some Linux applications refuse to accept pasted markup unless it is
169 // prefixed by a content-type meta tag.
170 CString markup = makeString(markupPrefix(), selection.markup()).utf8();
171 gtk_selection_data_set(selectionData, markupAtom, 8, reinterpret_cast<const guchar*>(markup.data()), markup.length());
172
173 } else if (info == TargetTypeURIList) {
174 CString uriList = selection.uriList().utf8();
175 gtk_selection_data_set(selectionData, uriListAtom, 8, reinterpret_cast<const guchar*>(uriList.data()), uriList.length());
176
177 } else if (info == TargetTypeNetscapeURL && selection.hasURL()) {
178 String url(selection.url());
179 String result(url);
180 result.append("\n");
181
182 if (selection.hasText())
183 result.append(selection.text());
184 else
185 result.append(url);
186
187 GUniquePtr<gchar> resultData(g_strdup(result.utf8().data()));
188 gtk_selection_data_set(selectionData, netscapeURLAtom, 8, reinterpret_cast<const guchar*>(resultData.get()), strlen(resultData.get()));
189
190 } else if (info == TargetTypeImage && selection.hasImage()) {
191 GRefPtr<GdkPixbuf> pixbuf = adoptGRef(selection.image()->getGdkPixbuf());
192 gtk_selection_data_set_pixbuf(selectionData, pixbuf.get());
193
194 } else if (info == TargetTypeSmartPaste)
195 gtk_selection_data_set_text(selectionData, "", -1);
196
197 else if (info == TargetTypeUnknown) {
198 GVariantBuilder builder;
199 g_variant_builder_init(&builder, G_VARIANT_TYPE_ARRAY);
200
201 for (auto& it : selection.unknownTypes()) {
202 GUniquePtr<gchar> dictItem(g_strdup_printf("{'%s', '%s'}", it.key.utf8().data(), it.value.utf8().data()));
203 g_variant_builder_add_parsed(&builder, dictItem.get());
204 }
205
206 GRefPtr<GVariant> variant = g_variant_builder_end(&builder);
207 GUniquePtr<gchar> serializedVariant(g_variant_print(variant.get(), TRUE));
208 gtk_selection_data_set(selectionData, unknownAtom, 1, reinterpret_cast<const guchar*>(serializedVariant.get()), strlen(serializedVariant.get()));
209 }
210}
211
212void PasteboardHelper::fillSelectionData(GtkSelectionData* data, unsigned /* info */, SelectionData& selection)
213{
214 if (gtk_selection_data_get_length(data) < 0)
215 return;
216
217 GdkAtom target = gtk_selection_data_get_target(data);
218 if (target == textPlainAtom)
219 selection.setText(selectionDataToUTF8String(data));
220 else if (target == markupAtom) {
221 String markup(selectionDataToUTF8String(data));
222 removeMarkupPrefix(markup);
223 selection.setMarkup(markup);
224 } else if (target == uriListAtom) {
225 selection.setURIList(selectionDataToUTF8String(data));
226 } else if (target == netscapeURLAtom) {
227 String urlWithLabel(selectionDataToUTF8String(data));
228 Vector<String> pieces = urlWithLabel.split('\n');
229
230 // Give preference to text/uri-list here, as it can hold more
231 // than one URI but still take the label if there is one.
232 if (!selection.hasURIList() && !pieces.isEmpty())
233 selection.setURIList(pieces[0]);
234 if (pieces.size() > 1)
235 selection.setText(pieces[1]);
236 } else if (target == unknownAtom && gtk_selection_data_get_length(data)) {
237 GRefPtr<GVariant> variant = g_variant_new_parsed(reinterpret_cast<const char*>(gtk_selection_data_get_data(data)));
238
239 GUniqueOutPtr<gchar> key;
240 GUniqueOutPtr<gchar> value;
241 GVariantIter iter;
242
243 g_variant_iter_init(&iter, variant.get());
244 while (g_variant_iter_next(&iter, "{ss}", &key.outPtr(), &value.outPtr()))
245 selection.setUnknownTypeData(key.get(), value.get());
246 }
247}
248
249Vector<GdkAtom> PasteboardHelper::dropAtomsForContext(GtkWidget* widget, GdkDragContext* context)
250{
251 // Always search for these common atoms.
252 Vector<GdkAtom> dropAtoms;
253 dropAtoms.append(textPlainAtom);
254 dropAtoms.append(markupAtom);
255 dropAtoms.append(uriListAtom);
256 dropAtoms.append(netscapeURLAtom);
257 dropAtoms.append(unknownAtom);
258
259 // For images, try to find the most applicable image type.
260 GRefPtr<GtkTargetList> list = adoptGRef(gtk_target_list_new(0, 0));
261 gtk_target_list_add_image_targets(list.get(), TargetTypeImage, TRUE);
262 GdkAtom atom = gtk_drag_dest_find_target(widget, context, list.get());
263 if (atom != GDK_NONE)
264 dropAtoms.append(atom);
265
266 return dropAtoms;
267}
268
269static SelectionData* settingClipboardSelection;
270
271struct ClipboardSetData {
272 ClipboardSetData(SelectionData& selection, WTF::Function<void()>&& selectionClearedCallback)
273 : selectionData(selection)
274 , selectionClearedCallback(WTFMove(selectionClearedCallback))
275 {
276 }
277
278 ~ClipboardSetData() = default;
279
280 Ref<SelectionData> selectionData;
281 WTF::Function<void()> selectionClearedCallback;
282};
283
284static void getClipboardContentsCallback(GtkClipboard*, GtkSelectionData *selectionData, guint info, gpointer userData)
285{
286 auto* data = static_cast<ClipboardSetData*>(userData);
287 PasteboardHelper::singleton().fillSelectionData(data->selectionData, info, selectionData);
288}
289
290static void clearClipboardContentsCallback(GtkClipboard*, gpointer userData)
291{
292 std::unique_ptr<ClipboardSetData> data(static_cast<ClipboardSetData*>(userData));
293 if (data->selectionClearedCallback)
294 data->selectionClearedCallback();
295}
296
297void PasteboardHelper::writeClipboardContents(GtkClipboard* clipboard, const SelectionData& selection, WTF::Function<void()>&& primarySelectionCleared)
298{
299 GRefPtr<GtkTargetList> list = targetListForSelectionData(selection);
300
301 int numberOfTargets;
302 GtkTargetEntry* table = gtk_target_table_new_from_list(list.get(), &numberOfTargets);
303
304 if (numberOfTargets > 0 && table) {
305 SetForScope<SelectionData*> change(settingClipboardSelection, const_cast<SelectionData*>(&selection));
306 auto data = std::make_unique<ClipboardSetData>(*settingClipboardSelection, WTFMove(primarySelectionCleared));
307 if (gtk_clipboard_set_with_data(clipboard, table, numberOfTargets, getClipboardContentsCallback, clearClipboardContentsCallback, data.get())) {
308 gtk_clipboard_set_can_store(clipboard, nullptr, 0);
309 // When gtk_clipboard_set_with_data() succeeds clearClipboardContentsCallback takes the ownership of data, so we leak it here.
310 data.release();
311 }
312 } else
313 gtk_clipboard_clear(clipboard);
314
315 if (table)
316 gtk_target_table_free(table, numberOfTargets);
317}
318
319}
320
321