blob: 6d83ea9d5746cf954833ff41ef9ed99436f3cdbd [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/base/x/x11_clipboard_helper.h"
#include <string>
#include <vector>
#include "base/compiler_specific.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/singleton.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/time.h"
#include "ui/base/clipboard/clipboard_buffer.h"
#include "ui/base/clipboard/clipboard_constants.h"
#include "ui/base/ui_base_features.h"
#include "ui/base/x/selection_owner.h"
#include "ui/base/x/selection_requestor.h"
#include "ui/base/x/selection_utils.h"
#include "ui/base/x/x11_util.h"
#include "ui/gfx/x/atom_cache.h"
#include "ui/gfx/x/event.h"
#include "ui/gfx/x/window_event_manager.h"
#include "ui/gfx/x/xfixes.h"
#include "ui/gfx/x/xproto.h"
namespace ui {
namespace {
const char kClipboard[] = "CLIPBOARD";
const char kClipboardManager[] = "CLIPBOARD_MANAGER";
// Uses the XFixes API to notify about selection changes.
class SelectionChangeObserver : public x11::EventObserver {
public:
using SelectionChangeCallback = XClipboardHelper::SelectionChangeCallback;
SelectionChangeObserver(const SelectionChangeObserver&) = delete;
SelectionChangeObserver& operator=(const SelectionChangeObserver&) = delete;
static SelectionChangeObserver* Get();
void set_callback(SelectionChangeCallback callback) {
callback_ = std::move(callback);
}
private:
friend struct base::DefaultSingletonTraits<SelectionChangeObserver>;
SelectionChangeObserver();
~SelectionChangeObserver() override = default;
// x11::EventObserver:
void OnEvent(const x11::Event& xev) override;
const x11::Atom clipboard_atom_{};
SelectionChangeCallback callback_;
};
SelectionChangeObserver::SelectionChangeObserver()
: clipboard_atom_(x11::GetAtom(kClipboard)) {
auto* connection = x11::Connection::Get();
auto& xfixes = connection->xfixes();
if (!xfixes.present()) {
return;
}
auto mask = x11::XFixes::SelectionEventMask::SetSelectionOwner |
x11::XFixes::SelectionEventMask::SelectionWindowDestroy |
x11::XFixes::SelectionEventMask::SelectionClientClose;
xfixes.SelectSelectionInput({GetX11RootWindow(), clipboard_atom_, mask});
// This seems to be semi-optional. For some reason, registering for any
// selection notify events seems to subscribe us to events for both the
// primary and the clipboard buffers. Register anyway just to be safe.
xfixes.SelectSelectionInput({GetX11RootWindow(), x11::Atom::PRIMARY, mask});
connection->AddEventObserver(this);
}
SelectionChangeObserver* SelectionChangeObserver::Get() {
return base::Singleton<SelectionChangeObserver>::get();
}
void SelectionChangeObserver::OnEvent(const x11::Event& xev) {
if (auto* ev = xev.As<x11::XFixes::SelectionNotifyEvent>()) {
if ((ev->selection == x11::Atom::PRIMARY ||
ev->selection == clipboard_atom_) &&
callback_) {
callback_.Run(ev->selection == x11::Atom::PRIMARY
? ClipboardBuffer::kSelection
: ClipboardBuffer::kCopyPaste);
}
}
}
x11::Window GetSelectionOwner(x11::Atom selection) {
auto response = x11::Connection::Get()->GetSelectionOwner({selection}).Sync();
return response ? response->owner : x11::Window::None;
}
} // namespace
class XClipboardHelper::TargetList {
public:
explicit TargetList(const std::vector<x11::Atom>& target_list)
: target_list_(target_list) {}
TargetList(const TargetList&) = default;
TargetList& operator=(const TargetList&) = default;
~TargetList() = default;
const std::vector<x11::Atom>& target_list() const { return target_list_; }
bool ContainsText() const {
for (const auto& atom : GetTextAtomsFrom()) {
if (base::Contains(target_list_, atom)) {
return true;
}
}
return false;
}
bool ContainsFormat(const ClipboardFormatType& format_type) const {
x11::Atom atom = x11::GetAtom(format_type.GetName().c_str());
return base::Contains(target_list_, atom);
}
private:
std::vector<x11::Atom> target_list_;
};
XClipboardHelper::XClipboardHelper(
SelectionChangeCallback selection_change_callback)
: connection_(*x11::Connection::Get()),
x_root_window_(ui::GetX11RootWindow()),
x_window_(connection_->CreateDummyWindow("Chromium Clipboard Window")),
selection_requestor_(
std::make_unique<SelectionRequestor>(x_window_, this)),
clipboard_owner_(connection_.get(), x_window_, x11::GetAtom(kClipboard)),
primary_owner_(connection_.get(), x_window_, x11::Atom::PRIMARY) {
DCHECK(selection_requestor_);
connection_->SetStringProperty(x_window_, x11::Atom::WM_NAME,
x11::Atom::STRING, "Chromium clipboard");
x_window_events_ =
connection_->ScopedSelectEvent(x_window_, x11::EventMask::PropertyChange);
connection_->AddEventObserver(this);
SelectionChangeObserver::Get()->set_callback(
std::move(selection_change_callback));
}
XClipboardHelper::~XClipboardHelper() {
connection_->RemoveEventObserver(this);
connection_->DestroyWindow({x_window_});
SelectionChangeObserver::Get()->set_callback(SelectionChangeCallback());
}
void XClipboardHelper::CreateNewClipboardData() {
clipboard_data_ = SelectionFormatMap();
}
void XClipboardHelper::InsertMapping(
const std::string& key,
const scoped_refptr<base::RefCountedMemory>& memory) {
x11::Atom atom_key = x11::GetAtom(key.c_str());
clipboard_data_.Insert(atom_key, memory);
}
void XClipboardHelper::TakeOwnershipOfSelection(ClipboardBuffer buffer) {
if (buffer == ClipboardBuffer::kCopyPaste) {
return clipboard_owner_.TakeOwnershipOfSelection(clipboard_data_);
} else {
return primary_owner_.TakeOwnershipOfSelection(clipboard_data_);
}
}
SelectionData XClipboardHelper::Read(ClipboardBuffer buffer,
const std::vector<x11::Atom>& types) {
x11::Atom selection_name = LookupSelectionForClipboardBuffer(buffer);
if (GetSelectionOwner(selection_name) == x_window_) {
// We can local fastpath instead of playing the nested run loop game
// with the X server.
const SelectionFormatMap& format_map = LookupStorageForAtom(selection_name);
for (const auto& type : types) {
auto format_map_it = format_map.find(type);
if (format_map_it != format_map.end()) {
return SelectionData(format_map_it->first, format_map_it->second);
}
}
return SelectionData();
}
auto targets = GetTargetList(buffer);
std::vector<x11::Atom> intersection;
GetAtomIntersection(types, targets.target_list(), &intersection);
return selection_requestor_->RequestAndWaitForTypes(selection_name,
intersection);
}
std::vector<std::string> XClipboardHelper::GetAvailableTypes(
ClipboardBuffer buffer) {
std::vector<std::string> available_types;
auto target_list = GetTargetList(buffer);
if (target_list.ContainsText()) {
available_types.push_back(kMimeTypePlainText);
}
if (target_list.ContainsFormat(ClipboardFormatType::HtmlType())) {
available_types.push_back(kMimeTypeHtml);
}
if (target_list.ContainsFormat(ClipboardFormatType::SvgType())) {
available_types.push_back(kMimeTypeSvg);
}
if (target_list.ContainsFormat(ClipboardFormatType::RtfType())) {
available_types.push_back(kMimeTypeRtf);
}
if (target_list.ContainsFormat(ClipboardFormatType::PngType())) {
available_types.push_back(kMimeTypePng);
}
if (target_list.ContainsFormat(ClipboardFormatType::FilenamesType())) {
available_types.push_back(kMimeTypeUriList);
}
if (target_list.ContainsFormat(
ClipboardFormatType::DataTransferCustomType())) {
available_types.push_back(kMimeTypeDataTransferCustomData);
}
return available_types;
}
std::vector<std::string> XClipboardHelper::GetAvailableAtomNames(
ClipboardBuffer buffer) {
auto target_list = GetTargetList(buffer).target_list();
if (target_list.empty()) {
return {};
}
auto* connection = x11::Connection::Get();
std::vector<x11::Future<x11::GetAtomNameReply>> futures;
for (x11::Atom target : target_list) {
futures.push_back(connection->GetAtomName({target}));
}
std::vector<std::string> atom_names;
atom_names.reserve(target_list.size());
for (auto& future : futures) {
if (auto response = future.Sync()) {
atom_names.push_back(response->name);
} else {
atom_names.emplace_back();
}
}
return atom_names;
}
bool XClipboardHelper::IsFormatAvailable(ClipboardBuffer buffer,
const ClipboardFormatType& format) {
auto target_list = GetTargetList(buffer);
if (format == ClipboardFormatType::PlainTextType() ||
format == ClipboardFormatType::UrlType()) {
return target_list.ContainsText();
}
return target_list.ContainsFormat(format);
}
bool XClipboardHelper::IsSelectionOwner(ClipboardBuffer buffer) const {
x11::Atom selection = LookupSelectionForClipboardBuffer(buffer);
return GetSelectionOwner(selection) == x_window_;
}
std::vector<x11::Atom> XClipboardHelper::GetTextAtoms() const {
return GetTextAtomsFrom();
}
std::vector<x11::Atom> XClipboardHelper::GetAtomsForFormat(
const ClipboardFormatType& format) {
return {x11::GetAtom(format.GetName().c_str())};
}
void XClipboardHelper::Clear(ClipboardBuffer buffer) {
if (buffer == ClipboardBuffer::kCopyPaste) {
clipboard_owner_.ClearSelectionOwner();
} else {
primary_owner_.ClearSelectionOwner();
}
}
void XClipboardHelper::StoreCopyPasteDataAndWait() {
x11::Atom selection = GetCopyPasteSelection();
if (GetSelectionOwner(selection) != x_window_) {
return;
}
x11::Atom clipboard_manager_atom = x11::GetAtom(kClipboardManager);
if (GetSelectionOwner(clipboard_manager_atom) == x11::Window::None) {
return;
}
const SelectionFormatMap& format_map = LookupStorageForAtom(selection);
if (format_map.size() == 0) {
return;
}
std::vector<x11::Atom> targets = format_map.GetTypes();
selection_requestor_->PerformBlockingConvertSelectionWithParameter(
x11::GetAtom(kClipboardManager), x11::GetAtom(kSaveTargets), targets);
}
XClipboardHelper::TargetList XClipboardHelper::GetTargetList(
ClipboardBuffer buffer) {
x11::Atom selection_name = LookupSelectionForClipboardBuffer(buffer);
std::vector<x11::Atom> out;
if (GetSelectionOwner(selection_name) == x_window_) {
// We can local fastpath and return the list of local targets.
const SelectionFormatMap& format_map = LookupStorageForAtom(selection_name);
for (const auto& format : format_map) {
out.push_back(format.first);
}
} else {
std::vector<uint8_t> data;
x11::Atom out_type = x11::Atom::None;
if (selection_requestor_->PerformBlockingConvertSelection(
selection_name, x11::GetAtom(kTargets), &data, &out_type)) {
// Some apps return an |out_type| of "TARGETS". (crbug.com/377893)
if (out_type == x11::Atom::ATOM || out_type == x11::GetAtom(kTargets)) {
const x11::Atom* atom_array =
UNSAFE_TODO(reinterpret_cast<const x11::Atom*>(data.data()));
for (size_t i = 0; i < data.size() / sizeof(x11::Atom); ++i) {
out.push_back(UNSAFE_TODO(atom_array[i]));
}
}
} else {
// There was no target list. Most Java apps doesn't offer a TARGETS list,
// even though they AWT to. They will offer individual text types if you
// ask. If this is the case we attempt to make sense of the contents as
// text. This is pretty unfortunate since it means we have to actually
// copy the data to see if it is available, but at least this path
// shouldn't be hit for conforming programs.
std::vector<x11::Atom> types = GetTextAtoms();
for (const auto& text_atom : types) {
x11::Atom type = x11::Atom::None;
if (selection_requestor_->PerformBlockingConvertSelection(
selection_name, text_atom, nullptr, &type) &&
type == text_atom) {
out.push_back(text_atom);
}
}
}
}
return XClipboardHelper::TargetList(out);
}
bool XClipboardHelper::DispatchEvent(const x11::Event& xev) {
if (auto* request = xev.As<x11::SelectionRequestEvent>()) {
if (request->owner != x_window_) {
return false;
}
if (request->selection == x11::Atom::PRIMARY) {
primary_owner_.OnSelectionRequest(*request);
} else {
// We should not get requests for the CLIPBOARD_MANAGER selection
// because we never take ownership of it.
DCHECK_EQ(GetCopyPasteSelection(), request->selection);
clipboard_owner_.OnSelectionRequest(*request);
}
} else if (auto* notify = xev.As<x11::SelectionNotifyEvent>()) {
if (notify->requestor == x_window_) {
selection_requestor_->OnSelectionNotify(*notify);
} else {
return false;
}
} else if (auto* clear = xev.As<x11::SelectionClearEvent>()) {
if (clear->owner != x_window_) {
return false;
}
if (clear->selection == x11::Atom::PRIMARY) {
primary_owner_.OnSelectionClear(*clear);
} else {
// We should not get requests for the CLIPBOARD_MANAGER selection
// because we never take ownership of it.
DCHECK_EQ(GetCopyPasteSelection(), clear->selection);
clipboard_owner_.OnSelectionClear(*clear);
}
} else if (auto* prop = xev.As<x11::PropertyNotifyEvent>()) {
if (primary_owner_.CanDispatchPropertyEvent(*prop)) {
primary_owner_.OnPropertyEvent(*prop);
} else if (clipboard_owner_.CanDispatchPropertyEvent(*prop)) {
clipboard_owner_.OnPropertyEvent(*prop);
} else if (selection_requestor_->CanDispatchPropertyEvent(*prop)) {
selection_requestor_->OnPropertyEvent(*prop);
} else {
return false;
}
} else {
return false;
}
return true;
}
SelectionRequestor* XClipboardHelper::GetSelectionRequestorForTest() {
return selection_requestor_.get();
}
void XClipboardHelper::OnEvent(const x11::Event& xev) {
DispatchEvent(xev);
}
x11::Atom XClipboardHelper::LookupSelectionForClipboardBuffer(
ClipboardBuffer buffer) const {
if (buffer == ClipboardBuffer::kCopyPaste) {
return GetCopyPasteSelection();
}
return x11::Atom::PRIMARY;
}
x11::Atom XClipboardHelper::GetCopyPasteSelection() const {
return x11::GetAtom(kClipboard);
}
const SelectionFormatMap& XClipboardHelper::LookupStorageForAtom(
x11::Atom atom) {
if (atom == x11::Atom::PRIMARY) {
return primary_owner_.selection_format_map();
}
DCHECK_EQ(GetCopyPasteSelection(), atom);
return clipboard_owner_.selection_format_map();
}
} // namespace ui