910e62b5创建于 1月15日历史提交
// 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