/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* vim:expandtab:shiftwidth=4:tabstop=4: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "mozilla/ArrayUtils.h" #include "nsArrayUtils.h" #include "nsClipboard.h" #include "nsClipboardX11.h" #if defined(MOZ_WAYLAND) #include "nsClipboardWayland.h" #endif #include "HeadlessClipboard.h" #include "nsSupportsPrimitives.h" #include "nsString.h" #include "nsReadableUtils.h" #include "nsPrimitiveHelpers.h" #include "nsIServiceManager.h" #include "nsImageToPixbuf.h" #include "nsStringStream.h" #include "nsIObserverService.h" #include "mozilla/Services.h" #include "mozilla/RefPtr.h" #include "mozilla/TimeStamp.h" #include "imgIContainer.h" #include #include #include "mozilla/Encoding.h" using namespace mozilla; // Callback when someone asks us for the data void clipboard_get_cb(GtkClipboard *aGtkClipboard, GtkSelectionData *aSelectionData, guint info, gpointer user_data); // Callback when someone asks us to clear a clipboard void clipboard_clear_cb(GtkClipboard *aGtkClipboard, gpointer user_data); static void ConvertHTMLtoUCS2 (const char* data, int32_t dataLength, char16_t **unicodeData, int32_t &outUnicodeLen); static void GetHTMLCharset (const char* data, int32_t dataLength, nsCString& str); GdkAtom GetSelectionAtom(int32_t aWhichClipboard) { if (aWhichClipboard == nsIClipboard::kGlobalClipboard) return GDK_SELECTION_CLIPBOARD; return GDK_SELECTION_PRIMARY; } // Idle timeout for receiving selection and property notify events (microsec) const int nsRetrievalContext::kClipboardTimeout = 500000; nsClipboard::nsClipboard() { nsCOMPtr os = mozilla::services::GetObserverService(); if (os) { os->AddObserver(this, "quit-application", false); os->AddObserver(this, "xpcom-shutdown", false); } } nsClipboard::~nsClipboard() { // We have to clear clipboard before gdk_display_close() call. // See bug 531580 for details. if (mGlobalTransferable) { gtk_clipboard_clear(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD)); } if (mSelectionTransferable) { gtk_clipboard_clear(gtk_clipboard_get(GDK_SELECTION_PRIMARY)); } } NS_IMPL_ISUPPORTS(nsClipboard, nsIClipboard) nsresult nsClipboard::Init(void) { // create nsRetrievalContext if (GDK_IS_X11_DISPLAY(gdk_display_get_default())) { mContext = new nsRetrievalContextX11(); #if defined(MOZ_WAYLAND) } else { mContext = new nsRetrievalContextWayland(); #endif } return NS_OK; } nsresult nsClipboard::Store(void) { if (mGlobalTransferable) { GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); gtk_clipboard_store(clipboard); } return NS_OK; } NS_IMETHODIMP nsClipboard::Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) { Store(); return NS_OK; } NS_IMETHODIMP nsClipboard::SetData(nsITransferable *aTransferable, nsIClipboardOwner *aOwner, int32_t aWhichClipboard) { // See if we can short cut if ((aWhichClipboard == kGlobalClipboard && aTransferable == mGlobalTransferable.get() && aOwner == mGlobalOwner.get()) || (aWhichClipboard == kSelectionClipboard && aTransferable == mSelectionTransferable.get() && aOwner == mSelectionOwner.get())) { return NS_OK; } // Clear out the clipboard in order to set the new data EmptyClipboard(aWhichClipboard); // List of suported targets GtkTargetList *list = gtk_target_list_new(nullptr, 0); // Get the types of supported flavors nsCOMPtr flavors; nsresult rv = aTransferable->FlavorsTransferableCanExport(getter_AddRefs(flavors)); if (!flavors || NS_FAILED(rv)) return NS_ERROR_FAILURE; // Add all the flavors to this widget's supported type. bool imagesAdded = false; uint32_t count; flavors->GetLength(&count); for (uint32_t i=0; i < count; i++) { nsCOMPtr flavor = do_QueryElementAt(flavors, i); if (flavor) { nsCString flavorStr; flavor->ToString(getter_Copies(flavorStr)); // special case text/unicode since we can handle all of // the string types if (flavorStr.EqualsLiteral(kUnicodeMime)) { gtk_target_list_add(list, gdk_atom_intern("UTF8_STRING", FALSE), 0, 0); gtk_target_list_add(list, gdk_atom_intern("COMPOUND_TEXT", FALSE), 0, 0); gtk_target_list_add(list, gdk_atom_intern("TEXT", FALSE), 0, 0); gtk_target_list_add(list, GDK_SELECTION_TYPE_STRING, 0, 0); continue; } if (flavorStr.EqualsLiteral(kNativeImageMime) || flavorStr.EqualsLiteral(kPNGImageMime) || flavorStr.EqualsLiteral(kJPEGImageMime) || flavorStr.EqualsLiteral(kJPGImageMime) || flavorStr.EqualsLiteral(kGIFImageMime)) { // don't bother adding image targets twice if (!imagesAdded) { // accept any writable image type gtk_target_list_add_image_targets(list, 0, TRUE); imagesAdded = true; } continue; } // Add this to our list of valid targets GdkAtom atom = gdk_atom_intern(flavorStr.get(), FALSE); gtk_target_list_add(list, atom, 0, 0); } } // Get GTK clipboard (CLIPBOARD or PRIMARY) GtkClipboard *gtkClipboard = gtk_clipboard_get(GetSelectionAtom(aWhichClipboard)); gint numTargets; GtkTargetEntry *gtkTargets = gtk_target_table_new_from_list(list, &numTargets); // Set getcallback and request to store data after an application exit if (gtkTargets && gtk_clipboard_set_with_data(gtkClipboard, gtkTargets, numTargets, clipboard_get_cb, clipboard_clear_cb, this)) { // We managed to set-up the clipboard so update internal state // We have to set it now because gtk_clipboard_set_with_data() calls clipboard_clear_cb() // which reset our internal state if (aWhichClipboard == kSelectionClipboard) { mSelectionOwner = aOwner; mSelectionTransferable = aTransferable; } else { mGlobalOwner = aOwner; mGlobalTransferable = aTransferable; gtk_clipboard_set_can_store(gtkClipboard, gtkTargets, numTargets); } rv = NS_OK; } else { rv = NS_ERROR_FAILURE; } gtk_target_table_free(gtkTargets, numTargets); gtk_target_list_unref(list); return rv; } void nsClipboard::SetTransferableData(nsITransferable* aTransferable, nsCString& aFlavor, const char* aClipboardData, uint32_t aClipboardDataLength) { nsCOMPtr wrapper; nsPrimitiveHelpers::CreatePrimitiveForData(aFlavor, aClipboardData, aClipboardDataLength, getter_AddRefs(wrapper)); aTransferable->SetTransferData(aFlavor.get(), wrapper, aClipboardDataLength); } NS_IMETHODIMP nsClipboard::GetData(nsITransferable *aTransferable, int32_t aWhichClipboard) { if (!aTransferable) return NS_ERROR_FAILURE; // Get a list of flavors this transferable can import nsCOMPtr flavors; nsresult rv; rv = aTransferable->FlavorsTransferableCanImport(getter_AddRefs(flavors)); if (!flavors || NS_FAILED(rv)) return NS_ERROR_FAILURE; uint32_t count; flavors->GetLength(&count); for (uint32_t i=0; i < count; i++) { nsCOMPtr currentFlavor; currentFlavor = do_QueryElementAt(flavors, i); if (!currentFlavor) continue; nsCString flavorStr; currentFlavor->ToString(getter_Copies(flavorStr)); if (flavorStr.EqualsLiteral(kJPEGImageMime) || flavorStr.EqualsLiteral(kJPGImageMime) || flavorStr.EqualsLiteral(kPNGImageMime) || flavorStr.EqualsLiteral(kGIFImageMime)) { // Emulate support for image/jpg if (flavorStr.EqualsLiteral(kJPGImageMime)) { flavorStr.Assign(kJPEGImageMime); } uint32_t clipboardDataLength; const char* clipboardData = mContext->GetClipboardData(flavorStr.get(), aWhichClipboard, &clipboardDataLength); if (!clipboardData) continue; nsCOMPtr byteStream; NS_NewByteInputStream(getter_AddRefs(byteStream), clipboardData, clipboardDataLength, NS_ASSIGNMENT_COPY); aTransferable->SetTransferData(flavorStr.get(), byteStream, sizeof(nsIInputStream*)); mContext->ReleaseClipboardData(clipboardData); return NS_OK; } // Special case text/unicode since we can convert any // string into text/unicode if (flavorStr.EqualsLiteral(kUnicodeMime)) { uint32_t clipboardDataLength; const char* clipboardData = mContext->GetClipboardData(GTK_DEFAULT_MIME_TEXT, aWhichClipboard, &clipboardDataLength); if (!clipboardData) { // If the type was text/unicode and we couldn't get // text off the clipboard, run the next loop // iteration. continue; } // Convert utf-8 into our unicode format. NS_ConvertUTF8toUTF16 ucs2string(clipboardData, clipboardDataLength); const char* unicodeData = (const char *)ToNewUnicode(ucs2string); uint32_t unicodeDataLength = ucs2string.Length() * 2; SetTransferableData(aTransferable, flavorStr, unicodeData, unicodeDataLength); free((void *)unicodeData); mContext->ReleaseClipboardData(clipboardData); return NS_OK; } uint32_t clipboardDataLength; const char* clipboardData = mContext->GetClipboardData( flavorStr.get(), aWhichClipboard, &clipboardDataLength); if (clipboardData) { // Special case text/html since we can convert into UCS2 if (flavorStr.EqualsLiteral(kHTMLMime)) { char16_t* htmlBody = nullptr; int32_t htmlBodyLen = 0; // Convert text/html into our unicode format ConvertHTMLtoUCS2(clipboardData, clipboardDataLength, &htmlBody, htmlBodyLen); // Try next data format? if (!htmlBodyLen) { mContext->ReleaseClipboardData(clipboardData); continue; } SetTransferableData(aTransferable, flavorStr, (const char*)htmlBody, htmlBodyLen * 2); free(htmlBody); } else { SetTransferableData(aTransferable, flavorStr, clipboardData, clipboardDataLength); } mContext->ReleaseClipboardData(clipboardData); return NS_OK; } } return NS_OK; } NS_IMETHODIMP nsClipboard::EmptyClipboard(int32_t aWhichClipboard) { if (aWhichClipboard == kSelectionClipboard) { if (mSelectionOwner) { mSelectionOwner->LosingOwnership(mSelectionTransferable); mSelectionOwner = nullptr; } mSelectionTransferable = nullptr; } else { if (mGlobalOwner) { mGlobalOwner->LosingOwnership(mGlobalTransferable); mGlobalOwner = nullptr; } mGlobalTransferable = nullptr; } return NS_OK; } NS_IMETHODIMP nsClipboard::HasDataMatchingFlavors(const char** aFlavorList, uint32_t aLength, int32_t aWhichClipboard, bool *_retval) { if (!aFlavorList || !_retval) return NS_ERROR_NULL_POINTER; *_retval = false; int targetNums; GdkAtom* targets = mContext->GetTargets(aWhichClipboard, &targetNums); // Walk through the provided types and try to match it to a // provided type. for (uint32_t i = 0; i < aLength && !*_retval; i++) { // We special case text/unicode here. if (!strcmp(aFlavorList[i], kUnicodeMime) && gtk_targets_include_text(targets, targetNums)) { *_retval = true; break; } for (int32_t j = 0; j < targetNums; j++) { gchar *atom_name = gdk_atom_name(targets[j]); if (!atom_name) continue; if (!strcmp(atom_name, aFlavorList[i])) *_retval = true; // X clipboard supports image/jpeg, but we want to emulate support // for image/jpg as well if (!strcmp(aFlavorList[i], kJPGImageMime) && !strcmp(atom_name, kJPEGImageMime)) { *_retval = true; } g_free(atom_name); if (*_retval) break; } } g_free(targets); return NS_OK; } NS_IMETHODIMP nsClipboard::SupportsSelectionClipboard(bool *_retval) { // yeah, unix supports the selection clipboard on X11 // TODO Wayland *_retval = GDK_IS_X11_DISPLAY(gdk_display_get_default()); return NS_OK; } NS_IMETHODIMP nsClipboard::SupportsFindClipboard(bool* _retval) { *_retval = false; return NS_OK; } nsITransferable * nsClipboard::GetTransferable(int32_t aWhichClipboard) { nsITransferable *retval; if (aWhichClipboard == kSelectionClipboard) retval = mSelectionTransferable.get(); else retval = mGlobalTransferable.get(); return retval; } void nsClipboard::SelectionGetEvent(GtkClipboard *aClipboard, GtkSelectionData *aSelectionData) { // Someone has asked us to hand them something. The first thing // that we want to do is see if that something includes text. If // it does, try to give it text/unicode after converting it to // utf-8. int32_t whichClipboard; // which clipboard? GdkAtom selection = gtk_selection_data_get_selection(aSelectionData); if (selection == GDK_SELECTION_PRIMARY) whichClipboard = kSelectionClipboard; else if (selection == GDK_SELECTION_CLIPBOARD) whichClipboard = kGlobalClipboard; else return; // THAT AIN'T NO CLIPBOARD I EVER HEARD OF nsCOMPtr trans = GetTransferable(whichClipboard); if (!trans) { // We have nothing to serve #ifdef DEBUG_CLIPBOARD printf("nsClipboard::SelectionGetEvent() - %s clipboard is empty!\n", whichClipboard == kSelectionClipboard ? "Selection" : "Global"); #endif return; } nsresult rv; nsCOMPtr item; uint32_t len; GdkAtom selectionTarget = gtk_selection_data_get_target(aSelectionData); // Check to see if the selection data includes any of the string // types that we support. if (selectionTarget == gdk_atom_intern ("STRING", FALSE) || selectionTarget == gdk_atom_intern ("TEXT", FALSE) || selectionTarget == gdk_atom_intern ("COMPOUND_TEXT", FALSE) || selectionTarget == gdk_atom_intern ("UTF8_STRING", FALSE)) { // Try to convert our internal type into a text string. Get // the transferable for this clipboard and try to get the // text/unicode type for it. rv = trans->GetTransferData("text/unicode", getter_AddRefs(item), &len); if (!item || NS_FAILED(rv)) return; nsCOMPtr wideString; wideString = do_QueryInterface(item); if (!wideString) return; nsAutoString ucs2string; wideString->GetData(ucs2string); char *utf8string = ToNewUTF8String(ucs2string); if (!utf8string) return; gtk_selection_data_set_text (aSelectionData, utf8string, strlen(utf8string)); free(utf8string); return; } // Check to see if the selection data is an image type if (gtk_targets_include_image(&selectionTarget, 1, TRUE)) { // Look through our transfer data for the image static const char* const imageMimeTypes[] = { kNativeImageMime, kPNGImageMime, kJPEGImageMime, kJPGImageMime, kGIFImageMime }; nsCOMPtr imageItem; nsCOMPtr ptrPrimitive; for (uint32_t i = 0; !ptrPrimitive && i < ArrayLength(imageMimeTypes); i++) { rv = trans->GetTransferData(imageMimeTypes[i], getter_AddRefs(imageItem), &len); ptrPrimitive = do_QueryInterface(imageItem); } if (!ptrPrimitive) return; nsCOMPtr primitiveData; ptrPrimitive->GetData(getter_AddRefs(primitiveData)); nsCOMPtr image(do_QueryInterface(primitiveData)); if (!image) // Not getting an image for an image mime type!? return; GdkPixbuf* pixbuf = nsImageToPixbuf::ImageToPixbuf(image); if (!pixbuf) return; gtk_selection_data_set_pixbuf(aSelectionData, pixbuf); g_object_unref(pixbuf); return; } // Try to match up the selection data target to something our // transferable provides. gchar *target_name = gdk_atom_name(selectionTarget); if (!target_name) return; rv = trans->GetTransferData(target_name, getter_AddRefs(item), &len); // nothing found? if (!item || NS_FAILED(rv)) { g_free(target_name); return; } void *primitive_data = nullptr; nsPrimitiveHelpers::CreateDataFromPrimitive(nsDependentCString(target_name), item, &primitive_data, len); if (primitive_data) { // Check to see if the selection data is text/html if (selectionTarget == gdk_atom_intern (kHTMLMime, FALSE)) { /* * "text/html" can be encoded UCS2. It is recommended that * documents transmitted as UCS2 always begin with a ZERO-WIDTH * NON-BREAKING SPACE character (hexadecimal FEFF, also called * Byte Order Mark (BOM)). Adding BOM can help other app to * detect mozilla use UCS2 encoding when copy-paste. */ guchar *buffer = (guchar *) g_malloc((len * sizeof(guchar)) + sizeof(char16_t)); if (!buffer) return; char16_t prefix = 0xFEFF; memcpy(buffer, &prefix, sizeof(prefix)); memcpy(buffer + sizeof(prefix), primitive_data, len); g_free((guchar *)primitive_data); primitive_data = (guchar *)buffer; len += sizeof(prefix); } gtk_selection_data_set(aSelectionData, selectionTarget, 8, /* 8 bits in a unit */ (const guchar *)primitive_data, len); g_free(primitive_data); } g_free(target_name); } void nsClipboard::SelectionClearEvent(GtkClipboard *aGtkClipboard) { int32_t whichClipboard; // which clipboard? if (aGtkClipboard == gtk_clipboard_get(GDK_SELECTION_PRIMARY)) whichClipboard = kSelectionClipboard; else if (aGtkClipboard == gtk_clipboard_get(GDK_SELECTION_CLIPBOARD)) whichClipboard = kGlobalClipboard; else return; // THAT AIN'T NO CLIPBOARD I EVER HEARD OF EmptyClipboard(whichClipboard); } void clipboard_get_cb(GtkClipboard *aGtkClipboard, GtkSelectionData *aSelectionData, guint info, gpointer user_data) { nsClipboard *aClipboard = static_cast(user_data); aClipboard->SelectionGetEvent(aGtkClipboard, aSelectionData); } void clipboard_clear_cb(GtkClipboard *aGtkClipboard, gpointer user_data) { nsClipboard *aClipboard = static_cast(user_data); aClipboard->SelectionClearEvent(aGtkClipboard); } /* * when copy-paste, mozilla wants data encoded using UCS2, * other app such as StarOffice use "text/html"(RFC2854). * This function convert data(got from GTK clipboard) * to data mozilla wanted. * * data from GTK clipboard can be 3 forms: * 1. From current mozilla * "text/html", charset = utf-16 * 2. From old version mozilla or mozilla-based app * content("body" only), charset = utf-16 * 3. From other app who use "text/html" when copy-paste * "text/html", has "charset" info * * data : got from GTK clipboard * dataLength: got from GTK clipboard * body : pass to Mozilla * bodyLength: pass to Mozilla */ void ConvertHTMLtoUCS2(const char* data, int32_t dataLength, char16_t** unicodeData, int32_t& outUnicodeLen) { nsAutoCString charset; GetHTMLCharset(data, dataLength, charset);// get charset of HTML if (charset.EqualsLiteral("UTF-16")) {//current mozilla outUnicodeLen = (dataLength / 2) - 1; *unicodeData = reinterpret_cast (moz_xmalloc((outUnicodeLen + sizeof('\0')) * sizeof(char16_t))); if (*unicodeData) { memcpy(*unicodeData, data + sizeof(char16_t), outUnicodeLen * sizeof(char16_t)); (*unicodeData)[outUnicodeLen] = '\0'; } } else if (charset.EqualsLiteral("UNKNOWN")) { outUnicodeLen = 0; return; } else { // app which use "text/html" to copy&paste // get the decoder auto encoding = Encoding::ForLabelNoReplacement(charset); if (!encoding) { #ifdef DEBUG_CLIPBOARD g_print(" get unicode decoder error\n"); #endif outUnicodeLen = 0; return; } auto decoder = encoding->NewDecoder(); CheckedInt needed = decoder->MaxUTF16BufferLength(dataLength); if (!needed.isValid() || needed.value() > INT32_MAX) { outUnicodeLen = 0; return; } outUnicodeLen = 0; if (needed.value()) { *unicodeData = reinterpret_cast( moz_xmalloc((needed.value() + 1) * sizeof(char16_t))); if (*unicodeData) { uint32_t result; size_t read; size_t written; bool hadErrors; Tie(result, read, written, hadErrors) = decoder->DecodeToUTF16(AsBytes(MakeSpan(data, dataLength)), MakeSpan(*unicodeData, needed.value()), true); MOZ_ASSERT(result == kInputEmpty); MOZ_ASSERT(read == size_t(dataLength)); MOZ_ASSERT(written <= needed.value()); Unused << hadErrors; #ifdef DEBUG_CLIPBOARD if (read != dataLength) printf("didn't consume all the bytes\n"); #endif outUnicodeLen = written; // null terminate. (*unicodeData)[outUnicodeLen] = '\0'; } } // if valid length } } /* * get "charset" information from clipboard data * return value can be: * 1. "UTF-16": mozilla or "text/html" with "charset=utf-16" * 2. "UNKNOWN": mozilla can't detect what encode it use * 3. other: "text/html" with other charset than utf-16 */ void GetHTMLCharset(const char* data, int32_t dataLength, nsCString& str) { // if detect "FFFE" or "FEFF", assume UTF-16 char16_t* beginChar = (char16_t*)data; if ((beginChar[0] == 0xFFFE) || (beginChar[0] == 0xFEFF)) { str.AssignLiteral("UTF-16"); return; } // no "FFFE" and "FEFF", assume ASCII first to find "charset" info const nsDependentCString htmlStr(data, dataLength); nsACString::const_iterator start, end; htmlStr.BeginReading(start); htmlStr.EndReading(end); nsACString::const_iterator valueStart(start), valueEnd(start); if (CaseInsensitiveFindInReadable( NS_LITERAL_CSTRING("CONTENT=\"text/html;"), start, end)) { start = end; htmlStr.EndReading(end); if (CaseInsensitiveFindInReadable( NS_LITERAL_CSTRING("charset="), start, end)) { valueStart = end; start = end; htmlStr.EndReading(end); if (FindCharInReadable('"', start, end)) valueEnd = start; } } // find "charset" in HTML if (valueStart != valueEnd) { str = Substring(valueStart, valueEnd); ToUpperCase(str); #ifdef DEBUG_CLIPBOARD printf("Charset of HTML = %s\n", charsetUpperStr.get()); #endif return; } str.AssignLiteral("UNKNOWN"); }