XMLHttpRequest should propagate user gestures for media playback
authoreric.carlson@apple.com <eric.carlson@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 1 May 2019 15:56:34 +0000 (15:56 +0000)
committereric.carlson@apple.com <eric.carlson@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 1 May 2019 15:56:34 +0000 (15:56 +0000)
https://bugs.webkit.org/show_bug.cgi?id=197428
<rdar://problem/46677392>

Reviewed by Jer Noble.

Source/WebCore:

A user gesture the would allow media state change in effect when XMLHttpRequest.send is
called should be active when the event handlers fire after the transaction completes successfully.

Test: http/tests/media/user-gesture-preserved-across-xmlhttprequest.html

* dom/UserGestureIndicator.cpp:
(WebCore::UserGestureIndicator::UserGestureIndicator): Add a 'scope' parameter to potentially
limit the scope of the gesture to just media.
(WebCore::UserGestureIndicator::~UserGestureIndicator): Clear the scope.
* dom/UserGestureIndicator.h:
(WebCore::UserGestureToken::processingUserGesture const):
(WebCore::UserGestureToken::setScope):
(WebCore::UserGestureToken::resetScope):
(WebCore::UserGestureToken::hasExpired const):

* page/DOMTimer.cpp:
(WebCore::DOMTimerFireState::DOMTimerFireState): Don't need to store the nested timer interval,
UserGestureIndicator knows when it started.
(WebCore::DOMTimer::DOMTimer): Ditto.
(WebCore::DOMTimer::fired): Ditto.
(WebCore::DOMTimerFireState::nestedTimerInterval const): Deleted.
(WebCore::shouldForwardUserGesture): Deleted.
(WebCore::userGestureTokenToForward): Deleted.
(WebCore::currentNestedTimerInterval): Deleted.
* page/DOMTimer.h:

* testing/Internals.cpp:
(WebCore::Internals::setXHRMaximumIntervalForUserGestureForwarding): Override the maximum
user gesture interval for testing.
* testing/Internals.h:
* testing/Internals.idl:

* xml/XMLHttpRequest.cpp:
(WebCore::XMLHttpRequest::XMLHttpRequest):
(WebCore::XMLHttpRequest::send): Stash the user gesture token.
(WebCore::XMLHttpRequest::dispatchEvent): Clear user gesture token if it has expired. If still
valid, activate it.
* xml/XMLHttpRequest.h:

LayoutTests:

* fast/events/popup-blocking-timers5-expected.txt:
* fast/events/popup-blocking-timers5.html: Decrease the timer interval from 1000 to 900
because the user gesture is invalidated based on wall clock time.
* fast/events/popup-blocking-timers6-expected.txt:
* fast/events/popup-blocking-timers6.html: Increase the timer interval from 1001 to 1100
because the user gesture is invalidated based on wall clock time.
* http/tests/media/user-gesture-preserved-across-xmlhttprequest-expected.txt: Added.
* http/tests/media/user-gesture-preserved-across-xmlhttprequest.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@244823 268f45cc-cd09-0410-ab3c-d52691b4dbfc

17 files changed:
LayoutTests/ChangeLog
LayoutTests/fast/events/popup-blocking-timers5-expected.txt
LayoutTests/fast/events/popup-blocking-timers5.html
LayoutTests/fast/events/popup-blocking-timers6-expected.txt
LayoutTests/fast/events/popup-blocking-timers6.html
LayoutTests/http/tests/media/user-gesture-preserved-across-xmlhttprequest-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/media/user-gesture-preserved-across-xmlhttprequest.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/dom/UserGestureIndicator.cpp
Source/WebCore/dom/UserGestureIndicator.h
Source/WebCore/page/DOMTimer.cpp
Source/WebCore/page/DOMTimer.h
Source/WebCore/testing/Internals.cpp
Source/WebCore/testing/Internals.h
Source/WebCore/testing/Internals.idl
Source/WebCore/xml/XMLHttpRequest.cpp
Source/WebCore/xml/XMLHttpRequest.h

