mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 16:12:44 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			325 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			325 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;
 | |
|     }
 | |
| 
 | |
|     if (is_scripting_disabled()) {
 | |
|         dbgln("HTMLScriptElement: Refusing to run script because scripting is disabled.");
 | |
|         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();
 | |
| }
 | |
| 
 | |
| }
 | 
