[WebIDL] Blob-related methods should use _relevant_ context instead of _current_
https://bugs.webkit.org/show_bug.cgi?id=235279

Patch by Alexey Shvayka <ashvayka@apple.com> on 2022-01-25
Reviewed by Darin Adler.

LayoutTests/imported/w3c:

Import WPT tests from TBA.

* web-platform-tests/FileAPI/blob/Blob-methods-from-detached-frame-expected.txt: Added.
* web-platform-tests/FileAPI/blob/Blob-methods-from-detached-frame.html: Added.
* web-platform-tests/FileAPI/support/empty-document.html: Added.

Source/WebCore:

This patch fixes the following methods to rely on _relevant_ global object instead of _current_:

  1. Blob's slice() / stream() / arrayBuffer() / text() as explicitly required by the File API spec [1].
     Before this change, methods from detached <iframe> were throwing when called on a main frame's Blob.
     Aligns WebKit with Blink and Gecko.

  2. HTMLCanvasElement's toBlob() as per HTML spec [2]: a task should be queued on _relevant_ document's
     event loop.

  3. HTMLCanvasElement's transferControlToOffscreen() / captureStream() per recommendatation for spec
     authors [4], and to align with toBlob(). transferControlToOffscreen() should certainly pass _relevant_
     context, which would be used later for Blob creation.

[1] https://w3c.github.io/FileAPI/#blob-get-stream
[2] https://html.spec.whatwg.org/#canvas-blob-serialisation-task-source
[3] https://html.spec.whatwg.org/multipage/webappapis.html#realms-settings-objects-global-objects:concept-relevant-everything-2

Test: imported/w3c/web-platform-tests/FileAPI/blob/Blob-methods-from-detached-frame.html

Not sure if changes to HTMLCanvasElement methods are even testable.

* fileapi/Blob.cpp:
(WebCore::Blob::slice const):
(WebCore::Blob::loadBlob):
(WebCore::Blob::text):
(WebCore::Blob::arrayBuffer):
(WebCore::Blob::stream):
* fileapi/Blob.h:
* fileapi/Blob.idl:
* html/HTMLCanvasElement.cpp:
(WebCore::HTMLCanvasElement::toBlob):
(WebCore::HTMLCanvasElement::transferControlToOffscreen):
(WebCore::HTMLCanvasElement::captureStream):
* html/HTMLCanvasElement.h:
* html/HTMLCanvasElement.idl:
Mark transferControlToOffscreen() as [NewObject].

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@288592 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/imported/w3c/ChangeLog b/LayoutTests/imported/w3c/ChangeLog
index 0cc1987..0d057c2 100644
--- a/LayoutTests/imported/w3c/ChangeLog
+++ b/LayoutTests/imported/w3c/ChangeLog
@@ -1,5 +1,18 @@
 2022-01-25  Alexey Shvayka  <ashvayka@apple.com>
 
+        [WebIDL] Blob-related methods should use _relevant_ context instead of _current_
+        https://bugs.webkit.org/show_bug.cgi?id=235279
+
+        Reviewed by Darin Adler.
+
+        Import WPT tests from TBA.
+
+        * web-platform-tests/FileAPI/blob/Blob-methods-from-detached-frame-expected.txt: Added.
+        * web-platform-tests/FileAPI/blob/Blob-methods-from-detached-frame.html: Added.
+        * web-platform-tests/FileAPI/support/empty-document.html: Added.
+
+2022-01-25  Alexey Shvayka  <ashvayka@apple.com>
+
         XPath::Step::nodesInAxis(): add null checks after Attr::ownerElement() calls
         https://bugs.webkit.org/show_bug.cgi?id=235500
 