index 808aa40..f9f4eeb 100644 (file)
@@ -1,3 +1,20 @@
+2019-05-01  Eric Carlson  <eric.carlson@apple.com>
+
+        XMLHttpRequest should propagate user gestures for media playback
+        https://bugs.webkit.org/show_bug.cgi?id=197428
+        <rdar://problem/46677392>
+
+        Reviewed by Jer Noble.
+
+        * fast/events/popup-blocking-timers5-expected.txt:
+        * fast/events/popup-blocking-timers5.html: Decrease the timer interval from 1000 to 900
+        because the user gesture is invalidated based on wall clock time.
+        * fast/events/popup-blocking-timers6-expected.txt:
+        * fast/events/popup-blocking-timers6.html: Increase the timer interval from 1001 to 1100
+        because the user gesture is invalidated based on wall clock time.
+        * http/tests/media/user-gesture-preserved-across-xmlhttprequest-expected.txt: Added.
+        * http/tests/media/user-gesture-preserved-across-xmlhttprequest.html: Added.
+
 2019-04-29  Darin Adler  <darin@apple.com>
 
         WebKit has too much of its own UTF-8 code and should rely more on ICU's UTF-8 support
index f39f7c7..4143952 100644 (file)
@@ -1,4 +1,4 @@
 Click Here
-Test calling window.open() with a 1000 ms delay. A popup should be allowed.
+Test calling window.open() with a 900 ms delay. A popup should be allowed.
 PASS newWindow is non-null.
 
index 015e77a..bd2215e 100644 (file)
             setTimeout(function() {
                 newWindow = window.open("about:blank");
                 self.focus();
-                debug("Test calling window.open() with a 1000 ms delay. A popup should be allowed.")
+                debug("Test calling window.open() with a 900 ms delay. A popup should be allowed.")
                 shouldBeNonNull("newWindow");
                 if (window.testRunner)
                     testRunner.notifyDone();
-            }, 1000);
+            }, 900);
 
             if (window.eventSender)
-                eventSender.leapForward(1000);
+                eventSender.leapForward(900);
         }
         
         function clickButton() {
index e882387..e2ce606 100644 (file)
@@ -1,4 +1,4 @@
 Click Here
-Test calling window.open() with a 1001 ms delay. A popup should not be allowed.
+Test calling window.open() with a 1100 ms delay. A popup should not be allowed.
 PASS newWindow is null
 
index a19660f..46313de 100644 (file)
             setTimeout(function() {
                 newWindow = window.open("about:blank");
                 self.focus();
-                debug("Test calling window.open() with a 1001 ms delay. A popup should not be allowed.")
+                debug("Test calling window.open() with a 1100 ms delay. A popup should not be allowed.")
                 shouldBeNull("newWindow");
 
                 if (window.testRunner)
                     testRunner.notifyDone();
-            }, 1001);
+            }, 1100);
             if (window.eventSender)
