blob: d0f764e1167cfff33e4f5f0e0bc5299947486a25 [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/clipboard/clipboard_item.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/clipboard/clipboard.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/script_function.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/core/clipboard/system_clipboard.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/modules/clipboard/clipboard.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
#include "ui/base/clipboard/clipboard_constants.h"
namespace blink {
// The time threshold to consider an operation as "delayed" for UseCounter
// purposes.
constexpr base::TimeDelta kClipboardOperation5SecDelay = base::Seconds(5);
constexpr base::TimeDelta kClipboardOperation1MinDelay = base::Minutes(1);
constexpr base::TimeDelta kClipboardOperation10MinDelay = base::Minutes(10);
class UnionToBlobResolverFunction final
: public ThenCallable<V8UnionBlobOrString,
UnionToBlobResolverFunction,
Blob> {
public:
explicit UnionToBlobResolverFunction(const String& mime_type)
: mime_type_(mime_type) {}
Blob* React(ScriptState* script_state, V8UnionBlobOrString* union_value) {
if (union_value->IsBlob()) {
return union_value->GetAsBlob();
} else if (union_value->IsString()) {
// ClipboardItem::getType() returns a Blob, so we need to convert the
// string to a Blob here.
return Blob::Create(union_value->GetAsString().Span8(), mime_type_);
}
return nullptr;
}
private:
String mime_type_;
};
// static
ClipboardItem* ClipboardItem::Create(
const HeapVector<
std::pair<String, MemberScriptPromise<V8UnionBlobOrString>>>&
representations,
ExceptionState& exception_state) {
// Check that incoming dictionary isn't empty. If it is, it's possible that
// Javascript bindings implicitly converted an Object (like a
// ScriptPromise<V8UnionBlobOrString>) into {}, an empty dictionary.
if (!representations.size()) {
exception_state.ThrowTypeError("Empty dictionary argument");
return nullptr;
}
return MakeGarbageCollected<ClipboardItem>(representations);
}
ClipboardItem::ClipboardItem(
const HeapVector<
std::pair<String, MemberScriptPromise<V8UnionBlobOrString>>>&
representations,
absl::uint128 sequence_number)
: sequence_number_(sequence_number),
creation_time_(base::TimeTicks::Now()) {
for (const auto& representation : representations) {
String web_custom_format =
Clipboard::ParseWebCustomFormat(representation.first);
if (web_custom_format.empty()) {
// Any arbitrary type can be added to ClipboardItem, but there may not be
// any read/write support for that type.
// TODO(caseq,japhet): we can't pass typed promises from bindings yet, but
// when we can, the type cast below should go away.
representations_.emplace_back(representation.first,
representation.second);
} else {
// Types with "web " prefix are special, so we do some level of MIME type
// parsing here to get a valid web custom format type.
// We want to ensure that the string after removing the "web " prefix is
// a valid MIME type.
// e.g. "web text/html" is a web custom MIME type & "text/html" is a
// well-known MIME type. Removing the "web " prefix makes it hard to
// differentiate between the two.
// TODO(caseq,japhet): we can't pass typed promises from bindings yet, but
// when we can, the type cast below should go away.
String web_custom_format_string =
String::Format("%s%s", ui::kWebClipboardFormatPrefix,
web_custom_format.Utf8().c_str());
representations_.emplace_back(web_custom_format_string,
representation.second);
custom_format_types_.push_back(web_custom_format_string);
}
}
}
Vector<String> ClipboardItem::types() const {
Vector<String> types;
types.ReserveInitialCapacity(representations_.size());
for (const auto& item : representations_) {
types.push_back(item.first);
}
return types;
}
ScriptPromise<Blob> ClipboardItem::getType(ScriptState* script_state,
const String& type,
ExceptionState& exception_state) {
for (const auto& item : representations_) {
if (type == item.first) {
if (RuntimeEnabledFeatures::ClipboardItemGetTypeCounterEnabled()) {
CaptureTelemetry(ExecutionContext::From(script_state), type);
}
return item.second.Unwrap().Then(
script_state,
MakeGarbageCollected<UnionToBlobResolverFunction>(type));
}
}
exception_state.ThrowDOMException(DOMExceptionCode::kNotFoundError,
"The type was not found");
return ScriptPromise<Blob>();
}
// static
bool ClipboardItem::supports(const String& type) {
if (type.length() >= mojom::blink::ClipboardHost::kMaxFormatSize) {
return false;
}
if (!Clipboard::ParseWebCustomFormat(type).empty()) {
return true;
}
// TODO(https://crbug.com/1029857): Add support for other types.
return type == ui::kMimeTypePng || type == ui::kMimeTypePlainText ||
type == ui::kMimeTypeHtml || type == ui::kMimeTypeSvg;
}
void ClipboardItem::Trace(Visitor* visitor) const {
visitor->Trace(representations_);
ScriptWrappable::Trace(visitor);
}
void ClipboardItem::CaptureTelemetry(ExecutionContext* context,
const String& type) {
if (!context) {
return;
}
LocalDOMWindow& window = *To<LocalDOMWindow>(context);
SystemClipboard* system_clipboard =
window.GetFrame() ? window.GetFrame()->GetSystemClipboard() : nullptr;
if (system_clipboard) {
absl::uint128 seqno = system_clipboard->SequenceNumber();
if (seqno != sequence_number_) {
// Case 1: Clipboard changed between read() and getType()
UseCounter::Count(context,
WebFeature::kClipboardChangedBetweenReadAndGetType);
// Case 2: Clipboard changed between two getType() calls
if (!last_get_type_calls_.empty()) {
UseCounter::Count(context,
WebFeature::kClipboardChangedBetweenGetTypes);
}
}
}
// Case 3: Time difference between read() and getType() calls is more
// than threshold
const base::TimeTicks current_time = base::TimeTicks::Now();
const base::TimeDelta time_diff = current_time - creation_time_;
if (time_diff >= kClipboardOperation5SecDelay &&
time_diff < kClipboardOperation1MinDelay) {
UseCounter::Count(
context,
WebFeature::kClipboardReadAndGetTypeTimeDiffIsBetween5SecAnd1Min);
} else if (time_diff >= kClipboardOperation1MinDelay &&
time_diff < kClipboardOperation10MinDelay) {
UseCounter::Count(
context,
WebFeature::kClipboardReadAndGetTypeTimeDiffIsBetween1MinAnd10Min);
} else if (time_diff > kClipboardOperation10MinDelay) {
UseCounter::Count(
context, WebFeature::kClipboardReadAndGetTypeTimeDiffIsMoreThan10Min);
}
// Case 4: Time difference between two getType() calls for the same
// types is more than threshold
auto it = last_get_type_calls_.find(type);
if (it != last_get_type_calls_.end()) {
const base::TimeDelta type_time_diff = current_time - it->value;
if (type_time_diff >= kClipboardOperation5SecDelay &&
type_time_diff < kClipboardOperation1MinDelay) {
UseCounter::Count(
context,
WebFeature::kClipboardGetTypeTimeDiffOfSameTypeIsBetween5SecAnd1Min);
} else if (type_time_diff >= kClipboardOperation1MinDelay &&
type_time_diff < kClipboardOperation10MinDelay) {
UseCounter::Count(
context,
WebFeature::kClipboardGetTypeTimeDiffOfSameTypeIsBetween1MinAnd10Min);
} else if (type_time_diff > kClipboardOperation10MinDelay) {
UseCounter::Count(
context,
WebFeature::kClipboardGetTypeTimeDiffOfSameTypeIsMoreThan10Min);
}
} else {
// Update the last call time for this type
last_get_type_calls_.Set(type, current_time);
}
if (!window.document()->hasFocus()) {
UseCounter::Count(context, WebFeature::kClipboardGetTypeWindowNotInFocus);
}
}
} // namespace blink