diff --git a/LayoutTests/imported/w3c/web-platform-tests/FileAPI/blob/Blob-methods-from-detached-frame-expected.txt b/LayoutTests/imported/w3c/web-platform-tests/FileAPI/blob/Blob-methods-from-detached-frame-expected.txt
new file mode 100644
index 0000000..bd72619
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/FileAPI/blob/Blob-methods-from-detached-frame-expected.txt
@@ -0,0 +1,6 @@
+
+PASS slice()
+PASS text()
+PASS arrayBuffer()
+PASS stream()
+
diff --git a/LayoutTests/imported/w3c/web-platform-tests/FileAPI/blob/Blob-methods-from-detached-frame.html b/LayoutTests/imported/w3c/web-platform-tests/FileAPI/blob/Blob-methods-from-detached-frame.html
new file mode 100644
index 0000000..37efd5e
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/FileAPI/blob/Blob-methods-from-detached-frame.html
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Blob methods from detached frame work as expected</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<iframe id="emptyDocumentIframe" src="../support/empty-document.html"></iframe>
+
+<script>
+const BlobPrototypeFromDetachedFramePromise = new Promise(resolve => {
+    emptyDocumentIframe.onload = () => {
+        const BlobPrototype = emptyDocumentIframe.contentWindow.Blob.prototype;
+        emptyDocumentIframe.remove();
+        resolve(BlobPrototype);
+    };
+});
+
+const charCodeArrayToString = charCodeArray => Array.from(charCodeArray, c => String.fromCharCode(c)).join("");
+const charCodeBufferToString = charCodeBuffer => charCodeArrayToString(new Uint8Array(charCodeBuffer));
+
+promise_test(async () => {
+    const { slice } = await BlobPrototypeFromDetachedFramePromise;
+    const blob = new Blob(["foobar"]);
+
+    const slicedBlob = slice.call(blob, 1, 3);
+    assert_true(slicedBlob instanceof Blob);
+
+    assert_equals(await slicedBlob.text(), "oo");
+    assert_equals(charCodeBufferToString(await slicedBlob.arrayBuffer()), "oo");
+
+    const reader = slicedBlob.stream().getReader();
+    const { value } = await reader.read();
+    assert_equals(charCodeArrayToString(value), "oo");
+}, "slice()");
+
+promise_test(async () => {
+    const { text } = await BlobPrototypeFromDetachedFramePromise;
+    const blob = new Blob(["foo"]);
+
+    assert_equals(await text.call(blob), "foo");
+}, "text()");
+
+promise_test(async () => {
+    const { arrayBuffer } = await BlobPrototypeFromDetachedFramePromise;
+    const blob = new Blob(["bar"]);
+
+    const charCodeBuffer = await arrayBuffer.call(blob);
+    assert_equals(charCodeBufferToString(charCodeBuffer), "bar");
+}, "arrayBuffer()");
+
+promise_test(async () => {
+    const { stream } = await BlobPrototypeFromDetachedFramePromise;
+    const blob = new Blob(["baz"]);
+
+    const reader = stream.call(blob).getReader();
+    const { value } = await reader.read();
+    assert_equals(charCodeArrayToString(value), "baz");
+}, "stream()");
+</script>
diff --git a/LayoutTests/imported/w3c/web-platform-tests/FileAPI/support/empty-document.html b/LayoutTests/imported/w3c/web-platform-tests/FileAPI/support/empty-document.html
new file mode 100644
index 0000000..b9cd130a
--- /dev/null
+++ b/LayoutTests/imported/w3c/web-platform-tests/FileAPI/support/empty-document.html
@@ -0,0 +1,3 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<body>
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 8dd6ab1..956a3e5 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,47 @@
+2022-01-25  Alexey Shvayka  <ashvayka@apple.com>
+
+        [WebIDL] Blob-related methods should use _relevant_ context instead of _current_
+        https://bugs.webkit.org/show_bug.cgi?id=235279
+
+        Reviewed by Darin Adler.
+
+        This patch fixes the following methods to rely on _relevant_ global object instead of _current_:
+
+          1. Blob's slice() / stream() / arrayBuffer() / text() as explicitly required by the File API spec [1].
+             Before this change, methods from detached <iframe> were throwing when called on a main frame's Blob.
+             Aligns WebKit with Blink and Gecko.
+
+          2. HTMLCanvasElement's toBlob() as per HTML spec [2]: a task should be queued on _relevant_ document's
+             event loop.
+
+          3. HTMLCanvasElement's transferControlToOffscreen() / captureStream() per recommendatation for spec
+             authors [4], and to align with toBlob(). transferControlToOffscreen() should certainly pass _relevant_
+             context, which would be used later for Blob creation.
+
+        [1] https://w3c.github.io/FileAPI/#blob-get-stream
+        [2] https://html.spec.whatwg.org/#canvas-blob-serialisation-task-source
+        [3] https://html.spec.whatwg.org/multipage/webappapis.html#realms-settings-objects-global-objects:concept-relevant-everything-2
+
+        Test: imported/w3c/web-platform-tests/FileAPI/blob/Blob-methods-from-detached-frame.html
+
+        Not sure if changes to HTMLCanvasElement methods are even testable.
+
+        * fileapi/Blob.cpp:
+        (WebCore::Blob::slice const):
+        (WebCore::Blob::loadBlob):
+        (WebCore::Blob::text):
+        (WebCore::Blob::arrayBuffer):
+        (WebCore::Blob::stream):
+        * fileapi/Blob.h:
+        * fileapi/Blob.idl:
+        * html/HTMLCanvasElement.cpp:
+        (WebCore::HTMLCanvasElement::toBlob):
+        (WebCore::HTMLCanvasElement::transferControlToOffscreen):
+        (WebCore::HTMLCanvasElement::captureStream):
+        * html/HTMLCanvasElement.h:
+        * html/HTMLCanvasElement.idl:
+        Mark transferControlToOffscreen() as [NewObject].
+
 2022-01-25  Aditya Keerthi  <akeerthi@apple.com>
 
         Disable input-security CSS property