-                eventSender.leapForward(1001);
+                eventSender.leapForward(1100);
         }
         
         function clickButton() {
diff --git a/LayoutTests/http/tests/media/user-gesture-preserved-across-xmlhttprequest-expected.txt b/LayoutTests/http/tests/media/user-gesture-preserved-across-xmlhttprequest-expected.txt
new file mode 100644 (file)
index 0000000..31c854c
--- /dev/null
@@ -0,0 +1,56 @@
+Test that a user gesture is preserved across XHR and setTimeout.
+
+
+Run!
+
+** gesture -> XHR -> timeout -> XHR -> window.open: should fail because XHR only propagates user gesture for media
+sending XHR, delay = 100
+EVENT(load): readyState = 4
+setting timeout, delay = 100
+sending XHR, delay = 100
+EVENT(load): readyState = 4
+EXPECTED (window.open("about:blank") == 'null') OK
+
+** gesture -> timeout -> XHR -> timeout -> window.open: should succeed
+setting timeout, delay = 100
+sending XHR, delay = 100
+EVENT(load): readyState = 4
+setting timeout, delay = 100
+EXPECTED (window.open("about:blank") != 'null') OK
+
+** gesture -> timeout -> XHR -> timeout -> video playback: should succeed
+EVENT(canplaythrough)
+setting timeout, delay = 100
+sending XHR, delay = 100
+EVENT(load): readyState = 4
+setting timeout, delay = 100
+RUN(shouldResolve(mediaElement.play()).then(testComplete, testComplete))
+Promise resolved OK
+
+** gesture -> XHR -> timeout -> XHR -> video playback: should succeed
+EVENT(canplaythrough)
+sending XHR, delay = 100
+EVENT(load): readyState = 4
+setting timeout, delay = 100
+sending XHR, delay = 100
+EVENT(load): readyState = 4
+RUN(shouldResolve(mediaElement.play()).then(testComplete, testComplete))
+Promise resolved OK
+
+** NO gesture -> XHR -> timeout -> video playback: should fail
+EVENT(canplaythrough)
+sending XHR, delay = 100
+EVENT(load): readyState = 4
+setting timeout, delay = 100
+RUN(shouldReject(mediaElement.play()).then(testComplete, testComplete))
+Promise rejected correctly OK
+
+** gesture -> "long" XHR -> video playback: should fail
+EVENT(canplaythrough)
+sending XHR, delay = 300
+EVENT(load): readyState = 4
+RUN(shouldReject(mediaElement.play()).then(testComplete, testComplete))
+Promise rejected correctly OK
+
+END OF TEST
+
diff --git a/LayoutTests/http/tests/media/user-gesture-preserved-across-xmlhttprequest.html b/LayoutTests/http/tests/media/user-gesture-preserved-across-xmlhttprequest.html
new file mode 100644 (file)
index 0000000..82a2e9b
--- /dev/null
@@ -0,0 +1,166 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta charset='utf-8'>
+    <title>user-gesture-preserved-across-xmlhttprequest</title>
+    <script src='/media-resources/media-file.js'></script>
+    <script src='/media-resources/video-test.js'></script>
+
+    <script>
+        let autoRun = true;
+
+        if (window.testRunner) {
+           testRunner.dumpAsText();
+           testRunner.waitUntilDone();
+           testRunner.setCanOpenWindows(true);
+        }
+
+        function doXHR(delay, timeout, completionHandler)
+        {
+            var xhr = new XMLHttpRequest();
+            if (window.internals)
+                window.internals.setXHRMaximumIntervalForUserGestureForwarding(xhr, timeout);
+
+            xhr.open('GET', `/xmlhttprequest/resources/download-with-delay.php?iteration=1&delay=${delay}`, true);
+
+            xhr.onload = (evt) => {
+                consoleWrite(`EVENT(load): readyState = ${xhr.readyState}`);
+                if (xhr.readyState === 4) {
+                    if (xhr.status === 200) {
+                        completionHandler();
+                    } else {
+                        logResult(Failed, `xhr.onload, status = ${xhr.statusText}`);
+                    }
+                }
+            };
+
+            xhr.onerror = (err) => {
+                logResult(Failed, `xhr.onerror, status = ${xhr.statusText}`);
+            };
+
+            xhr.send(null);
+        }
+
+        function testComplete()
+        {
+            if (autoRun)
+                nextTest();
+        }
+
+        function runTest(test)
+        {
+            if (!test.sequence.length) {
+                switch (test.action) {
+                case 'play':
+                    if (test.success)
+                        run("shouldResolve(mediaElement.play()).then(testComplete, testComplete)");
+                    else
+                        run("shouldReject(mediaElement.play()).then(testComplete, testComplete)");
+                    break;
+
+                case 'popup':
+                    if (window.internals)
+                        internals.settings.setJavaScriptCanOpenWindowsAutomatically(false);
+                    testExpected('window.open("about:blank")', null, test.success ? '!=' : '==');
+                    testComplete();
+                    break;
+                }
+                return;
+            }
+
+            let command = test.sequence.shift().trim().split(' ');
+            let delay = command[0];
+            let method = command[1].toLowerCase();
+
+            switch (method) {
+            case 'xhr':
+                consoleWrite(`sending XHR, delay = ${delay}`);
+                doXHR(delay, test.timeout || 0.6, () => runTest(test));
+                break;
+            case 'timeout':
+                consoleWrite(`setting timeout, delay = ${delay}`);
+                setTimeout(() => { runTest(test); }, delay);
+                break;
+            }
+
+        }
+
+        let tests = [
+            { title : 'gesture -> XHR -> timeout -> XHR -> window.open: should fail because XHR only propagates user gesture for media',
+                action : 'popup', withkey : true,  success : false,  sequence : ['100 XHR', '100 timeout', '100 xhr'] },
+
+            { title : 'gesture -> timeout -> XHR -> timeout -> window.open: should succeed',
+                action : 'popup', withkey : true,  success : true,  sequence : ['100 timeout', '100 xhr', '100 timeout'] },
+
+            { title : 'gesture -> timeout -> XHR -> timeout -> video playback: should succeed',
+                action : 'play',  withkey : true,  success : true,  sequence : ['100 timeout', '100 XHR', '100 timeout'] },
+
+            { title : 'gesture -> XHR -> timeout -> XHR -> video playback: should succeed',
+                action : 'play',  withkey : true,  success : true,  sequence : ['100 XHR', '100 timeout', '100 xhr'] },
+
+            { title : 'NO gesture -> XHR -> timeout -> video playback: should fail',
+                action : 'play',  withkey : false,  success : false,  sequence : ['100 XHR', '100 timeout'] },
+
+            { title : 'gesture -> "long" XHR -> video playback: should fail',
+                action : 'play',  withkey : false,  success : false,  timeout : 0.1, sequence : ['300 XHR'] },
+        ];
+
+        function nextTest()
+        {
+            if (!tests.length) {
+                consoleWrite('');
+                endTest()
+                return;
+            }
+
+            let test = tests.shift();
+            let action = () => { runTest(test) };
+            consoleWrite(`<br>** ${test.title}`);
+            switch (test.action) {
+            case 'play':
+                let container = document.getElementById('parent');
+                if (container.firstChild)
+                    container.removeChild(container.firstChild);
+
+                mediaElement = document.createElement('video');
+                if (window.internals)
+                    internals.setMediaElementRestrictions(mediaElement, 'RequireUserGestureForAudioRateChange');
+
+                mediaElement.src = "/media-resources/content/" + findMediaFile("video", "test");
+                mediaElement.controls = 1;
+                container.appendChild(mediaElement);
+                waitForEvent('canplaythrough', event => {
+                    if (test.withkey)
+                        runWithKeyDown(action);
+                    else
+                        action();
+                });
+
+                break;
+
+            case 'popup':
+                if (test.withkey)
+                    runWithKeyDown(action);
+                else
+                    action();
+                break;
+            }
+        }
+
+        function start()
+        {
+            if (autoRun)
+                nextTest();
+        }
+
+    </script>
+</head>
+
+<body onload="start()">
+    <p>Test that a user gesture is preserved across XHR and setTimeout.</p>
+    <div id='parent'></div>
+    <button onclick="nextTest()">Run!</button><br>
+    <pre id='consoleLog'></pre>
+</body>
+</html>
+
index 5dfed73..1b27ebd 100644 (file)
@@ -1,3 +1,50 @@
+2019-05-01  Eric Carlson  <eric.carlson@apple.com>
+
+        XMLHttpRequest should propagate user gestures for media playback
+        https://bugs.webkit.org/show_bug.cgi?id=197428
+        <rdar://problem/46677392>
+
+        Reviewed by Jer Noble.
+
+        A user gesture the would allow media state change in effect when XMLHttpRequest.send is 
+        called should be active when the event handlers fire after the transaction completes successfully.
+
+        Test: http/tests/media/user-gesture-preserved-across-xmlhttprequest.html
+
+        * dom/UserGestureIndicator.cpp:
+        (WebCore::UserGestureIndicator::UserGestureIndicator): Add a 'scope' parameter to potentially
+        limit the scope of the gesture to just media.
+        (WebCore::UserGestureIndicator::~UserGestureIndicator): Clear the scope.
+        * dom/UserGestureIndicator.h:
+        (WebCore::UserGestureToken::processingUserGesture const):
+        (WebCore::UserGestureToken::setScope):
+        (WebCore::UserGestureToken::resetScope):
+        (WebCore::UserGestureToken::hasExpired const):
+
+        * page/DOMTimer.cpp:
+        (WebCore::DOMTimerFireState::DOMTimerFireState): Don't need to store the nested timer interval,
+        UserGestureIndicator knows when it started.
+        (WebCore::DOMTimer::DOMTimer): Ditto.
+        (WebCore::DOMTimer::fired): Ditto.
+        (WebCore::DOMTimerFireState::nestedTimerInterval const): Deleted.
+        (WebCore::shouldForwardUserGesture): Deleted.
+        (WebCore::userGestureTokenToForward): Deleted.
+        (WebCore::currentNestedTimerInterval): Deleted.
+        * page/DOMTimer.h:
+
+        * testing/Internals.cpp:
+        (WebCore::Internals::setXHRMaximumIntervalForUserGestureForwarding): Override the maximum
+        user gesture interval for testing.
+        * testing/Internals.h:
+        * testing/Internals.idl:
+
+        * xml/XMLHttpRequest.cpp:
+        (WebCore::XMLHttpRequest::XMLHttpRequest): 
+        (WebCore::XMLHttpRequest::send): Stash the user gesture token.
+        (WebCore::XMLHttpRequest::dispatchEvent): Clear user gesture token if it has expired. If still
+        valid, activate it.
+        * xml/XMLHttpRequest.h:
+
 2019-04-29  Darin Adler  <darin@apple.com>
 
         WebKit has too much of its own UTF-8 code and should rely more on ICU's UTF-8 support
index 0460c81..0ec20b1 100644 (file)
@@ -70,7 +70,7 @@ UserGestureIndicator::UserGestureIndicator(Optional<ProcessingUserGestureState>
     }
 }
 
