mirror of
				https://github.com/LadybirdBrowser/ladybird.git
				synced 2025-11-03 23:00:58 +00:00 
			
		
		
		
	Just the basename is not enough in most cases, as it's usually not immediately obvious where scripts are loaded from.
		
			
				
	
	
		
			322 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			322 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
 | 
						|
 *
 | 
						|
 * SPDX-License-Identifier: BSD-2-Clause
 | 
						|
 */
 | 
						|
 | 
						|
#include <AK/Debug.h>
 | 
						|
#include <AK/StringBuilder.h>
 | 
						|
#include <LibJS/Parser.h>
 | 
						|
#include <LibTextCodec/Decoder.h>
 | 
						|
#include <LibWeb/DOM/Document.h>
 | 
						|
#include <LibWeb/DOM/Event.h>
 | 
						|
#include <LibWeb/DOM/ShadowRoot.h>
 | 
						|
#include <LibWeb/DOM/Text.h>
 | 
						|
#include <LibWeb/HTML/EventNames.h>
 | 
						|
#include <LibWeb/HTML/HTMLScriptElement.h>
 | 
						|
#include <LibWeb/Loader/ResourceLoader.h>
 | 
						|
 | 
						|
namespace Web::HTML {
 | 
						|
 | 
						|
HTMLScriptElement::HTMLScriptElement(DOM::Document& document, QualifiedName qualified_name)
 | 
						|
    : HTMLElement(document, move(qualified_name))
 | 
						|
    , m_script_filename("(document)")
 | 
						|
{
 | 
						|
}
 | 
						|
 | 
						|
HTMLScriptElement::~HTMLScriptElement()
 | 
						|
{
 | 
						|
}
 | 
						|
 | 
						|
void HTMLScriptElement::set_parser_document(Badge<HTMLDocumentParser>, DOM::Document& document)
 | 
						|
{
 | 
						|
    m_parser_document = document;
 | 
						|
}
 | 
						|
 | 
						|
void HTMLScriptElement::set_non_blocking(Badge<HTMLDocumentParser>, bool non_blocking)
 | 
						|
{
 | 
						|
    m_non_blocking = non_blocking;
 | 
						|
}
 | 
						|
 | 
						|
void HTMLScriptElement::execute_script()
 | 
						|
{
 | 
						|
    if (m_preparation_time_document.ptr() != &document()) {
 | 
						|
        dbgln("HTMLScriptElement: Refusing to run script because the preparation time document is not the same as the node document.");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (m_script_source.is_null()) {
 | 
						|
        dbgln("HTMLScriptElement: Refusing to run script because the script source is null.");
 | 
						|
        dispatch_event(DOM::Event::create(HTML::EventNames::error));
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    bool incremented_destructive_writes_counter = false;
 | 
						|
 | 
						|
    if (m_from_an_external_file || m_script_type == ScriptType::Module) {
 | 
						|
        document().increment_ignore_destructive_writes_counter();
 | 
						|
        incremented_destructive_writes_counter = true;
 | 
						|
    }
 | 
						|
 | 
						|
    if (m_script_type == ScriptType::Classic) {
 | 
						|
        auto old_current_script = document().current_script();
 | 
						|
        if (!is<DOM::ShadowRoot>(root()))
 | 
						|
            document().set_current_script({}, this);
 | 
						|
        else
 | 
						|
            document().set_current_script({}, nullptr);
 | 
						|
 | 
						|
        if (m_from_an_external_file)
 | 
						|
            dbgln_if(HTML_SCRIPT_DEBUG, "HTMLScriptElement: Running script {}", attribute(HTML::AttributeNames::src));
 | 
						|
        else
 | 
						|
            dbgln_if(HTML_SCRIPT_DEBUG, "HTMLScriptElement: Running inline script");
 | 
						|
 | 
						|
        document().run_javascript(m_script_source, m_script_filename);
 | 
						|
 | 
						|
        document().set_current_script({}, old_current_script);
 | 
						|
    } else {
 | 
						|
        VERIFY(!document().current_script());
 | 
						|
        TODO();
 | 
						|
    }
 | 
						|
 | 
						|
    if (incremented_destructive_writes_counter)
 | 
						|
        document().decrement_ignore_destructive_writes_counter();
 | 
						|
 | 
						|
    if (m_from_an_external_file)
 | 
						|
        dispatch_event(DOM::Event::create(HTML::EventNames::load));
 | 
						|
}
 | 
						|
 | 
						|
// https://mimesniff.spec.whatwg.org/#javascript-mime-type-essence-match
 | 
						|
static bool is_javascript_mime_type_essence_match(const String& string)
 | 
						|
{
 | 
						|
    auto lowercase_string = string.to_lowercase();
 | 
						|
    return lowercase_string.is_one_of("application/ecmascript", "application/javascript", "application/x-ecmascript", "application/x-javascript", "text/ecmascript", "text/javascript", "text/javascript1.0", "text/javascript1.1", "text/javascript1.2", "text/javascript1.3", "text/javascript1.4", "text/javascript1.5", "text/jscript", "text/livescript", "text/x-ecmascript", "text/x-javascript");
 | 
						|
}
 | 
						|
 | 
						|
// https://html.spec.whatwg.org/multipage/scripting.html#prepare-a-script
 | 
						|
void HTMLScriptElement::prepare_script()
 | 
						|
{
 | 
						|
    if (m_already_started) {
 | 
						|
        dbgln("HTMLScriptElement: Refusing to run script because it has already started.");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    RefPtr<DOM::Document> parser_document = m_parser_document.ptr();
 | 
						|
    m_parser_document = nullptr;
 | 
						|
 | 
						|
    if (parser_document && !has_attribute(HTML::AttributeNames::async)) {
 | 
						|
        m_non_blocking = true;
 | 
						|
    }
 | 
						|
 | 
						|
    auto source_text = child_text_content();
 | 
						|
    if (!has_attribute(HTML::AttributeNames::src) && source_text.is_empty()) {
 | 
						|
        dbgln("HTMLScriptElement: Refusing to run empty script.");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (!is_connected()) {
 | 
						|
        dbgln("HTMLScriptElement: Refusing to run script because the element is not connected.");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    String script_block_type;
 | 
						|
    bool has_type = has_attribute(HTML::AttributeNames::type);
 | 
						|
    bool has_language = has_attribute(HTML::AttributeNames::language);
 | 
						|
    if ((has_type && attribute(HTML::AttributeNames::type).is_empty())
 | 
						|
        || (!has_type && has_language && attribute(HTML::AttributeNames::language).is_empty())
 | 
						|
        || (!has_type && !has_language)) {
 | 
						|
        script_block_type = "text/javascript";
 | 
						|
    } else if (has_type) {
 | 
						|
        script_block_type = attribute(HTML::AttributeNames::type).trim_whitespace();
 | 
						|
    } else if (!attribute(HTML::AttributeNames::language).is_empty()) {
 | 
						|
        script_block_type = String::formatted("text/{}", attribute(HTML::AttributeNames::language));
 | 
						|
    }
 | 
						|
 | 
						|
    if (is_javascript_mime_type_essence_match(script_block_type)) {
 | 
						|
        m_script_type = ScriptType::Classic;
 | 
						|
    } else if (script_block_type.equals_ignoring_case("module")) {
 | 
						|
        m_script_type = ScriptType::Module;
 | 
						|
    } else {
 | 
						|
        dbgln("HTMLScriptElement: Refusing to run script because the type '{}' is not recognized.", script_block_type);
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (parser_document) {
 | 
						|
        m_parser_document = *parser_document;
 | 
						|
        m_non_blocking = false;
 | 
						|
    }
 | 
						|
 | 
						|
    m_already_started = true;
 | 
						|
    m_preparation_time_document = document();
 | 
						|
 | 
						|
    if (parser_document && parser_document.ptr() != m_preparation_time_document.ptr()) {
 | 
						|
        dbgln("HTMLScriptElement: Refusing to run script because the parser document is not the same as the preparation time document.");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    // FIXME: Check if scripting is disabled, if so return
 | 
						|
 | 
						|
    if (m_script_type == ScriptType::Classic && has_attribute(HTML::AttributeNames::nomodule)) {
 | 
						|
        dbgln("HTMLScriptElement: Refusing to run classic script because it has the nomodule attribute.");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    // FIXME: Check CSP
 | 
						|
 | 
						|
    if (m_script_type == ScriptType::Classic && has_attribute(HTML::AttributeNames::event) && has_attribute(HTML::AttributeNames::for_)) {
 | 
						|
        auto for_ = attribute(HTML::AttributeNames::for_).trim_whitespace();
 | 
						|
        auto event = attribute(HTML::AttributeNames::event).trim_whitespace();
 | 
						|
 | 
						|
        if (!for_.equals_ignoring_case("window")) {
 | 
						|
            dbgln("HTMLScriptElement: Refusing to run classic script because the provided 'for' attribute is not equal to 'window'");
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        if (!event.equals_ignoring_case("onload") && !event.equals_ignoring_case("onload()")) {
 | 
						|
            dbgln("HTMLScriptElement: Refusing to run classic script because the provided 'event' attribute is not equal to 'onload' or 'onload()'");
 | 
						|
            return;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // FIXME: Check "charset" attribute
 | 
						|
    // FIXME: Check CORS
 | 
						|
    // FIXME: Module script credentials mode
 | 
						|
    // FIXME: Cryptographic nonce
 | 
						|
    // FIXME: Check "integrity" attribute
 | 
						|
    // FIXME: Check "referrerpolicy" attribute
 | 
						|
    // FIXME: Check fetch options
 | 
						|
 | 
						|
    if (has_attribute(HTML::AttributeNames::src)) {
 | 
						|
        auto src = attribute(HTML::AttributeNames::src);
 | 
						|
        if (src.is_empty()) {
 | 
						|
            dbgln("HTMLScriptElement: Refusing to run script because the src attribute is empty.");
 | 
						|
            // FIXME: Queue a task to do this.
 | 
						|
            dispatch_event(DOM::Event::create(HTML::EventNames::error));
 | 
						|
            return;
 | 
						|
        }
 | 
						|
        m_from_an_external_file = true;
 | 
						|
 | 
						|
        auto url = document().complete_url(src);
 | 
						|
        if (!url.is_valid()) {
 | 
						|
            dbgln("HTMLScriptElement: Refusing to run script because the src URL '{}' is invalid.", url);
 | 
						|
            // FIXME: Queue a task to do this.
 | 
						|
            dispatch_event(DOM::Event::create(HTML::EventNames::error));
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        if (m_script_type == ScriptType::Classic) {
 | 
						|
            auto request = LoadRequest::create_for_url_on_page(url, document().page());
 | 
						|
 | 
						|
            // FIXME: This load should be made asynchronous and the parser should spin an event loop etc.
 | 
						|
            m_script_filename = url.to_string();
 | 
						|
            ResourceLoader::the().load_sync(
 | 
						|
                request,
 | 
						|
                [this, url](auto data, auto&, auto) {
 | 
						|
                    if (data.is_null()) {
 | 
						|
                        dbgln("HTMLScriptElement: Failed to load {}", url);
 | 
						|
                        return;
 | 
						|
                    }
 | 
						|
                    m_script_source = String::copy(data);
 | 
						|
                    script_became_ready();
 | 
						|
                },
 | 
						|
                [this](auto&, auto) {
 | 
						|
                    m_failed_to_load = true;
 | 
						|
                });
 | 
						|
        } else {
 | 
						|
            TODO();
 | 
						|
        }
 | 
						|
    } else {
 | 
						|
        if (m_script_type == ScriptType::Classic) {
 | 
						|
            m_script_source = source_text;
 | 
						|
            script_became_ready();
 | 
						|
        } else {
 | 
						|
            TODO();
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    if ((m_script_type == ScriptType::Classic && has_attribute(HTML::AttributeNames::src) && has_attribute(HTML::AttributeNames::defer) && is_parser_inserted() && !has_attribute(HTML::AttributeNames::async))
 | 
						|
        || (m_script_type == ScriptType::Module && is_parser_inserted() && !has_attribute(HTML::AttributeNames::async))) {
 | 
						|
        document().add_script_to_execute_when_parsing_has_finished({}, *this);
 | 
						|
        when_the_script_is_ready([this] {
 | 
						|
            m_ready_to_be_parser_executed = true;
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    else if (m_script_type == ScriptType::Classic && has_attribute(HTML::AttributeNames::src) && is_parser_inserted() && !has_attribute(HTML::AttributeNames::async)) {
 | 
						|
        document().set_pending_parsing_blocking_script({}, this);
 | 
						|
        when_the_script_is_ready([this] {
 | 
						|
            m_ready_to_be_parser_executed = true;
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    else if ((m_script_type == ScriptType::Classic && has_attribute(HTML::AttributeNames::src) && !has_attribute(HTML::AttributeNames::async) && !m_non_blocking)
 | 
						|
        || (m_script_type == ScriptType::Module && !has_attribute(HTML::AttributeNames::async) && !m_non_blocking)) {
 | 
						|
        m_preparation_time_document->add_script_to_execute_as_soon_as_possible({}, *this);
 | 
						|
 | 
						|
        // FIXME: When the script is ready, run the following steps:
 | 
						|
        //
 | 
						|
        // If the element is not now the first element in the list of scripts
 | 
						|
        // that will execute in order as soon as possible to which it was added above,
 | 
						|
        // then mark the element as ready but return without executing the script yet.
 | 
						|
        //
 | 
						|
        // Execution: Execute the script block corresponding to the first script element
 | 
						|
        // in this list of scripts that will execute in order as soon as possible.
 | 
						|
        //
 | 
						|
        // Remove the first element from this list of scripts that will execute in order
 | 
						|
        // as soon as possible.
 | 
						|
        //
 | 
						|
        // If this list of scripts that will execute in order as soon as possible is still
 | 
						|
        // not empty and the first entry has already been marked as ready, then jump back
 | 
						|
        // to the step labeled execution.
 | 
						|
    }
 | 
						|
 | 
						|
    else if ((m_script_type == ScriptType::Classic && has_attribute(HTML::AttributeNames::src)) || m_script_type == ScriptType::Module) {
 | 
						|
        // FIXME: This should add to a set, not a list.
 | 
						|
        m_preparation_time_document->add_script_to_execute_as_soon_as_possible({}, *this);
 | 
						|
        // FIXME: When the script is ready, execute the script block and then remove the element
 | 
						|
        //        from the set of scripts that will execute as soon as possible.
 | 
						|
    }
 | 
						|
 | 
						|
    // FIXME: If the element does not have a src attribute, and the element is "parser-inserted",
 | 
						|
    //        and either the parser that created the script is an XML parser or it's an HTML parser
 | 
						|
    //        whose script nesting level is not greater than one, and the element's parser document
 | 
						|
    //        has a style sheet that is blocking scripts:
 | 
						|
    //        The element is the pending parsing-blocking script of its parser document.
 | 
						|
    //        (There can only be one such script per Document at a time.)
 | 
						|
    //        Set the element's "ready to be parser-executed" flag. The parser will handle executing the script.
 | 
						|
 | 
						|
    else {
 | 
						|
        // Immediately execute the script block, even if other scripts are already executing.
 | 
						|
        execute_script();
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
void HTMLScriptElement::script_became_ready()
 | 
						|
{
 | 
						|
    m_script_ready = true;
 | 
						|
    if (!m_script_ready_callback)
 | 
						|
        return;
 | 
						|
    m_script_ready_callback();
 | 
						|
    m_script_ready_callback = nullptr;
 | 
						|
}
 | 
						|
 | 
						|
void HTMLScriptElement::when_the_script_is_ready(Function<void()> callback)
 | 
						|
{
 | 
						|
    if (m_script_ready) {
 | 
						|
        callback();
 | 
						|
        return;
 | 
						|
    }
 | 
						|
    m_script_ready_callback = move(callback);
 | 
						|
}
 | 
						|
 | 
						|
void HTMLScriptElement::inserted()
 | 
						|
{
 | 
						|
    if (!is_parser_inserted()) {
 | 
						|
        // FIXME: Only do this if the element was previously not connected.
 | 
						|
        if (is_connected()) {
 | 
						|
            prepare_script();
 | 
						|
        }
 | 
						|
    }
 | 
						|
    HTMLElement::inserted();
 | 
						|
}
 | 
						|
 | 
						|
}
 |