diff --git a/Source/WebCore/fileapi/Blob.cpp b/Source/WebCore/fileapi/Blob.cpp
index 23608cb..f614a1e 100644
--- a/Source/WebCore/fileapi/Blob.cpp
+++ b/Source/WebCore/fileapi/Blob.cpp
@@ -159,9 +159,9 @@
         (*m_blobLoaders.begin())->cancel();
 }
 
-Ref<Blob> Blob::slice(ScriptExecutionContext& context, long long start, long long end, const String& contentType) const
+Ref<Blob> Blob::slice(long long start, long long end, const String& contentType) const
 {
-    auto blob = adoptRef(*new Blob(&context, m_internalURL, start, end, contentType));
+    auto blob = adoptRef(*new Blob(scriptExecutionContext(), m_internalURL, start, end, contentType));
     blob->suspendIfNeeded();
     return blob;
 }
@@ -196,22 +196,22 @@
     return contentType.convertToASCIILowercase();
 }
 
-void Blob::loadBlob(ScriptExecutionContext& context, FileReaderLoader::ReadType readType, CompletionHandler<void(BlobLoader&)>&& completionHandler)
+void Blob::loadBlob(FileReaderLoader::ReadType readType, CompletionHandler<void(BlobLoader&)>&& completionHandler)
 {
     auto blobLoader = makeUnique<BlobLoader>([this, pendingActivity = makePendingActivity(*this), completionHandler = WTFMove(completionHandler)](BlobLoader& blobLoader) mutable {
         completionHandler(blobLoader);
         m_blobLoaders.take(&blobLoader);
     });
 
-    blobLoader->start(*this, &context, readType);
+    blobLoader->start(*this, scriptExecutionContext(), readType);
 
     if (blobLoader->isLoading())
         m_blobLoaders.add(WTFMove(blobLoader));
 }
 