-UserGestureIndicator::UserGestureIndicator(RefPtr<UserGestureToken> token)
+UserGestureIndicator::UserGestureIndicator(RefPtr<UserGestureToken> token, UserGestureToken::GestureScope scope)
 {
     // Silently ignore UserGestureIndicators on non main threads.
     if (!isMainThread())
@@ -79,8 +79,10 @@ UserGestureIndicator::UserGestureIndicator(RefPtr<UserGestureToken> token)
     // It is only safe to use currentToken() on the main thread.
     m_previousToken = currentToken();
 
-    if (token)
+    if (token) {
+        token->setScope(scope);
         currentToken() = token;
+    }
 }
 
 UserGestureIndicator::~UserGestureIndicator()
@@ -88,8 +90,10 @@ UserGestureIndicator::~UserGestureIndicator()
     if (!isMainThread())
         return;
     
-    if (auto token = currentToken())
+    if (auto token = currentToken()) {
         token->resetDOMPasteAccess();
+        token->resetScope();
+    }
 
     currentToken() = m_previousToken;
 }
index 66bf7dd..cca4240 100644 (file)
@@ -27,6 +27,7 @@
 
 #include "DOMPasteAccess.h"
 #include <wtf/Function.h>
+#include <wtf/MonotonicTime.h>
 #include <wtf/Noncopyable.h>
 #include <wtf/RefCounted.h>
 #include <wtf/Vector.h>
