Commit eae7976f authored by Dylan Sleeper's avatar Dylan Sleeper Committed by Commit Bot

Clipboard API: Implement the HTML clipboard reader and writer.

This CL adds two subclasses: ClipboardHTMLReader and
ClipboardHTMLWriter. When HTML is read and written it is "sanitized"
which means that all script objects are removed from the html. They
are both entirely synchronous because it is impossible to use HTML
DOM nodes off of the main thread.

Design Doc: https://docs.google.com/document/d/1tzjfckWUKAAxEbKcUjg21uz7LFDMQ7ZDzfTTj3Lp93Y/edit

Bug: 931839
Change-Id: I29b26f1c721102005bcbb5911549a9546aed3006
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2243532
Commit-Queue: Dylan Sleeper <dsleeps@google.com>
Reviewed-by: default avatarJarryd Goodman <jarrydg@chromium.org>
Reviewed-by: default avatarDarwin Huang <huangdarwin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#787862}
parent 023614ad
...@@ -54,6 +54,8 @@ class ClipboardPromise final : public GarbageCollected<ClipboardPromise>, ...@@ -54,6 +54,8 @@ class ClipboardPromise final : public GarbageCollected<ClipboardPromise>,
// Adds the blob to the clipboard items. // Adds the blob to the clipboard items.
void OnRead(Blob* blob); void OnRead(Blob* blob);
LocalFrame* GetLocalFrame() const;
void Trace(Visitor*) const override; void Trace(Visitor*) const override;
private: private:
...@@ -84,7 +86,6 @@ class ClipboardPromise final : public GarbageCollected<ClipboardPromise>, ...@@ -84,7 +86,6 @@ class ClipboardPromise final : public GarbageCollected<ClipboardPromise>,
bool allow_without_sanitization, bool allow_without_sanitization,
base::OnceCallback<void(::blink::mojom::PermissionStatus)> callback); base::OnceCallback<void(::blink::mojom::PermissionStatus)> callback);
LocalFrame* GetLocalFrame() const;
scoped_refptr<base::SingleThreadTaskRunner> GetTaskRunner(); scoped_refptr<base::SingleThreadTaskRunner> GetTaskRunner();
Member<ScriptState> script_state_; Member<ScriptState> script_state_;
......
...@@ -6,7 +6,10 @@ ...@@ -6,7 +6,10 @@
#include "third_party/blink/public/mojom/clipboard/clipboard.mojom-blink.h" #include "third_party/blink/public/mojom/clipboard/clipboard.mojom-blink.h"
#include "third_party/blink/renderer/core/clipboard/clipboard_mime_types.h" #include "third_party/blink/renderer/core/clipboard/clipboard_mime_types.h"
#include "third_party/blink/renderer/core/clipboard/system_clipboard.h" #include "third_party/blink/renderer/core/clipboard/system_clipboard.h"
#include "third_party/blink/renderer/core/dom/document_fragment.h"
#include "third_party/blink/renderer/core/editing/serializers/serialization.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h" #include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h" #include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h"
#include "third_party/blink/renderer/modules/clipboard/clipboard_promise.h" #include "third_party/blink/renderer/modules/clipboard/clipboard_promise.h"
#include "third_party/blink/renderer/platform/image-encoders/image_encoder.h" #include "third_party/blink/renderer/platform/image-encoders/image_encoder.h"
...@@ -142,10 +145,77 @@ class ClipboardTextReader final : public ClipboardReader { ...@@ -142,10 +145,77 @@ class ClipboardTextReader final : public ClipboardReader {
} }
}; };
class ClipboardHtmlReader final : public ClipboardReader {
public:
explicit ClipboardHtmlReader(SystemClipboard* system_clipboard,
ClipboardPromise* promise)
: ClipboardReader(system_clipboard, promise) {}
~ClipboardHtmlReader() override = default;
// This must be called on the main thread because HTML DOM nodes can
// only be used on the main thread.
void Read() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
KURL url;
unsigned fragment_start = 0;
unsigned fragment_end = 0;
String html_string =
system_clipboard()->ReadHTML(url, fragment_start, fragment_end);
// Now sanitize the HTML string.
LocalFrame* frame = promise_->GetLocalFrame();
DocumentFragment* fragment = CreateSanitizedFragmentFromMarkupWithContext(
*frame->GetDocument(), html_string, fragment_start,
html_string.length(), url);
String sanitized_html =
CreateMarkup(fragment, kIncludeNode, kResolveAllURLs);
if (sanitized_html.IsEmpty()) {
NextRead(Vector<uint8_t>());
return;
}
worker_pool::PostTask(
FROM_HERE,
CrossThreadBindOnce(&ClipboardHtmlReader::EncodeHTMLOnBackgroundThread,
std::move(sanitized_html),
WrapCrossThreadPersistent(this),
std::move(clipboard_task_runner_)));
}
private:
static void EncodeHTMLOnBackgroundThread(
String plain_text,
ClipboardHtmlReader* reader,
scoped_refptr<base::SingleThreadTaskRunner> clipboard_task_runner) {
DCHECK(!IsMainThread());
// Encode WTF String to UTF-8, the standard text format for blobs.
StringUTF8Adaptor utf8_text(plain_text);
Vector<uint8_t> utf8_bytes;
utf8_bytes.ReserveInitialCapacity(utf8_text.size());
utf8_bytes.Append(utf8_text.data(), utf8_text.size());
PostCrossThreadTask(*clipboard_task_runner, FROM_HERE,
CrossThreadBindOnce(&ClipboardHtmlReader::NextRead,
WrapCrossThreadPersistent(reader),
std::move(utf8_bytes)));
}
void NextRead(Vector<uint8_t> utf8_bytes) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
Blob* blob = nullptr;
if (utf8_bytes.size()) {
blob =
Blob::Create(utf8_bytes.data(), utf8_bytes.size(), kMimeTypeTextHTML);
}
promise_->OnRead(blob);
}
};
} // anonymous namespace } // anonymous namespace
// ClipboardReader functions. // ClipboardReader functions.
// static
ClipboardReader* ClipboardReader::Create(SystemClipboard* system_clipboard, ClipboardReader* ClipboardReader::Create(SystemClipboard* system_clipboard,
const String& mime_type, const String& mime_type,
ClipboardPromise* promise) { ClipboardPromise* promise) {
...@@ -155,6 +225,8 @@ ClipboardReader* ClipboardReader::Create(SystemClipboard* system_clipboard, ...@@ -155,6 +225,8 @@ ClipboardReader* ClipboardReader::Create(SystemClipboard* system_clipboard,
if (mime_type == kMimeTypeTextPlain) if (mime_type == kMimeTypeTextPlain)
return MakeGarbageCollected<ClipboardTextReader>(system_clipboard, promise); return MakeGarbageCollected<ClipboardTextReader>(system_clipboard, promise);
if (mime_type == kMimeTypeTextHTML)
return MakeGarbageCollected<ClipboardHtmlReader>(system_clipboard, promise);
// The MIME type is not supported. // The MIME type is not supported.
return nullptr; return nullptr;
} }
......
...@@ -10,8 +10,11 @@ ...@@ -10,8 +10,11 @@
#include "third_party/blink/renderer/core/clipboard/clipboard_mime_types.h" #include "third_party/blink/renderer/core/clipboard/clipboard_mime_types.h"
#include "third_party/blink/renderer/core/clipboard/raw_system_clipboard.h" #include "third_party/blink/renderer/core/clipboard/raw_system_clipboard.h"
#include "third_party/blink/renderer/core/clipboard/system_clipboard.h" #include "third_party/blink/renderer/core/clipboard/system_clipboard.h"
#include "third_party/blink/renderer/core/dom/document_fragment.h"
#include "third_party/blink/renderer/core/editing/serializers/serialization.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h" #include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/fileapi/file_reader_loader.h" #include "third_party/blink/renderer/core/fileapi/file_reader_loader.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h" #include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h"
#include "third_party/blink/renderer/modules/clipboard/clipboard_promise.h" #include "third_party/blink/renderer/modules/clipboard/clipboard_promise.h"
#include "third_party/blink/renderer/platform/image-decoders/image_decoder.h" #include "third_party/blink/renderer/platform/image-decoders/image_decoder.h"
...@@ -33,6 +36,16 @@ class ClipboardImageWriter final : public ClipboardWriter { ...@@ -33,6 +36,16 @@ class ClipboardImageWriter final : public ClipboardWriter {
~ClipboardImageWriter() override = default; ~ClipboardImageWriter() override = default;
private: private:
void StartWrite(scoped_refptr<base::SingleThreadTaskRunner> task_runner,
DOMArrayBuffer* raw_data) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
worker_pool::PostTask(
FROM_HERE,
CrossThreadBindOnce(&ClipboardImageWriter::DecodeOnBackgroundThread,
WrapCrossThreadPersistent(this), task_runner,
WrapCrossThreadPersistent(raw_data)));
}
void DecodeOnBackgroundThread( void DecodeOnBackgroundThread(
scoped_refptr<base::SingleThreadTaskRunner> task_runner, scoped_refptr<base::SingleThreadTaskRunner> task_runner,
DOMArrayBuffer* png_data) override { DOMArrayBuffer* png_data) override {
...@@ -74,6 +87,16 @@ class ClipboardTextWriter final : public ClipboardWriter { ...@@ -74,6 +87,16 @@ class ClipboardTextWriter final : public ClipboardWriter {
~ClipboardTextWriter() override = default; ~ClipboardTextWriter() override = default;
private: private:
void StartWrite(scoped_refptr<base::SingleThreadTaskRunner> task_runner,
DOMArrayBuffer* raw_data) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
worker_pool::PostTask(
FROM_HERE,
CrossThreadBindOnce(&ClipboardTextWriter::DecodeOnBackgroundThread,
WrapCrossThreadPersistent(this), task_runner,
WrapCrossThreadPersistent(raw_data)));
}
void DecodeOnBackgroundThread( void DecodeOnBackgroundThread(
scoped_refptr<base::SingleThreadTaskRunner> task_runner, scoped_refptr<base::SingleThreadTaskRunner> task_runner,
DOMArrayBuffer* raw_data) override { DOMArrayBuffer* raw_data) override {
...@@ -96,6 +119,50 @@ class ClipboardTextWriter final : public ClipboardWriter { ...@@ -96,6 +119,50 @@ class ClipboardTextWriter final : public ClipboardWriter {
} }
}; };
class ClipboardHtmlWriter final : public ClipboardWriter {
public:
ClipboardHtmlWriter(SystemClipboard* system_clipboard,
ClipboardPromise* promise)
: ClipboardWriter(system_clipboard, promise) {}
~ClipboardHtmlWriter() override = default;
private:
void StartWrite(scoped_refptr<base::SingleThreadTaskRunner> task_runner,
DOMArrayBuffer* html_data) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
String html_string =
String::FromUTF8(reinterpret_cast<const LChar*>(html_data->Data()),
html_data->ByteLengthAsSizeT());
// Sanitizing on the main thread because HTML DOM nodes can only be used
// on the main thread.
KURL url;
unsigned fragment_start = 0;
unsigned fragment_end = html_string.length();
Document* document = promise_->GetLocalFrame()->GetDocument();
DocumentFragment* fragment = CreateSanitizedFragmentFromMarkupWithContext(
*document, html_string, fragment_start, fragment_end, url);
String sanitized_html =
CreateMarkup(fragment, kIncludeNode, kResolveAllURLs);
Write(sanitized_html, url);
}
void DecodeOnBackgroundThread(
scoped_refptr<base::SingleThreadTaskRunner> task_runner,
DOMArrayBuffer* html_data) override {
NOTREACHED() << "HTML's serializers cannot be used on background threads.";
}
void Write(const String& sanitized_html, KURL url) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
String plain_text = "";
system_clipboard()->WriteHTML(sanitized_html, url);
promise_->CompleteWriteRepresentation();
}
};
// Writes a blob with arbitrary, unsanitized content to the System Clipboard. // Writes a blob with arbitrary, unsanitized content to the System Clipboard.
class ClipboardRawDataWriter final : public ClipboardWriter { class ClipboardRawDataWriter final : public ClipboardWriter {
public: public:
...@@ -106,6 +173,17 @@ class ClipboardRawDataWriter final : public ClipboardWriter { ...@@ -106,6 +173,17 @@ class ClipboardRawDataWriter final : public ClipboardWriter {
~ClipboardRawDataWriter() override = default; ~ClipboardRawDataWriter() override = default;
private: private:
void StartWrite(scoped_refptr<base::SingleThreadTaskRunner> task_runner,
DOMArrayBuffer* raw_data) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
worker_pool::PostTask(
FROM_HERE,
CrossThreadBindOnce(&ClipboardRawDataWriter::DecodeOnBackgroundThread,
WrapCrossThreadPersistent(this), task_runner,
WrapCrossThreadPersistent(raw_data)));
}
// Unfortunately, in order to use the same ClipboardWriter base, // Unfortunately, in order to use the same ClipboardWriter base,
// ClipboardRawDataWriter does need to have these extra 2 thread hops. // ClipboardRawDataWriter does need to have these extra 2 thread hops.
void DecodeOnBackgroundThread( void DecodeOnBackgroundThread(
...@@ -156,6 +234,9 @@ ClipboardWriter* ClipboardWriter::Create(SystemClipboard* system_clipboard, ...@@ -156,6 +234,9 @@ ClipboardWriter* ClipboardWriter::Create(SystemClipboard* system_clipboard,
if (mime_type == kMimeTypeTextPlain) if (mime_type == kMimeTypeTextPlain)
return MakeGarbageCollected<ClipboardTextWriter>(system_clipboard, promise); return MakeGarbageCollected<ClipboardTextWriter>(system_clipboard, promise);
if (mime_type == kMimeTypeTextHTML)
return MakeGarbageCollected<ClipboardHtmlWriter>(system_clipboard, promise);
NOTREACHED() << "Type " << mime_type << " was not implemented"; NOTREACHED() << "Type " << mime_type << " was not implemented";
return nullptr; return nullptr;
} }
...@@ -197,9 +278,9 @@ bool ClipboardWriter::IsValidType(const String& type, bool is_raw) { ...@@ -197,9 +278,9 @@ bool ClipboardWriter::IsValidType(const String& type, bool is_raw) {
if (is_raw) if (is_raw)
return type.length() < mojom::blink::RawClipboardHost::kMaxFormatSize; return type.length() < mojom::blink::RawClipboardHost::kMaxFormatSize;
// TODO(https://crbug.com/931839): Add support for text/html and other // TODO(https://crbug.com/1029857): Add support for other types.
// types. return type == kMimeTypeImagePng || type == kMimeTypeTextPlain ||
return type == kMimeTypeImagePng || type == kMimeTypeTextPlain; type == kMimeTypeTextHTML;
} }
void ClipboardWriter::WriteToSystem(Blob* blob) { void ClipboardWriter::WriteToSystem(Blob* blob) {
...@@ -222,11 +303,7 @@ void ClipboardWriter::DidFinishLoading() { ...@@ -222,11 +303,7 @@ void ClipboardWriter::DidFinishLoading() {
DCHECK(array_buffer); DCHECK(array_buffer);
file_reader_.reset(); file_reader_.reset();
worker_pool::PostTask( StartWrite(clipboard_task_runner_, array_buffer);
FROM_HERE, CrossThreadBindOnce(&ClipboardWriter::DecodeOnBackgroundThread,
WrapCrossThreadPersistent(this),
clipboard_task_runner_,
WrapCrossThreadPersistent(array_buffer)));
} }
void ClipboardWriter::DidFail(FileErrorCode error_code) { void ClipboardWriter::DidFail(FileErrorCode error_code) {
......
...@@ -36,7 +36,10 @@ class ClipboardWriter : public GarbageCollected<ClipboardWriter>, ...@@ -36,7 +36,10 @@ class ClipboardWriter : public GarbageCollected<ClipboardWriter>,
static ClipboardWriter* Create(RawSystemClipboard* raw_system_clipboard, static ClipboardWriter* Create(RawSystemClipboard* raw_system_clipboard,
const String& mime_type, const String& mime_type,
ClipboardPromise* promise); ClipboardPromise* promise);
static ClipboardWriter* Create(SystemClipboard* system_clipboard,
const String& mime_type,
ClipboardPromise* promise,
LocalFrame* frame);
~ClipboardWriter() override; ~ClipboardWriter() override;
static bool IsValidType(const String& type, bool is_raw); static bool IsValidType(const String& type, bool is_raw);
...@@ -55,11 +58,15 @@ class ClipboardWriter : public GarbageCollected<ClipboardWriter>, ...@@ -55,11 +58,15 @@ class ClipboardWriter : public GarbageCollected<ClipboardWriter>,
ClipboardWriter(RawSystemClipboard* raw_system_clipboard, ClipboardWriter(RawSystemClipboard* raw_system_clipboard,
ClipboardPromise* promise); ClipboardPromise* promise);
virtual void StartWrite(
scoped_refptr<base::SingleThreadTaskRunner> task_runner,
DOMArrayBuffer* raw_data) = 0;
virtual void DecodeOnBackgroundThread( virtual void DecodeOnBackgroundThread(
scoped_refptr<base::SingleThreadTaskRunner> task_runner, scoped_refptr<base::SingleThreadTaskRunner> task_runner,
DOMArrayBuffer* raw_data) = 0; DOMArrayBuffer* raw_data) = 0;
// This ClipboardPromise owns this. // This ClipboardPromise owns this.
Member<ClipboardPromise> promise_; Member<ClipboardPromise> promise_;
// Ensure that System Clipboard operations occur on the main thread. // Ensure that System Clipboard operations occur on the main thread.
SEQUENCE_CHECKER(sequence_checker_); SEQUENCE_CHECKER(sequence_checker_);
......
<!doctype html>
<meta charset="utf-8">
<title>
Async Clipboard write ([text/html ClipboardItem]) -> readHtml (and remove scripts) tests
</title>
<link rel="help" href="https://w3c.github.io/clipboard-apis/#async-clipboard-api">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script>
'use strict';
// This function removes extra spaces between tags in html. For example, the
// following html: "<p> Hello </p> <body> World </body>" would turn into this
// html: "<p> Hello </p> <body> World </body>"
// We remove the extra spaces because in html they are considered equivalent,
// but when we are comparing for equality the spaces make a difference.
function reformatHtml(html) {
const parser = new DOMParser();
const htmlString =
parser.parseFromString(html, 'text/html').documentElement.innerHTML;
const reformattedString = htmlString.replace(/\>\s*\</g, '> <');
return reformattedString;
}
// The string must be concatenated in this way because the html parser
// will recognize a script tag even in quotes as a real script tag. By
// splitting it up in this way we avoid that error.
const html_with_script =
'<title>Title of the document</title> <script>const a = 5;</scr'
+ 'ipt> <p>Hello World</p>';
const html_without_script =
'<title>Title of the document</title> <p>Hello World</p>';
promise_test(async t => {
await test_driver.set_permission({name: 'clipboard-read'}, 'granted');
await test_driver.set_permission({name: 'clipboard-write'}, 'granted');
const blobInput = new Blob([html_with_script], {type: 'text/html'});
const clipboardItem = new ClipboardItem({'text/html': blobInput});
await navigator.clipboard.write([clipboardItem]);
const clipboardItems = await navigator.clipboard.read({type: 'text/html'});
const html = clipboardItems[0];
assert_equals(html.types.length, 1);
assert_equals(html.types[0], 'text/html');
const blobOutput = await html.getType('text/html');
assert_equals(blobOutput.type, 'text/html');
const blobText = await (new Response(blobOutput)).text();
const outputHtml = reformatHtml(blobText);
const inputHtml = reformatHtml(html_without_script);
assert_equals(outputHtml, inputHtml);
}, 'Verify write and read clipboard with scripts removed given text/html: '
+ html_with_script);
</script>
<!doctype html>
<meta charset="utf-8">
<title>
Async Clipboard write ([text/html ClipboardItem]) -> readHtml tests
</title>
<link rel="help" href="https://w3c.github.io/clipboard-apis/#async-clipboard-api">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script>
'use strict';
// This function removes extra spaces between tags in html. For example, the
// following html: "<p> Hello </p> <body> World </body>" would turn into this
// html: "<p> Hello </p> <body> World </body>"
// We remove the extra spaces because in html they are considered equivalent,
// but when we are comparing for equality the spaces make a difference.
function reformatHtml(html) {
const parser = new DOMParser();
const htmlString =
parser.parseFromString(html, 'text/html').documentElement.innerHTML;
const reformattedString = htmlString.replace(/\>\s*\</g, '> <');
return reformattedString;
}
async function readWriteTest(textInput) {
await test_driver.set_permission({name: 'clipboard-read'}, 'granted');
await test_driver.set_permission({name: 'clipboard-write'}, 'granted');
const blobInput = new Blob([textInput], {type: 'text/html'});
const clipboardItem = new ClipboardItem({'text/html': blobInput});
await navigator.clipboard.write([clipboardItem]);
const clipboardItems = await navigator.clipboard.read({type: 'text/html'});
const html = clipboardItems[0];
assert_equals(html.types.length, 1);
assert_equals(html.types[0], 'text/html');
const blobOutput = await html.getType('text/html');
assert_equals(blobOutput.type, 'text/html');
const blobText = await (new Response(blobOutput)).text();
const outputHtml = reformatHtml(blobText);
const inputHtml = reformatHtml(textInput);
assert_equals(outputHtml, inputHtml);
}
const testCases = [`<!doctype html> <html> <head> <title>Title of the
document</title> </head> <body> <p>Hello World</p>
</body> </html>`,
'<title>Title of the document</title> <p>Hello World</p>'];
promise_test(async t => {
for (const testCase of testCases) {
await readWriteTest(testCase);
}
}, 'Verify read and write of some text/html content');
</script>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment