1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 15:37:46 +00:00

LibWeb: Implement setTimeout/setInterval with ESO according to the spec

Our setInterval implementation currently crashes on DuckDuckGo when it's
invoked with a string argument. In this path, we were creating a native
function to evaluate and execute that string. That evaluation was always
returning a Completion, but NativeFunction expects ThrowCompletionOr.
The conversion from Completion to ThrowCompletionOr would fail a VERIFY
because that conversion is only valid if the Completion is an error; but
we would trigger this conversion even on success.

This change re-implements setTimeout & setInterval in direct accordance
with the spec. So we avoid making that NativeFunction altogether, and
DDG can progress past its invocation to the timer. With this change, we
also have other features we did not previously support, such as passing
any number of arguments to the timers. This does not implement handling
of nesting levels yet.
This commit is contained in:
Timothy Flynn 2022-03-04 10:41:12 -05:00 committed by Andreas Kling
parent 8156ec5da8
commit 18b9d02edd
6 changed files with 184 additions and 138 deletions

View file

@ -41,7 +41,6 @@
#include <LibWeb/DOM/Window.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/EventHandler.h>
#include <LibWeb/HTML/Scripting/ClassicScript.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Storage.h>
#include <LibWeb/Origin.h>
@ -222,96 +221,80 @@ JS_DEFINE_NATIVE_FUNCTION(WindowObject::prompt)
return JS::js_string(vm, response);
}
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-setinterval
JS_DEFINE_NATIVE_FUNCTION(WindowObject::set_interval)
static JS::ThrowCompletionOr<TimerHandler> make_timer_handler(JS::GlobalObject& global_object, JS::Value handler)
{
// FIXME: Ideally this would share more code with setTimeout() using the "timer initialization steps"
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timer-initialisation-steps
auto* impl = TRY(impl_from(vm, global_object));
if (!vm.argument_count())
return vm.throw_completion<JS::TypeError>(global_object, JS::ErrorType::BadArgCountAtLeastOne, "setInterval");
JS::Object* callback_object;
if (vm.argument(0).is_function()) {
callback_object = &vm.argument(0).as_function();
} else {
auto script_source = TRY(vm.argument(0).to_string(global_object));
// FIXME: This needs more work once we have a environment settings object.
// The spec wants us to use a task for the "run function or script string" part,
// using a NativeFunction for the latter is a workaround so that we can reuse the
// DOM::Timer API unaltered (always expects a JS::FunctionObject).
callback_object = JS::NativeFunction::create(global_object, "", [impl, script_source = move(script_source)](auto&, auto&) mutable {
auto& settings_object = verify_cast<HTML::EnvironmentSettingsObject>(*impl->associated_document().realm().host_defined());
auto script = HTML::ClassicScript::create(impl->associated_document().url().to_string(), script_source, settings_object, AK::URL());
return script->run();
});
}
i32 interval = 0;
if (vm.argument_count() >= 2) {
interval = TRY(vm.argument(1).to_i32(global_object));
if (interval < 0)
interval = 0;
}
NonnullOwnPtr<Bindings::CallbackType> callback = adopt_own(*new Bindings::CallbackType(JS::make_handle(callback_object), HTML::incumbent_settings_object()));
// FIXME: Pass ...arguments to the callback function when it's invoked
auto timer_id = impl->set_interval(move(callback), interval);
return JS::Value(timer_id);
if (handler.is_function())
return Bindings::CallbackType(JS::make_handle<JS::Object>(handler.as_function()), HTML::incumbent_settings_object());
return TRY(handler.to_string(global_object));
}
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-settimeout
JS_DEFINE_NATIVE_FUNCTION(WindowObject::set_timeout)
{
// FIXME: Ideally this would share more code with setInterval() using the "timer initialization steps"
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timer-initialisation-steps
auto* impl = TRY(impl_from(vm, global_object));
if (!vm.argument_count())
return vm.throw_completion<JS::TypeError>(global_object, JS::ErrorType::BadArgCountAtLeastOne, "setTimeout");
JS::Object* callback_object;
if (vm.argument(0).is_function()) {
callback_object = &vm.argument(0).as_function();
} else {
auto script_source = TRY(vm.argument(0).to_string(global_object));
// FIXME: This needs more work once we have a environment settings object.
// The spec wants us to use a task for the "run function or script string" part,
// using a NativeFunction for the latter is a workaround so that we can reuse the
// DOM::Timer API unaltered (always expects a JS::FunctionObject).
callback_object = JS::NativeFunction::create(global_object, "", [impl, script_source = move(script_source)](auto&, auto&) mutable {
auto& settings_object = verify_cast<HTML::EnvironmentSettingsObject>(*impl->associated_document().realm().host_defined());
auto script = HTML::ClassicScript::create(impl->associated_document().url().to_string(), script_source, settings_object, AK::URL());
return script->run();
});
}
i32 interval = 0;
if (vm.argument_count() >= 2) {
interval = TRY(vm.argument(1).to_i32(global_object));
if (interval < 0)
interval = 0;
}
NonnullOwnPtr<Bindings::CallbackType> callback = adopt_own(*new Bindings::CallbackType(JS::make_handle(callback_object), HTML::incumbent_settings_object()));
auto handler = TRY(make_timer_handler(global_object, vm.argument(0)));
// FIXME: Pass ...arguments to the callback function when it's invoked
auto timer_id = impl->set_timeout(move(callback), interval);
return JS::Value(timer_id);
i32 timeout = 0;
if (vm.argument_count() >= 2)
timeout = TRY(vm.argument(1).to_i32(global_object));
JS::MarkedVector<JS::Value> arguments { vm.heap() };
for (size_t i = 2; i < vm.argument_count(); ++i)
arguments.append(vm.argument(i));
auto id = impl->set_timeout(move(handler), timeout, move(arguments));
return JS::Value(id);
}
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-setinterval
JS_DEFINE_NATIVE_FUNCTION(WindowObject::set_interval)
{
auto* impl = TRY(impl_from(vm, global_object));
if (!vm.argument_count())
return vm.throw_completion<JS::TypeError>(global_object, JS::ErrorType::BadArgCountAtLeastOne, "setInterval");
auto handler = TRY(make_timer_handler(global_object, vm.argument(0)));
i32 timeout = 0;
if (vm.argument_count() >= 2)
timeout = TRY(vm.argument(1).to_i32(global_object));
JS::MarkedVector<JS::Value> arguments { vm.heap() };
for (size_t i = 2; i < vm.argument_count(); ++i)
arguments.append(vm.argument(i));
auto id = impl->set_interval(move(handler), timeout, move(arguments));
return JS::Value(id);
}
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-cleartimeout
JS_DEFINE_NATIVE_FUNCTION(WindowObject::clear_timeout)
{
auto* impl = TRY(impl_from(vm, global_object));
if (!vm.argument_count())
return vm.throw_completion<JS::TypeError>(global_object, JS::ErrorType::BadArgCountAtLeastOne, "clearTimeout");
i32 timer_id = TRY(vm.argument(0).to_i32(global_object));
impl->clear_timeout(timer_id);
i32 id = 0;
if (vm.argument_count())
id = TRY(vm.argument(0).to_i32(global_object));
impl->clear_timeout(id);
return JS::js_undefined();
}
// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-clearinterval
JS_DEFINE_NATIVE_FUNCTION(WindowObject::clear_interval)
{
auto* impl = TRY(impl_from(vm, global_object));
if (!vm.argument_count())
return vm.throw_completion<JS::TypeError>(global_object, JS::ErrorType::BadArgCountAtLeastOne, "clearInterval");
i32 timer_id = TRY(vm.argument(0).to_i32(global_object));
impl->clear_interval(timer_id);
i32 id = 0;
if (vm.argument_count())
id = TRY(vm.argument(0).to_i32(global_object));
impl->clear_interval(id);
return JS::js_undefined();
}