@@ -53,7 +54,7 @@ public:
     WEBCORE_EXPORT ~UserGestureToken();
 
     ProcessingUserGestureState state() const { return m_state; }
-    bool processingUserGesture() const { return m_state == ProcessingUserGesture; }
+    bool processingUserGesture() const { return m_scope == GestureScope::All && m_state == ProcessingUserGesture; }
     bool processingUserGestureForMedia() const { return m_state == ProcessingUserGesture || m_state == ProcessingPotentialUserGesture; }
     UserGestureType gestureType() const { return m_gestureType; }
 
@@ -78,6 +79,15 @@ public:
     }
     void resetDOMPasteAccess() { m_domPasteAccessPolicy = DOMPasteAccessPolicy::NotRequestedYet; }
 
+    enum class GestureScope { All, MediaOnly };
+    void setScope(GestureScope scope) { m_scope = scope; }
+    void resetScope() { m_scope = GestureScope::All; }
+
+    bool hasExpired(Seconds expirationInterval) const
+    {
+        return m_startTime + expirationInterval < MonotonicTime::now();
+    }
+
 private:
     UserGestureToken(ProcessingUserGestureState state, UserGestureType gestureType)
         : m_state(state)
@@ -89,6 +99,8 @@ private:
     Vector<WTF::Function<void (UserGestureToken&)>> m_destructionObservers;
     UserGestureType m_gestureType;
     DOMPasteAccessPolicy m_domPasteAccessPolicy { DOMPasteAccessPolicy::NotRequestedYet };
