blob: 7ed451db1a95409492f231d6cdde0da4fe2d6123 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Many of these functions are based on those found in
// webkit/port/platform/PasteboardWin.cpp
#include "ui/base/clipboard/clipboard_win.h"
#include <shellapi.h>
#include <shlobj.h>
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/message_loop/message_loop.h"
#include "base/numerics/safe_conversions.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_offset_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/message_window.h"
#include "base/win/scoped_gdi_object.h"
#include "base/win/scoped_hdc.h"
#include "skia/ext/skia_utils_base.h"
#include "skia/ext/skia_utils_win.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/clipboard/clipboard_constants.h"
#include "ui/base/clipboard/clipboard_util_win.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/size.h"
namespace ui {
namespace {
// A scoper to impersonate the anonymous token and revert when leaving scope
class AnonymousImpersonator {
public:
AnonymousImpersonator() {
must_revert_ = ::ImpersonateAnonymousToken(::GetCurrentThread());
}
~AnonymousImpersonator() {
if (must_revert_)
::RevertToSelf();
}
private:
BOOL must_revert_;
DISALLOW_COPY_AND_ASSIGN(AnonymousImpersonator);
};
// A scoper to manage acquiring and automatically releasing the clipboard.
class ScopedClipboard {
public:
ScopedClipboard() : opened_(false) { }
~ScopedClipboard() {
if (opened_)
Release();
}
bool Acquire(HWND owner) {
const int kMaxAttemptsToOpenClipboard = 5;
if (opened_) {
NOTREACHED();
return false;
}
// Attempt to open the clipboard, which will acquire the Windows clipboard
// lock. This may fail if another process currently holds this lock.
// We're willing to try a few times in the hopes of acquiring it.
//
// This turns out to be an issue when using remote desktop because the
// rdpclip.exe process likes to read what we've written to the clipboard and
// send it to the RDP client. If we open and close the clipboard in quick
// succession, we might be trying to open it while rdpclip.exe has it open,
// See Bug 815425.
//
// In fact, we believe we'll only spin this loop over remote desktop. In
// normal situations, the user is initiating clipboard operations and there
// shouldn't be contention.
for (int attempts = 0; attempts < kMaxAttemptsToOpenClipboard; ++attempts) {
// If we didn't manage to open the clipboard, sleep a bit and be hopeful.
if (attempts != 0)
::Sleep(5);
if (::OpenClipboard(owner)) {
opened_ = true;
return true;
}
}
// We failed to acquire the clipboard.
return false;
}
void Release() {
if (opened_) {
// Impersonate the anonymous token during the call to CloseClipboard
// This prevents Windows 8+ capturing the broker's access token which
// could be accessed by lower-privileges chrome processes leading to
// a risk of EoP
AnonymousImpersonator impersonator;
::CloseClipboard();
opened_ = false;
} else {
NOTREACHED();
}
}
private:
bool opened_;
};
bool ClipboardOwnerWndProc(UINT message,
WPARAM wparam,
LPARAM lparam,
LRESULT* result) {
switch (message) {
case WM_RENDERFORMAT:
// This message comes when SetClipboardData was sent a null data handle
// and now it's come time to put the data on the clipboard.
// We always set data, so there isn't a need to actually do anything here.
break;
case WM_RENDERALLFORMATS:
// This message comes when SetClipboardData was sent a null data handle
// and now this application is about to quit, so it must put data on
// the clipboard before it exits.
// We always set data, so there isn't a need to actually do anything here.
break;
case WM_DRAWCLIPBOARD:
break;
case WM_DESTROY:
break;
case WM_CHANGECBCHAIN:
break;
default:
return false;
}
*result = 0;
return true;
}
template <typename charT>
HGLOBAL CreateGlobalData(const std::basic_string<charT>& str) {
HGLOBAL data =
::GlobalAlloc(GMEM_MOVEABLE, ((str.size() + 1) * sizeof(charT)));
if (data) {
charT* raw_data = static_cast<charT*>(::GlobalLock(data));
memcpy(raw_data, str.data(), str.size() * sizeof(charT));
raw_data[str.size()] = '\0';
::GlobalUnlock(data);
}
return data;
}
bool BitmapHasInvalidPremultipliedColors(const SkPixmap& pixmap) {
for (int x = 0; x < pixmap.width(); ++x) {
for (int y = 0; y < pixmap.height(); ++y) {
uint32_t pixel = *pixmap.addr32(x, y);
if (SkColorGetR(pixel) > SkColorGetA(pixel) ||
SkColorGetG(pixel) > SkColorGetA(pixel) ||
SkColorGetB(pixel) > SkColorGetA(pixel))
return true;
}
}
return false;
}
void MakeBitmapOpaque(SkPixmap* pixmap) {
for (int x = 0; x < pixmap->width(); ++x) {
for (int y = 0; y < pixmap->height(); ++y) {
*pixmap->writable_addr32(x, y) = SkColorSetA(*pixmap->addr32(x, y), 0xFF);
}
}
}
void ParseBookmarkClipboardFormat(const base::string16& bookmark,
base::string16* title,
std::string* url) {
const base::string16 kDelim = base::ASCIIToUTF16("\r\n");
const size_t title_end = bookmark.find_first_of(kDelim);
if (title)
title->assign(bookmark.substr(0, title_end));
if (url) {
const size_t url_start = bookmark.find_first_not_of(kDelim, title_end);
if (url_start != base::string16::npos) {
*url =
base::UTF16ToUTF8(bookmark.substr(url_start, base::string16::npos));
}
}
}
void FreeData(unsigned int format, HANDLE data) {
if (format == CF_BITMAP)
::DeleteObject(static_cast<HBITMAP>(data));
else
::GlobalFree(data);
}
template <typename StringType>
void TrimAfterNull(StringType* result) {
// Text copied to the clipboard may explicitly contain null characters that
// should be ignored, depending on the application that does the copying.
constexpr typename StringType::value_type kNull = 0;
size_t pos = result->find_first_of(kNull);
if (pos != StringType::npos)
result->resize(pos);
}
} // namespace
// Clipboard factory method.
// static
Clipboard* Clipboard::Create() {
return new ClipboardWin;
}
// ClipboardWin implementation.
ClipboardWin::ClipboardWin() {
if (base::MessageLoopCurrentForUI::IsSet())
clipboard_owner_.reset(new base::win::MessageWindow());
}
ClipboardWin::~ClipboardWin() {
}
void ClipboardWin::OnPreShutdown() {}
uint64_t ClipboardWin::GetSequenceNumber(ClipboardType type) const {
DCHECK_EQ(type, ClipboardType::kCopyPaste);
return ::GetClipboardSequenceNumber();
}
bool ClipboardWin::IsFormatAvailable(const ClipboardFormatType& format,
ClipboardType type) const {
DCHECK_EQ(type, ClipboardType::kCopyPaste);
return ::IsClipboardFormatAvailable(format.ToFormatEtc().cfFormat) != FALSE;
}
void ClipboardWin::Clear(ClipboardType type) {
DCHECK_EQ(type, ClipboardType::kCopyPaste);
ScopedClipboard clipboard;
if (!clipboard.Acquire(GetClipboardWindow()))
return;
::EmptyClipboard();
}
void ClipboardWin::ReadAvailableTypes(ClipboardType type,
std::vector<base::string16>* types,
bool* contains_filenames) const {
if (!types || !contains_filenames) {
NOTREACHED();
return;
}
types->clear();
if (::IsClipboardFormatAvailable(
ClipboardFormatType::GetPlainTextType().ToFormatEtc().cfFormat))
types->push_back(base::UTF8ToUTF16(kMimeTypeText));
if (::IsClipboardFormatAvailable(
ClipboardFormatType::GetHtmlType().ToFormatEtc().cfFormat))
types->push_back(base::UTF8ToUTF16(kMimeTypeHTML));
if (::IsClipboardFormatAvailable(
ClipboardFormatType::GetRtfType().ToFormatEtc().cfFormat))
types->push_back(base::UTF8ToUTF16(kMimeTypeRTF));
if (::IsClipboardFormatAvailable(CF_DIB))
types->push_back(base::UTF8ToUTF16(kMimeTypePNG));
*contains_filenames = false;
// Acquire the clipboard.
ScopedClipboard clipboard;
if (!clipboard.Acquire(GetClipboardWindow()))
return;
HANDLE hdata = ::GetClipboardData(
ClipboardFormatType::GetWebCustomDataType().ToFormatEtc().cfFormat);
if (!hdata)
return;
ReadCustomDataTypes(::GlobalLock(hdata), ::GlobalSize(hdata), types);
::GlobalUnlock(hdata);
}
void ClipboardWin::ReadText(ClipboardType type, base::string16* result) const {
DCHECK_EQ(type, ClipboardType::kCopyPaste);
if (!result) {
NOTREACHED();
return;
}
result->clear();
// Acquire the clipboard.
ScopedClipboard clipboard;
if (!clipboard.Acquire(GetClipboardWindow()))
return;
HANDLE data = ::GetClipboardData(CF_UNICODETEXT);
if (!data)
return;
result->assign(static_cast<const base::char16*>(::GlobalLock(data)),
::GlobalSize(data) / sizeof(base::char16));
::GlobalUnlock(data);
TrimAfterNull(result);
}
void ClipboardWin::ReadAsciiText(ClipboardType type,
std::string* result) const {
DCHECK_EQ(type, ClipboardType::kCopyPaste);
if (!result) {
NOTREACHED();
return;
}
result->clear();
// Acquire the clipboard.
ScopedClipboard clipboard;
if (!clipboard.Acquire(GetClipboardWindow()))
return;
HANDLE data = ::GetClipboardData(CF_TEXT);
if (!data)
return;
result->assign(static_cast<const char*>(::GlobalLock(data)),
::GlobalSize(data));
::GlobalUnlock(data);
TrimAfterNull(result);
}
void ClipboardWin::ReadHTML(ClipboardType type,
base::string16* markup,
std::string* src_url,
uint32_t* fragment_start,
uint32_t* fragment_end) const {
DCHECK_EQ(type, ClipboardType::kCopyPaste);
markup->clear();
// TODO(dcheng): Remove these checks, I don't think they should be optional.
DCHECK(src_url);
if (src_url)
src_url->clear();
*fragment_start = 0;
*fragment_end = 0;
// Acquire the clipboard.
ScopedClipboard clipboard;
if (!clipboard.Acquire(GetClipboardWindow()))
return;
HANDLE data = ::GetClipboardData(
ClipboardFormatType::GetHtmlType().ToFormatEtc().cfFormat);
if (!data)
return;
std::string cf_html(static_cast<const char*>(::GlobalLock(data)),
::GlobalSize(data));
::GlobalUnlock(data);
TrimAfterNull(&cf_html);
size_t html_start = std::string::npos;
size_t start_index = std::string::npos;
size_t end_index = std::string::npos;
ClipboardUtil::CFHtmlExtractMetadata(cf_html, src_url, &html_start,
&start_index, &end_index);
// This might happen if the contents of the clipboard changed and CF_HTML is
// no longer available.
if (start_index == std::string::npos ||
end_index == std::string::npos ||
html_start == std::string::npos)
return;
if (start_index < html_start || end_index < start_index)
return;
std::vector<size_t> offsets;
offsets.push_back(start_index - html_start);
offsets.push_back(end_index - html_start);
markup->assign(base::UTF8ToUTF16AndAdjustOffsets(cf_html.data() + html_start,
&offsets));
// Ensure the Fragment points within the string; see https://crbug.com/607181.
size_t end = std::min(offsets[1], markup->length());
*fragment_start = base::checked_cast<uint32_t>(std::min(offsets[0], end));
*fragment_end = base::checked_cast<uint32_t>(end);
}
void ClipboardWin::ReadRTF(ClipboardType type, std::string* result) const {
DCHECK_EQ(type, ClipboardType::kCopyPaste);
ReadData(ClipboardFormatType::GetRtfType(), result);
TrimAfterNull(result);
}
SkBitmap ClipboardWin::ReadImage(ClipboardType type) const {
DCHECK_EQ(type, ClipboardType::kCopyPaste);
// Acquire the clipboard.
ScopedClipboard clipboard;
if (!clipboard.Acquire(GetClipboardWindow()))
return SkBitmap();
// We use a DIB rather than a DDB here since ::GetObject() with the
// HBITMAP returned from ::GetClipboardData(CF_BITMAP) always reports a color
// depth of 32bpp.
BITMAPINFO* bitmap = static_cast<BITMAPINFO*>(::GetClipboardData(CF_DIB));
if (!bitmap)
return SkBitmap();
int color_table_length = 0;
// For more information on BITMAPINFOHEADER and biBitCount definition,
// see https://docs.microsoft.com/en-us/previous-versions//dd183376(v=vs.85)
switch (bitmap->bmiHeader.biBitCount) {
case 1:
case 4:
case 8:
color_table_length = bitmap->bmiHeader.biClrUsed
? bitmap->bmiHeader.biClrUsed
: 1 << bitmap->bmiHeader.biBitCount;
break;
case 16:
case 32:
if (bitmap->bmiHeader.biCompression == BI_BITFIELDS)
color_table_length = 3;
break;
case 24:
break;
default:
NOTREACHED();
}
const void* bitmap_bits = reinterpret_cast<const char*>(bitmap)
+ bitmap->bmiHeader.biSize + color_table_length * sizeof(RGBQUAD);
void* dst_bits;
// dst_hbitmap is freed by the release_proc in skia_bitmap (below)
HBITMAP dst_hbitmap =
skia::CreateHBitmap(bitmap->bmiHeader.biWidth, bitmap->bmiHeader.biHeight,
false, 0, &dst_bits);
{
base::win::ScopedCreateDC hdc(CreateCompatibleDC(NULL));
HBITMAP old_hbitmap =
static_cast<HBITMAP>(SelectObject(hdc.Get(), dst_hbitmap));
::SetDIBitsToDevice(hdc.Get(), 0, 0, bitmap->bmiHeader.biWidth,
bitmap->bmiHeader.biHeight, 0, 0, 0,
bitmap->bmiHeader.biHeight, bitmap_bits, bitmap,
DIB_RGB_COLORS);
SelectObject(hdc.Get(), old_hbitmap);
}
// Windows doesn't really handle alpha channels well in many situations. When
// the source image is < 32 bpp, we force the bitmap to be opaque. When the
// source image is 32 bpp, the alpha channel might still contain garbage data.
// Since Windows uses premultiplied alpha, we scan for instances where
// (R, G, B) > A. If there are any invalid premultiplied colors in the image,
// we assume the alpha channel contains garbage and force the bitmap to be
// opaque as well. Note that this heuristic will fail on a transparent bitmap
// containing only black pixels...
SkPixmap device_pixels(SkImageInfo::MakeN32Premul(bitmap->bmiHeader.biWidth,
bitmap->bmiHeader.biHeight),
dst_bits, bitmap->bmiHeader.biWidth * 4);
{
bool has_invalid_alpha_channel = bitmap->bmiHeader.biBitCount < 32 ||
BitmapHasInvalidPremultipliedColors(device_pixels);
if (has_invalid_alpha_channel) {
MakeBitmapOpaque(&device_pixels);
}
}
SkBitmap skia_bitmap;
skia_bitmap.installPixels(device_pixels.info(), device_pixels.writable_addr(),
device_pixels.rowBytes(),
[](void* pixels, void* hbitmap) {
DeleteObject(static_cast<HBITMAP>(hbitmap));
},
dst_hbitmap);
return skia_bitmap;
}
void ClipboardWin::ReadCustomData(ClipboardType clipboard_type,
const base::string16& type,
base::string16* result) const {
DCHECK_EQ(clipboard_type, ClipboardType::kCopyPaste);
// Acquire the clipboard.
ScopedClipboard clipboard;
if (!clipboard.Acquire(GetClipboardWindow()))
return;
HANDLE hdata = ::GetClipboardData(
ClipboardFormatType::GetWebCustomDataType().ToFormatEtc().cfFormat);
if (!hdata)
return;
ReadCustomDataForType(::GlobalLock(hdata), ::GlobalSize(hdata), type, result);
::GlobalUnlock(hdata);
}
void ClipboardWin::ReadBookmark(base::string16* title, std::string* url) const {
if (title)
title->clear();
if (url)
url->clear();
// Acquire the clipboard.
ScopedClipboard clipboard;
if (!clipboard.Acquire(GetClipboardWindow()))
return;
HANDLE data = ::GetClipboardData(
ClipboardFormatType::GetUrlWType().ToFormatEtc().cfFormat);
if (!data)
return;
base::string16 bookmark(static_cast<const base::char16*>(::GlobalLock(data)),
::GlobalSize(data) / sizeof(base::char16));
::GlobalUnlock(data);
TrimAfterNull(&bookmark);
ParseBookmarkClipboardFormat(bookmark, title, url);
}
void ClipboardWin::ReadData(const ClipboardFormatType& format,
std::string* result) const {
if (!result) {
NOTREACHED();
return;
}
ScopedClipboard clipboard;
if (!clipboard.Acquire(GetClipboardWindow()))
return;
HANDLE data = ::GetClipboardData(format.ToFormatEtc().cfFormat);
if (!data)
return;
result->assign(static_cast<const char*>(::GlobalLock(data)),
::GlobalSize(data));
::GlobalUnlock(data);
}
void ClipboardWin::WriteObjects(ClipboardType type, const ObjectMap& objects) {
DCHECK_EQ(type, ClipboardType::kCopyPaste);
ScopedClipboard clipboard;
if (!clipboard.Acquire(GetClipboardWindow()))
return;
::EmptyClipboard();
for (ObjectMap::const_iterator iter = objects.begin(); iter != objects.end();
++iter) {
DispatchObject(static_cast<ObjectType>(iter->first), iter->second);
}
}
void ClipboardWin::WriteText(const char* text_data, size_t text_len) {
base::string16 text;
base::UTF8ToUTF16(text_data, text_len, &text);
HGLOBAL glob = CreateGlobalData(text);
WriteToClipboard(CF_UNICODETEXT, glob);
}
void ClipboardWin::WriteHTML(const char* markup_data,
size_t markup_len,
const char* url_data,
size_t url_len) {
std::string markup(markup_data, markup_len);
std::string url;
if (url_len > 0)
url.assign(url_data, url_len);
std::string html_fragment = ClipboardUtil::HtmlToCFHtml(markup, url);
HGLOBAL glob = CreateGlobalData(html_fragment);
WriteToClipboard(ClipboardFormatType::GetHtmlType().ToFormatEtc().cfFormat,
glob);
}
void ClipboardWin::WriteRTF(const char* rtf_data, size_t data_len) {
WriteData(ClipboardFormatType::GetRtfType(), rtf_data, data_len);
}
void ClipboardWin::WriteBookmark(const char* title_data,
size_t title_len,
const char* url_data,
size_t url_len) {
std::string bookmark(title_data, title_len);
bookmark.append(1, L'\n');
bookmark.append(url_data, url_len);
base::string16 wide_bookmark = base::UTF8ToUTF16(bookmark);
HGLOBAL glob = CreateGlobalData(wide_bookmark);
WriteToClipboard(ClipboardFormatType::GetUrlWType().ToFormatEtc().cfFormat,
glob);
}
void ClipboardWin::WriteWebSmartPaste() {
DCHECK(clipboard_owner_->hwnd() != NULL);
::SetClipboardData(
ClipboardFormatType::GetWebKitSmartPasteType().ToFormatEtc().cfFormat,
NULL);
}
void ClipboardWin::WriteBitmap(const SkBitmap& in_bitmap) {
HDC dc = ::GetDC(NULL);
SkBitmap bitmap;
// Either points bitmap at in_bitmap, or allocates and converts pixels.
if (!skia::SkBitmapToN32OpaqueOrPremul(in_bitmap, &bitmap)) {
NOTREACHED() << "Unable to convert bitmap for clipboard";
return;
}
// This doesn't actually cost us a memcpy when the bitmap comes from the
// renderer as we load it into the bitmap using setPixels which just sets a
// pointer. Someone has to memcpy it into GDI, it might as well be us here.
// TODO(darin): share data in gfx/bitmap_header.cc somehow
BITMAPINFO bm_info = {};
bm_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bm_info.bmiHeader.biWidth = bitmap.width();
bm_info.bmiHeader.biHeight = -bitmap.height(); // sets vertical orientation
bm_info.bmiHeader.biPlanes = 1;
bm_info.bmiHeader.biBitCount = 32;
bm_info.bmiHeader.biCompression = BI_RGB;
// ::CreateDIBSection allocates memory for us to copy our bitmap into.
// Unfortunately, we can't write the created bitmap to the clipboard,
// (see http://msdn2.microsoft.com/en-us/library/ms532292.aspx)
void* bits;
HBITMAP source_hbitmap =
::CreateDIBSection(dc, &bm_info, DIB_RGB_COLORS, &bits, NULL, 0);
if (bits && source_hbitmap) {
// Copy the bitmap out of shared memory and into GDI
memcpy(bits, bitmap.getPixels(), bitmap.computeByteSize());
// Now we have an HBITMAP, we can write it to the clipboard
WriteBitmapFromHandle(source_hbitmap,
gfx::Size(bitmap.width(), bitmap.height()));
}
::DeleteObject(source_hbitmap);
::ReleaseDC(NULL, dc);
}
void ClipboardWin::WriteData(const ClipboardFormatType& format,
const char* data_data,
size_t data_len) {
HGLOBAL hdata = ::GlobalAlloc(GMEM_MOVEABLE, data_len);
if (!hdata)
return;
char* data = static_cast<char*>(::GlobalLock(hdata));
memcpy(data, data_data, data_len);
::GlobalUnlock(data);
WriteToClipboard(format.ToFormatEtc().cfFormat, hdata);
}
void ClipboardWin::WriteBitmapFromHandle(HBITMAP source_hbitmap,
const gfx::Size& size) {
// We would like to just call ::SetClipboardData on the source_hbitmap,
// but that bitmap might not be of a sort we can write to the clipboard.
// For this reason, we create a new bitmap, copy the bits over, and then
// write that to the clipboard.
HDC dc = ::GetDC(NULL);
HDC compatible_dc = ::CreateCompatibleDC(NULL);
HDC source_dc = ::CreateCompatibleDC(NULL);
// This is the HBITMAP we will eventually write to the clipboard
HBITMAP hbitmap = ::CreateCompatibleBitmap(dc, size.width(), size.height());
if (!hbitmap) {
// Failed to create the bitmap
::DeleteDC(compatible_dc);
::DeleteDC(source_dc);
::ReleaseDC(NULL, dc);
return;
}
HBITMAP old_hbitmap = (HBITMAP)SelectObject(compatible_dc, hbitmap);
HBITMAP old_source = (HBITMAP)SelectObject(source_dc, source_hbitmap);
// Now we need to blend it into an HBITMAP we can place on the clipboard
BLENDFUNCTION bf = {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA};
::GdiAlphaBlend(compatible_dc,
0,
0,
size.width(),
size.height(),
source_dc,
0,
0,
size.width(),
size.height(),
bf);
// Clean up all the handles we just opened
::SelectObject(compatible_dc, old_hbitmap);
::SelectObject(source_dc, old_source);
::DeleteObject(old_hbitmap);
::DeleteObject(old_source);
::DeleteDC(compatible_dc);
::DeleteDC(source_dc);
::ReleaseDC(NULL, dc);
WriteToClipboard(CF_BITMAP, hbitmap);
}
void ClipboardWin::WriteToClipboard(unsigned int format, HANDLE handle) {
DCHECK(clipboard_owner_->hwnd() != NULL);
if (handle && !::SetClipboardData(format, handle)) {
DCHECK(ERROR_CLIPBOARD_NOT_OPEN != GetLastError());
FreeData(format, handle);
}
}
HWND ClipboardWin::GetClipboardWindow() const {
if (!clipboard_owner_)
return NULL;
if (clipboard_owner_->hwnd() == NULL)
clipboard_owner_->Create(base::Bind(&ClipboardOwnerWndProc));
return clipboard_owner_->hwnd();
}
} // namespace ui