-void Blob::text(ScriptExecutionContext& context, Ref<DeferredPromise>&& promise)
+void Blob::text(Ref<DeferredPromise>&& promise)
 {
-    loadBlob(context, FileReaderLoader::ReadAsText, [promise = WTFMove(promise)](BlobLoader& blobLoader) mutable {
+    loadBlob(FileReaderLoader::ReadAsText, [promise = WTFMove(promise)](BlobLoader& blobLoader) mutable {
         if (auto optionalErrorCode = blobLoader.errorCode()) {
             promise->reject(Exception { *optionalErrorCode });
             return;
@@ -220,9 +220,9 @@
     });
 }
 
-void Blob::arrayBuffer(ScriptExecutionContext& context, Ref<DeferredPromise>&& promise)
+void Blob::arrayBuffer(Ref<DeferredPromise>&& promise)
 {
-    loadBlob(context, FileReaderLoader::ReadAsArrayBuffer, [promise = WTFMove(promise)](BlobLoader& blobLoader) mutable {
+    loadBlob(FileReaderLoader::ReadAsArrayBuffer, [promise = WTFMove(promise)](BlobLoader& blobLoader) mutable {
         if (auto optionalErrorCode = blobLoader.errorCode()) {
             promise->reject(Exception { *optionalErrorCode });
             return;
@@ -236,7 +236,7 @@
     });
 }
 
-ExceptionOr<Ref<ReadableStream>> Blob::stream(ScriptExecutionContext& scriptExecutionContext)
+ExceptionOr<Ref<ReadableStream>> Blob::stream()
 {
     class BlobStreamSource : public FileReaderLoaderClient, public ReadableStreamSource {
     public:
@@ -290,10 +290,11 @@
         std::optional<Exception> m_exception;
     };
 
-    auto* globalObject = scriptExecutionContext.globalObject();
+    auto* context = scriptExecutionContext();
+    auto* globalObject = context ? context->globalObject() : nullptr;
     if (!globalObject)
         return Exception { InvalidStateError };
-    return ReadableStream::create(*globalObject, adoptRef(*new BlobStreamSource(scriptExecutionContext, *this)));
+    return ReadableStream::create(*globalObject, adoptRef(*new BlobStreamSource(*context, *this)));
 }
 
 #if ASSERT_ENABLED
diff --git a/Source/WebCore/fileapi/Blob.h b/Source/WebCore/fileapi/Blob.h
index bca6696..6eff570 100644
--- a/Source/WebCore/fileapi/Blob.h
+++ b/Source/WebCore/fileapi/Blob.h
@@ -111,11 +111,11 @@
     // URLRegistrable
     URLRegistry& registry() const override;
 
-    Ref<Blob> slice(ScriptExecutionContext&, long long start, long long end, const String& contentType) const;
+    Ref<Blob> slice(long long start, long long end, const String& contentType) const;
 
-    void text(ScriptExecutionContext&, Ref<DeferredPromise>&&);
-    void arrayBuffer(ScriptExecutionContext&, Ref<DeferredPromise>&&);
-    ExceptionOr<Ref<ReadableStream>> stream(ScriptExecutionContext&);
+    void text(Ref<DeferredPromise>&&);
+    void arrayBuffer(Ref<DeferredPromise>&&);
+    ExceptionOr<Ref<ReadableStream>> stream();
 
     // Keeping the handle alive will keep the Blob data alive (but not the Blob object).
     BlobURLHandle handle() const;
@@ -138,7 +138,7 @@
     Blob(ScriptExecutionContext*, const URL& srcURL, long long start, long long end, const String& contentType);
 
 private:
-    void loadBlob(ScriptExecutionContext&, FileReaderLoader::ReadType, CompletionHandler<void(BlobLoader&)>&&);
+    void loadBlob(FileReaderLoader::ReadType, CompletionHandler<void(BlobLoader&)>&&);
 
     // ActiveDOMObject.
     const char* activeDOMObjectName() const override;
diff --git a/Source/WebCore/fileapi/Blob.idl b/Source/WebCore/fileapi/Blob.idl
index 810343e..4aabc97 100644
--- a/Source/WebCore/fileapi/Blob.idl
+++ b/Source/WebCore/fileapi/Blob.idl
@@ -42,9 +42,9 @@
     readonly attribute unsigned long long size;
     readonly attribute DOMString type;
 
-    [CallWith=ScriptExecutionContext] Blob slice(optional long long start = 0, optional long long end = 0x7FFFFFFFFFFFFFFF, optional DOMString contentType = "");
+    [NewObject] Blob slice(optional long long start = 0, optional long long end = 0x7FFFFFFFFFFFFFFF, optional DOMString contentType = "");
 
-    [NewObject, CallWith=ScriptExecutionContext] ReadableStream stream();
-    [NewObject, CallWith=ScriptExecutionContext] Promise<USVString> text();
-    [NewObject, CallWith=ScriptExecutionContext] Promise<ArrayBuffer> arrayBuffer();
+    [NewObject] ReadableStream stream();
+    [NewObject] Promise<USVString> text();
+    [NewObject] Promise<ArrayBuffer> arrayBuffer();
 };
diff --git a/Source/WebCore/html/HTMLCanvasElement.cpp b/Source/WebCore/html/HTMLCanvasElement.cpp
index c3e0166..ac0d382 100644
--- a/Source/WebCore/html/HTMLCanvasElement.cpp
+++ b/Source/WebCore/html/HTMLCanvasElement.cpp
@@ -708,13 +708,13 @@
     return toDataURL(mimeType, { });
 }
 
-ExceptionOr<void> HTMLCanvasElement::toBlob(ScriptExecutionContext& context, Ref<BlobCallback>&& callback, const String& mimeType, JSC::JSValue qualityValue)
+ExceptionOr<void> HTMLCanvasElement::toBlob(Ref<BlobCallback>&& callback, const String& mimeType, JSC::JSValue qualityValue)
 {
     if (!originClean())
         return Exception { SecurityError };
 
     if (size().isEmpty() || !buffer()) {
-        callback->scheduleCallback(context, nullptr);
+        callback->scheduleCallback(document(), nullptr);
         return { };
     }
     if (RuntimeEnabledFeatures::sharedFeatures().webAPIStatisticsEnabled())
@@ -729,7 +729,7 @@
         Vector<uint8_t> blobData = data(imageData->pixelBuffer(), encodingMIMEType, quality);
         if (!blobData.isEmpty())
             blob = Blob::create(&document(), WTFMove(blobData), encodingMIMEType);
-        callback->scheduleCallback(context, WTFMove(blob));
+        callback->scheduleCallback(document(), WTFMove(blob));
         return { };
     }
 #endif
@@ -740,12 +740,12 @@
     Vector<uint8_t> blobData = buffer()->toData(encodingMIMEType, quality);
     if (!blobData.isEmpty())
         blob = Blob::create(&document(), WTFMove(blobData), encodingMIMEType);
-    callback->scheduleCallback(context, WTFMove(blob));
+    callback->scheduleCallback(document(), WTFMove(blob));
     return { };
 }
 
 #if ENABLE(OFFSCREEN_CANVAS)
-ExceptionOr<Ref<OffscreenCanvas>> HTMLCanvasElement::transferControlToOffscreen(ScriptExecutionContext& context)
+ExceptionOr<Ref<OffscreenCanvas>> HTMLCanvasElement::transferControlToOffscreen()
 {
     if (m_context)
         return Exception { InvalidStateError };
@@ -754,7 +754,7 @@
     if (m_context->isAccelerated())
         invalidateStyleAndLayerComposition();
 
-    return OffscreenCanvas::create(context, *this);
+    return OffscreenCanvas::create(document(), *this);
 }
 #endif
 
@@ -807,7 +807,7 @@
 #endif
 }
 
-ExceptionOr<Ref<MediaStream>> HTMLCanvasElement::captureStream(Document& document, std::optional<double>&& frameRequestRate)
+ExceptionOr<Ref<MediaStream>> HTMLCanvasElement::captureStream(std::optional<double>&& frameRequestRate)
 {
     if (!originClean())
         return Exception(SecurityError, "Canvas is tainted"_s);
@@ -817,8 +817,8 @@
     if (frameRequestRate && frameRequestRate.value() < 0)
         return Exception(NotSupportedError, "frameRequestRate is negative"_s);
 
-    auto track = CanvasCaptureMediaStreamTrack::create(document, *this, WTFMove(frameRequestRate));
-    auto stream =  MediaStream::create(document);
+    auto track = CanvasCaptureMediaStreamTrack::create(document(), *this, WTFMove(frameRequestRate));
+    auto stream = MediaStream::create(document());
     stream->addTrack(track);
     return stream;
 }
diff --git a/Source/WebCore/html/HTMLCanvasElement.h b/Source/WebCore/html/HTMLCanvasElement.h
index 921383e..b4e1857 100644
--- a/Source/WebCore/html/HTMLCanvasElement.h
+++ b/Source/WebCore/html/HTMLCanvasElement.h
@@ -96,9 +96,9 @@
 
     WEBCORE_EXPORT ExceptionOr<UncachedString> toDataURL(const String& mimeType, JSC::JSValue quality);
     WEBCORE_EXPORT ExceptionOr<UncachedString> toDataURL(const String& mimeType);
-    ExceptionOr<void> toBlob(ScriptExecutionContext&, Ref<BlobCallback>&&, const String& mimeType, JSC::JSValue quality);
+    ExceptionOr<void> toBlob(Ref<BlobCallback>&&, const String& mimeType, JSC::JSValue quality);
 #if ENABLE(OFFSCREEN_CANVAS)
-    ExceptionOr<Ref<OffscreenCanvas>> transferControlToOffscreen(ScriptExecutionContext&);
+    ExceptionOr<Ref<OffscreenCanvas>> transferControlToOffscreen();
 #endif
 
     // Used for rendering
@@ -108,7 +108,7 @@
 
 #if ENABLE(MEDIA_STREAM)
     RefPtr<MediaSample> toMediaSample();
-    ExceptionOr<Ref<MediaStream>> captureStream(Document&, std::optional<double>&& frameRequestRate);
+    ExceptionOr<Ref<MediaStream>> captureStream(std::optional<double>&& frameRequestRate);
 #endif
 
     Image* copiedImage() const final;
diff --git a/Source/WebCore/html/HTMLCanvasElement.idl b/Source/WebCore/html/HTMLCanvasElement.idl
index de682e1..387e22b 100644
--- a/Source/WebCore/html/HTMLCanvasElement.idl
+++ b/Source/WebCore/html/HTMLCanvasElement.idl
@@ -48,8 +48,8 @@
     [CallWith=GlobalObject] RenderingContext? getContext(DOMString contextId, any... arguments);
 
     DOMString toDataURL(optional DOMString type, optional any quality);
-    [CallWith=ScriptExecutionContext] undefined toBlob(BlobCallback callback, optional DOMString type, optional any quality);
-    [Conditional=OFFSCREEN_CANVAS, EnabledAtRuntime=OffscreenCanvasEnabled, CallWith=ScriptExecutionContext] OffscreenCanvas transferControlToOffscreen();
+    undefined toBlob(BlobCallback callback, optional DOMString type, optional any quality);
+    [Conditional=OFFSCREEN_CANVAS, EnabledAtRuntime=OffscreenCanvasEnabled, NewObject] OffscreenCanvas transferControlToOffscreen();
 
-    [Conditional=MEDIA_STREAM, CallWith=Document, NewObject] MediaStream captureStream(optional double frameRequestRate);
+    [Conditional=MEDIA_STREAM, NewObject] MediaStream captureStream(optional double frameRequestRate);
 };