+    GestureScope m_scope { GestureScope::All };
+    MonotonicTime m_startTime { MonotonicTime::now() };
 };
 
 class UserGestureIndicator {
@@ -103,7 +115,7 @@ public:
     // If a document is provided, its last known user gesture timestamp is updated.
     enum class ProcessInteractionStyle { Immediate, Delayed };
     WEBCORE_EXPORT explicit UserGestureIndicator(Optional<ProcessingUserGestureState>, Document* = nullptr, UserGestureType = UserGestureType::Other, ProcessInteractionStyle = ProcessInteractionStyle::Immediate);
-    WEBCORE_EXPORT explicit UserGestureIndicator(RefPtr<UserGestureToken>);
+    WEBCORE_EXPORT explicit UserGestureIndicator(RefPtr<UserGestureToken>, UserGestureToken::GestureScope = UserGestureToken::GestureScope::All);
     WEBCORE_EXPORT ~UserGestureIndicator();
 
 private:
index 64f2654..6a22dbe 100644 (file)
@@ -53,9 +53,8 @@ static const int maxTimerNestingLevel = 5;
 
 class DOMTimerFireState {
 public:
-    DOMTimerFireState(ScriptExecutionContext& context, int nestingLevel, const Seconds& nestedTimerInterval)
+    DOMTimerFireState(ScriptExecutionContext& context, int nestingLevel)
         : m_context(context)
-        , m_nestedTimerInterval(nestedTimerInterval)
         , m_contextIsDocument(is<Document>(m_context))
     {
         // For worker threads, don't update the current DOMTimerFireState.
@@ -78,8 +77,6 @@ public:
 
     Document* contextDocument() const { return m_contextIsDocument ? &downcast<Document>(m_context) : nullptr; }
 
-    const Seconds& nestedTimerInterval() const { return m_nestedTimerInterval; }
-
     void setScriptMadeUserObservableChanges() { m_scriptMadeUserObservableChanges = true; }
     void setScriptMadeNonUserObservableChanges() { m_scriptMadeNonUserObservableChanges = true; }
 
@@ -98,7 +95,6 @@ public:
 
 private:
     ScriptExecutionContext& m_context;
-    Seconds m_nestedTimerInterval;
     uint64_t m_initialDOMTreeVersion;
     DOMTimerFireState* m_previous;
     bool m_contextIsDocument;
@@ -163,27 +159,6 @@ private:
 
 bool NestedTimersMap::isTrackingNestedTimers = false;
 
-static inline bool shouldForwardUserGesture(Seconds interval)
-{
-    return UserGestureIndicator::processingUserGesture()
-        && interval <= maxIntervalForUserGestureForwarding;
-}
-
-static inline RefPtr<UserGestureToken> userGestureTokenToForward(Seconds interval)
-{
-    if (!shouldForwardUserGesture(interval))
-        return nullptr;
-
-    return UserGestureIndicator::currentUserGesture();
-}
-
-static inline Seconds currentNestedTimerInterval()
-{
-    if (DOMTimerFireState::current)
-        return DOMTimerFireState::current->nestedTimerInterval();
-    return { };
-}
-
 DOMTimer::DOMTimer(ScriptExecutionContext& context, std::unique_ptr<ScheduledAction> action, Seconds interval, bool singleShot)
     : SuspendableTimer(context)
     , m_nestingLevel(context.timerNestingLevel())
@@ -191,8 +166,7 @@ DOMTimer::DOMTimer(ScriptExecutionContext& context, std::unique_ptr<ScheduledAct
     , m_originalInterval(interval)
     , m_throttleState(Undetermined)
     , m_currentTimerInterval(intervalClampedToMinimum())
-    , m_nestedTimerInterval(currentNestedTimerInterval())
-    , m_userGestureTokenToForward(userGestureTokenToForward(m_nestedTimerInterval + m_currentTimerInterval))
+    , m_userGestureTokenToForward(UserGestureIndicator::currentUserGesture())
 {
     RefPtr<DOMTimer> reference = adoptRef(this);
 
@@ -310,7 +284,10 @@ void DOMTimer::fired()
     ASSERT(scriptExecutionContext());
     ScriptExecutionContext& context = *scriptExecutionContext();
 
-    DOMTimerFireState fireState(context, std::min(m_nestingLevel + 1, maxTimerNestingLevel), m_nestedTimerInterval + m_currentTimerInterval);
+    DOMTimerFireState fireState(context, std::min(m_nestingLevel + 1, maxTimerNestingLevel));
+
+    if (m_userGestureTokenToForward && m_userGestureTokenToForward->hasExpired(maxIntervalForUserGestureForwarding))
+        m_userGestureTokenToForward = nullptr;
 
     ASSERT(!isSuspended());
     ASSERT(!context.activeDOMObjectsAreSuspended());
index 9438657..b9755a7 100644 (file)
@@ -92,7 +92,6 @@ private:
     Seconds m_originalInterval;
     TimerThrottleState m_throttleState;
     Seconds m_currentTimerInterval;
-    Seconds m_nestedTimerInterval;
     RefPtr<UserGestureToken> m_userGestureTokenToForward;
 };
 
index 10edd27..d4cd40b 100644 (file)
@@ -5032,4 +5032,9 @@ void Internals::testDictionaryLogging()
     page->diagnosticLoggingClient().logDiagnosticMessageWithValueDictionary("testMessage"_s, "testDescription"_s, dictionary, ShouldSample::No);
 }
 
+void Internals::setXHRMaximumIntervalForUserGestureForwarding(XMLHttpRequest& request, double interval)
+{
+    request.setMaximumIntervalForUserGestureForwarding(interval);
+}
+
 } // namespace WebCore
index fbc74ab..491197a 100644 (file)
@@ -816,7 +816,9 @@ public:
     void processDidResume();
 
     void testDictionaryLogging();
-        
+
+    void setXHRMaximumIntervalForUserGestureForwarding(XMLHttpRequest&, double);
+
 private:
     explicit Internals(Document&);
     Document* contextDocument() const;
index d883c04..78d4b88 100644 (file)
@@ -747,4 +747,6 @@ enum CompositingPolicy {
     void processDidResume();
 
     void testDictionaryLogging();
+
+    void setXHRMaximumIntervalForUserGestureForwarding(XMLHttpRequest xhr, double interval);
 };
index a20e256..2f717c3 100644 (file)
@@ -66,6 +66,8 @@
 
 namespace WebCore {
 
+static const Seconds maximumIntervalForUserGestureForwarding { 10_s };
+
 WTF_MAKE_ISO_ALLOCATED_IMPL(XMLHttpRequest);
 
 DEFINE_DEBUG_ONLY_GLOBAL(WTF::RefCountedLeakCounter, xmlHttpRequestCounter, ("XMLHttpRequest"));
@@ -121,6 +123,7 @@ XMLHttpRequest::XMLHttpRequest(ScriptExecutionContext& context)
     , m_resumeTimer(*this, &XMLHttpRequest::resumeTimerFired)
     , m_networkErrorTimer(*this, &XMLHttpRequest::networkErrorTimerFired)
     , m_timeoutTimer(*this, &XMLHttpRequest::didReachTimeout)
+    , m_maximumIntervalForUserGestureForwarding(maximumIntervalForUserGestureForwarding)
 {
 #ifndef NDEBUG
     xmlHttpRequestCounter.increment();
@@ -436,6 +439,7 @@ Optional<ExceptionOr<void>> XMLHttpRequest::prepareToSend()
 ExceptionOr<void> XMLHttpRequest::send(Optional<SendTypes>&& sendType)
 {
     InspectorInstrumentation::willSendXMLHttpRequest(scriptExecutionContext(), url());
+    m_userGestureToken = UserGestureIndicator::currentUserGesture();
 
     ExceptionOr<void> result;
     if (!sendType)
@@ -1089,6 +1093,20 @@ void XMLHttpRequest::didReceiveData(const char* data, int len)
     }
 }
 
+void XMLHttpRequest::dispatchEvent(Event& event)
+{
+    if (m_userGestureToken && m_userGestureToken->hasExpired(m_maximumIntervalForUserGestureForwarding))
+        m_userGestureToken = nullptr;
+
+    if (readyState() != DONE || !m_userGestureToken || !m_userGestureToken->processingUserGesture()) {
+        EventTarget::dispatchEvent(event);
+        return;
+    }
+
+    UserGestureIndicator gestureIndicator(m_userGestureToken, UserGestureToken::GestureScope::MediaOnly);
+    EventTarget::dispatchEvent(event);
+}
+
 void XMLHttpRequest::dispatchErrorEvents(const AtomicString& type)
 {
     if (!m_uploadComplete) {
@@ -1188,4 +1206,9 @@ void XMLHttpRequest::contextDestroyed()
     ActiveDOMObject::contextDestroyed();
 }
 
+void XMLHttpRequest::setMaximumIntervalForUserGestureForwarding(double interval)
+{
+    m_maximumIntervalForUserGestureForwarding = Seconds(interval);    
+}
+
 } // namespace WebCore
index 9299f4c..2da53e5 100644 (file)
@@ -26,6 +26,7 @@
 #include "FormData.h"
 #include "ResourceResponse.h"
 #include "ThreadableLoaderClient.h"
+#include "UserGestureIndicator.h"
 #include <wtf/URL.h>
 #include "XMLHttpRequestEventTarget.h"
 #include "XMLHttpRequestProgressEventThrottle.h"
@@ -127,6 +128,8 @@ public:
 
     size_t memoryCost() const;
 
+    WEBCORE_EXPORT void setMaximumIntervalForUserGestureForwarding(double);
+
 private:
     explicit XMLHttpRequest(ScriptExecutionContext&);
 
@@ -187,6 +190,9 @@ private:
 
     void dispatchErrorEvents(const AtomicString&);
 
+    using EventTarget::dispatchEvent;
+    void dispatchEvent(Event&) override;
+
     void resumeTimerFired();
     Ref<TextResourceDecoder> createDecoder() const;
 
@@ -243,6 +249,8 @@ private:
     MonotonicTime m_sendingTime;
 
     Optional<ExceptionCode> m_exceptionCode;
+    RefPtr<UserGestureToken> m_userGestureToken;
+    Seconds m_maximumIntervalForUserGestureForwarding;
 };
 
 inline auto XMLHttpRequest::responseType() const -> ResponseType