diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index 8d825b4d02..67e9b4170b 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -77,6 +78,7 @@ #include #include #include +#include #include #include #include @@ -1756,6 +1758,10 @@ void Document::completely_finish_loading() // 2. Set document's completely loaded time to the current time. m_completely_loaded_time = AK::UnixDateTime::now(); + // NOTE: See the end of shared_declarative_refresh_steps. + if (m_active_refresh_timer) + m_active_refresh_timer->start(); + // 3. Let container be document's browsing context's container. auto container = JS::make_handle(browsing_context()->container()); @@ -3049,4 +3055,162 @@ void Document::start_intersection_observing_a_lazy_loading_element(Element& elem m_lazy_load_intersection_observer->observe(element); } +// https://html.spec.whatwg.org/multipage/semantics.html#shared-declarative-refresh-steps +void Document::shared_declarative_refresh_steps(StringView input, JS::GCPtr meta_element) +{ + // 1. If document's will declaratively refresh is true, then return. + if (m_will_declaratively_refresh) + return; + + // 2. Let position point at the first code point of input. + GenericLexer lexer(input); + + // 3. Skip ASCII whitespace within input given position. + lexer.ignore_while(Infra::is_ascii_whitespace); + + // 4. Let time be 0. + u32 time = 0; + + // 5. Collect a sequence of code points that are ASCII digits from input given position, and let the result be timeString. + auto time_string = lexer.consume_while(is_ascii_digit); + + // 6. If timeString is the empty string, then: + if (time_string.is_empty()) { + // 1. If the code point in input pointed to by position is not U+002E (.), then return. + if (lexer.peek() != '.') + return; + } + + // 7. Otherwise, set time to the result of parsing timeString using the rules for parsing non-negative integers. + // FIXME: Not sure if this exactly matches the spec's "rules for parsing non-negative integers". + auto maybe_time = time_string.to_uint(); + + // FIXME: Since we only collected ASCII digits, this can only fail because of overflow. What do we do when that happens? For now, default to 0. + if (maybe_time.has_value() && maybe_time.value() < NumericLimits::max() && !Checked::multiplication_would_overflow(static_cast(maybe_time.value()), 1000)) { + time = maybe_time.value(); + } + + // 8. Collect a sequence of code points that are ASCII digits and U+002E FULL STOP characters (.) from input given + // position. Ignore any collected characters. + lexer.ignore_while([](auto c) { + return is_ascii_digit(c) || c == '.'; + }); + + // 9. Let urlRecord be document's URL. + auto url_record = url(); + + // 10. If position is not past the end of input, then: + if (!lexer.is_eof()) { + // 1. If the code point in input pointed to by position is not U+003B (;), U+002C (,), or ASCII whitespace, then return. + if (lexer.peek() != ';' && lexer.peek() != ',' && !Infra::is_ascii_whitespace(lexer.peek())) + return; + + // 2. Skip ASCII whitespace within input given position. + lexer.ignore_while(Infra::is_ascii_whitespace); + + // 3. If the code point in input pointed to by position is U+003B (;) or U+002C (,), then advance position to the next code point. + if (lexer.peek() == ';' || lexer.peek() == ',') + lexer.ignore(1); + + // 4. Skip ASCII whitespace within input given position. + lexer.ignore_while(Infra::is_ascii_whitespace); + } + + // 11. If position is not past the end of input, then: + if (!lexer.is_eof()) { + // 1. Let urlString be the substring of input from the code point at position to the end of the string. + auto url_string = lexer.remaining(); + + // 2. If the code point in input pointed to by position is U+0055 (U) or U+0075 (u), then advance position to the next code point. Otherwise, jump to the step labeled skip quotes. + if (lexer.peek() == 'U' || lexer.peek() == 'u') + lexer.ignore(1); + else + goto skip_quotes; + + // 3. If the code point in input pointed to by position is U+0052 (R) or U+0072 (r), then advance position to the next code point. Otherwise, jump to the step labeled parse. + if (lexer.peek() == 'R' || lexer.peek() == 'r') + lexer.ignore(1); + else + goto parse; + + // 4. If the code point in input pointed to by position is U+004C (L) or U+006C (l), then advance position to the next code point. Otherwise, jump to the step labeled parse. + if (lexer.peek() == 'L' || lexer.peek() == 'l') + lexer.ignore(1); + else + goto parse; + + // 5. Skip ASCII whitespace within input given position. + lexer.ignore_while(Infra::is_ascii_whitespace); + + // 6. If the code point in input pointed to by position is U+003D (=), then advance position to the next code point. Otherwise, jump to the step labeled parse. + if (lexer.peek() == '=') + lexer.ignore(1); + else + goto parse; + + // 7. Skip ASCII whitespace within input given position. + lexer.ignore_while(Infra::is_ascii_whitespace); + + skip_quotes : { + // 8. Skip quotes: If the code point in input pointed to by position is U+0027 (') or U+0022 ("), then let + // quote be that code point, and advance position to the next code point. Otherwise, let quote be the empty + // string. + Optional quote; + if (lexer.peek() == '\'' || lexer.peek() == '"') + quote = lexer.consume(); + + // 9. Set urlString to the substring of input from the code point at position to the end of the string. + // 10. If quote is not the empty string, and there is a code point in urlString equal to quote, then truncate + // urlString at that code point, so that it and all subsequent code points are removed. + url_string = lexer.consume_while(["e](auto c) { + return !quote.has_value() || c != quote.value(); + }); + } + + parse: + // 11. Parse: Parse urlString relative to document. If that fails, return. Otherwise, set urlRecord to the + // resulting URL record. + auto maybe_url_record = parse_url(url_string); + if (!maybe_url_record.is_valid()) + return; + + url_record = maybe_url_record; + } + + // 12. Set document's will declaratively refresh to true. + m_will_declaratively_refresh = true; + + // 13. Perform one or more of the following steps: + // - After the refresh has come due (as defined below), if the user has not canceled the redirect and, if meta is + // given, document's active sandboxing flag set does not have the sandboxed automatic features browsing context + // flag set, then navigate document's node navigable to urlRecord using document, with historyHandling set to + // "replace". + m_active_refresh_timer = Core::Timer::create_single_shot(time * 1000, [this, has_meta_element = !!meta_element, url_record = move(url_record)]() { + if (has_meta_element && active_sandboxing_flag_set().flags & HTML::SandboxingFlagSet::SandboxedAutomaticFeatures) + return; + + // FIXME: Use navigables when they're used for all navigation (otherwise, navigable() would be null in some cases) + VERIFY(browsing_context()); + auto request = Fetch::Infrastructure::Request::create(vm()); + request->set_url(url_record); + MUST(browsing_context()->navigate(request, *browsing_context(), false, HTML::HistoryHandlingBehavior::Replace)); + }).release_value_but_fixme_should_propagate_errors(); + + // For the purposes of the previous paragraph, a refresh is said to have come due as soon as the later of the + // following two conditions occurs: + + // - At least time seconds have elapsed since document's completely loaded time, adjusted to take into + // account user or user agent preferences. + // m_active_refresh_timer is started in completely_finished_loading after setting the completely loaded time. + + // - If meta is given, at least time seconds have elapsed since meta was inserted into the document document, + // adjusted to take into account user or user agent preferences. + // NOTE: This is only done if completely loaded time has a value because shared_declarative_refresh_steps is called + // by HTMLMetaElement::inserted and if the document hasn't finished loading when the meta element was inserted, + // then the document completely finishing loading will _always_ come after inserting the meta element. + if (meta_element && m_completely_loaded_time.has_value()) { + m_active_refresh_timer->start(); + } +} + } diff --git a/Userland/Libraries/LibWeb/DOM/Document.h b/Userland/Libraries/LibWeb/DOM/Document.h index e2b34fc916..08c6c9086c 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.h +++ b/Userland/Libraries/LibWeb/DOM/Document.h @@ -495,6 +495,8 @@ public: void start_intersection_observing_a_lazy_loading_element(Element& element); + void shared_declarative_refresh_steps(StringView input, JS::GCPtr meta_element = nullptr); + protected: virtual JS::ThrowCompletionOr initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; @@ -676,6 +678,12 @@ private: // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#lazy-load-intersection-observer // Each Document has a lazy load intersection observer, initially set to null but can be set to an IntersectionObserver instance. JS::GCPtr m_lazy_load_intersection_observer; + + // https://html.spec.whatwg.org/multipage/semantics.html#will-declaratively-refresh + // A Document object has an associated will declaratively refresh (a boolean). It is initially false. + bool m_will_declaratively_refresh { false }; + + RefPtr m_active_refresh_timer; }; template<> diff --git a/Userland/Libraries/LibWeb/HTML/HTMLMetaElement.cpp b/Userland/Libraries/LibWeb/HTML/HTMLMetaElement.cpp index c32581290d..85456fd1da 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLMetaElement.cpp +++ b/Userland/Libraries/LibWeb/HTML/HTMLMetaElement.cpp @@ -1,10 +1,12 @@ /* * Copyright (c) 2020, the SerenityOS developers. + * Copyright (c) 2023, Luke Wilde * * SPDX-License-Identifier: BSD-2-Clause */ #include +#include #include namespace Web::HTML { @@ -24,4 +26,50 @@ JS::ThrowCompletionOr HTMLMetaElement::initialize(JS::Realm& realm) return {}; } +Optional HTMLMetaElement::http_equiv_state() const +{ + auto value = attribute(HTML::AttributeNames::http_equiv); + +#define __ENUMERATE_HTML_META_HTTP_EQUIV_ATTRIBUTE(keyword, state) \ + if (value.equals_ignoring_ascii_case(#keyword##sv)) \ + return HTMLMetaElement::HttpEquivAttributeState::state; + ENUMERATE_HTML_META_HTTP_EQUIV_ATTRIBUTES +#undef __ENUMERATE_HTML_META_HTTP_EQUIV_ATTRIBUTE + + return OptionalNone {}; +} + +void HTMLMetaElement::inserted() +{ + Base::inserted(); + + // https://html.spec.whatwg.org/multipage/semantics.html#pragma-directives + // When a meta element is inserted into the document, if its http-equiv attribute is present and represents one of + // the above states, then the user agent must run the algorithm appropriate for that state, as described in the + // following list: + auto http_equiv = http_equiv_state(); + if (http_equiv.has_value()) { + switch (http_equiv.value()) { + case HttpEquivAttributeState::Refresh: { + // https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-http-equiv-refresh + // 1. If the meta element has no content attribute, or if that attribute's value is the empty string, then return. + // 2. Let input be the value of the element's content attribute. + if (!has_attribute(AttributeNames::content)) + break; + + auto input = attribute(AttributeNames::content); + if (input.is_empty()) + break; + + // 3. Run the shared declarative refresh steps with the meta element's node document, input, and the meta element. + document().shared_declarative_refresh_steps(input, this); + break; + } + default: + dbgln("FIXME: Implement '{}' http-equiv state", attribute(AttributeNames::http_equiv)); + break; + } + } +} + } diff --git a/Userland/Libraries/LibWeb/HTML/HTMLMetaElement.h b/Userland/Libraries/LibWeb/HTML/HTMLMetaElement.h index dc81eaff21..1124dbf135 100644 --- a/Userland/Libraries/LibWeb/HTML/HTMLMetaElement.h +++ b/Userland/Libraries/LibWeb/HTML/HTMLMetaElement.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2020, the SerenityOS developers. + * Copyright (c) 2023, Luke Wilde * * SPDX-License-Identifier: BSD-2-Clause */ @@ -10,16 +11,37 @@ namespace Web::HTML { +// https://html.spec.whatwg.org/multipage/semantics.html#pragma-directives +#define ENUMERATE_HTML_META_HTTP_EQUIV_ATTRIBUTES \ + __ENUMERATE_HTML_META_HTTP_EQUIV_ATTRIBUTE("content-language", ContentLanguage) \ + __ENUMERATE_HTML_META_HTTP_EQUIV_ATTRIBUTE("content-type", EncodingDeclaration) \ + __ENUMERATE_HTML_META_HTTP_EQUIV_ATTRIBUTE("default-style", DefaultStyle) \ + __ENUMERATE_HTML_META_HTTP_EQUIV_ATTRIBUTE(refresh, Refresh) \ + __ENUMERATE_HTML_META_HTTP_EQUIV_ATTRIBUTE("set-cookie", SetCookie) \ + __ENUMERATE_HTML_META_HTTP_EQUIV_ATTRIBUTE("x-ua-compatible", XUACompatible) \ + __ENUMERATE_HTML_META_HTTP_EQUIV_ATTRIBUTE("content-security-policy", ContentSecurityPolicy) + class HTMLMetaElement final : public HTMLElement { WEB_PLATFORM_OBJECT(HTMLMetaElement, HTMLElement); public: virtual ~HTMLMetaElement() override; + enum class HttpEquivAttributeState { +#define __ENUMERATE_HTML_META_HTTP_EQUIV_ATTRIBUTE(_, state) state, + ENUMERATE_HTML_META_HTTP_EQUIV_ATTRIBUTES +#undef __ENUMERATE_HTML_META_HTTP_EQUIV_ATTRIBUTE + }; + + Optional http_equiv_state() const; + private: HTMLMetaElement(DOM::Document&, DOM::QualifiedName); virtual JS::ThrowCompletionOr initialize(JS::Realm&) override; + + // ^DOM::Element + virtual void inserted() override; }; }