servo/components/script/dom/htmlscriptelement.rs
Martin Robinson 036e74524a
net: Start reducing number of IPCs channels used for fetch with a FetchThread (#33863)
Instead of creating a `ROUTER` for each fetch, create a fetch thread
which handles all incoming and outcoming fetch requests. Now messages
involving fetches carry a "request id" which indicates which fetch is
being addressed by the message. This greatly reduces the number of file
descriptors used by fetch.

In addition, the interface for kicking off fetches is simplified when
using the `Listener` with `Document`s and the `GlobalScope`.

This does not fix all leaked file descriptors / mach ports, but greatly
eliminates the number used. Now tests can be run without limiting
procesess on modern macOS systems.

Followup work:

1. There are more instances where fetch is done using the old method.
   Some of these require more changes in order to be converted to the
   `FetchThread` approach.
2. Eliminate usage of IPC channels when doing redirects.
3. Also eliminate the IPC channel used for cancel handling.
4. This change opens up the possiblity of controlling the priority of
   fetch requests.

Fixes #29834.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
2024-10-16 16:53:24 +00:00

1377 lines
46 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
#![allow(unused_imports)]
use core::ffi::c_void;
use std::cell::Cell;
use std::fs::{create_dir_all, read_to_string, File};
use std::io::{Read, Seek, Write};
use std::path::PathBuf;
use std::process::Command;
use std::ptr;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use base::id::PipelineId;
use content_security_policy as csp;
use dom_struct::dom_struct;
use encoding_rs::Encoding;
use html5ever::{local_name, namespace_url, ns, LocalName, Prefix};
use ipc_channel::ipc;
use js::jsval::UndefinedValue;
use js::rust::{transform_str_to_source_text, CompileOptionsWrapper, HandleObject, Stencil};
use net_traits::http_status::HttpStatus;
use net_traits::request::{
CorsSettings, CredentialsMode, Destination, ParserMetadata, RequestBuilder, RequestId,
};
use net_traits::{
FetchMetadata, FetchResponseListener, Metadata, NetworkError, ResourceFetchTiming,
ResourceTimingType,
};
use servo_atoms::Atom;
use servo_config::pref;
use servo_url::{ImmutableOrigin, ServoUrl};
use style::str::{StaticStringVec, HTML_SPACE_CHARACTERS};
use uuid::Uuid;
use crate::document_loader::LoadType;
use crate::dom::attr::Attr;
use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
use crate::dom::bindings::codegen::Bindings::HTMLScriptElementBinding::HTMLScriptElementMethods;
use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::refcounted::Trusted;
use crate::dom::bindings::reflector::DomObject;
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::bindings::settings_stack::AutoEntryScript;
use crate::dom::bindings::str::{DOMString, USVString};
use crate::dom::bindings::trace::NoTrace;
use crate::dom::document::Document;
use crate::dom::element::{
cors_setting_for_element, referrer_policy_for_element, reflect_cross_origin_attribute,
reflect_referrer_policy_attribute, set_cross_origin_attribute, AttributeMutation, Element,
ElementCreator,
};
use crate::dom::event::{Event, EventBubbles, EventCancelable, EventStatus};
use crate::dom::globalscope::GlobalScope;
use crate::dom::htmlelement::HTMLElement;
use crate::dom::node::{
document_from_node, window_from_node, BindContext, ChildrenMutation, CloneChildrenFlag, Node,
};
use crate::dom::performanceresourcetiming::InitiatorType;
use crate::dom::virtualmethods::VirtualMethods;
use crate::fetch::create_a_potential_cors_request;
use crate::network_listener::{self, NetworkListener, PreInvoke, ResourceTimingListener};
use crate::realms::enter_realm;
use crate::script_module::{
fetch_external_module_script, fetch_inline_module_script, ModuleOwner, ScriptFetchOptions,
};
use crate::script_runtime::CanGc;
use crate::task::TaskCanceller;
use crate::task_source::dom_manipulation::DOMManipulationTaskSource;
use crate::task_source::{TaskSource, TaskSourceName};
// TODO Implement offthread compilation in mozjs
/*pub struct OffThreadCompilationContext {
script_element: Trusted<HTMLScriptElement>,
script_kind: ExternalScriptKind,
final_url: ServoUrl,
url: ServoUrl,
task_source: DOMManipulationTaskSource,
canceller: TaskCanceller,
script_text: String,
fetch_options: ScriptFetchOptions,
}
#[allow(unsafe_code)]
unsafe extern "C" fn off_thread_compilation_callback(
token: *mut OffThreadToken,
callback_data: *mut c_void,
) {
let mut context = Box::from_raw(callback_data as *mut OffThreadCompilationContext);
let token = OffThreadCompilationToken(token);
let url = context.url.clone();
let final_url = context.final_url.clone();
let script_element = context.script_element.clone();
let script_kind = context.script_kind;
let script = std::mem::take(&mut context.script_text);
let fetch_options = context.fetch_options.clone();
// Continue with <https://html.spec.whatwg.org/multipage/#fetch-a-classic-script>
let _ = context.task_source.queue_with_canceller(
task!(off_thread_compile_continue: move || {
let elem = script_element.root();
let global = elem.global();
let cx = GlobalScope::get_cx();
let _ar = enter_realm(&*global);
// TODO: This is necessary because the rust compiler will otherwise try to move the *mut
// OffThreadToken directly, which isn't marked as Send. The correct fix is that this
// type is marked as Send in mozjs.
let used_token = token;
let compiled_script = FinishOffThreadStencil(*cx, used_token.0, ptr::null_mut());
let load = if compiled_script.is_null() {
Err(NoTrace(NetworkError::Internal(
"Off-thread compilation failed.".into(),
)))
} else {
let script_text = DOMString::from(script);
let code = SourceCode::Compiled(CompiledSourceCode {
source_code: compiled_script,
original_text: Rc::new(script_text),
});
Ok(ScriptOrigin {
code,
url: final_url,
external: true,
fetch_options,
type_: ScriptType::Classic,
})
};
finish_fetching_a_classic_script(&elem, script_kind, url, load);
}),
&context.canceller,
);
}*/
/// An unique id for script element.
#[derive(Clone, Copy, Debug, Eq, Hash, JSTraceable, PartialEq)]
pub struct ScriptId(#[no_trace] Uuid);
#[dom_struct]
pub struct HTMLScriptElement {
htmlelement: HTMLElement,
/// <https://html.spec.whatwg.org/multipage/#already-started>
already_started: Cell<bool>,
/// <https://html.spec.whatwg.org/multipage/#parser-inserted>
parser_inserted: Cell<bool>,
/// <https://html.spec.whatwg.org/multipage/#non-blocking>
///
/// (currently unused)
non_blocking: Cell<bool>,
/// Document of the parser that created this element
parser_document: Dom<Document>,
/// Track line line_number
line_number: u64,
/// Unique id for each script element
#[ignore_malloc_size_of = "Defined in uuid"]
id: ScriptId,
}
impl HTMLScriptElement {
fn new_inherited(
local_name: LocalName,
prefix: Option<Prefix>,
document: &Document,
creator: ElementCreator,
) -> HTMLScriptElement {
HTMLScriptElement {
id: ScriptId(Uuid::new_v4()),
htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
already_started: Cell::new(false),
parser_inserted: Cell::new(creator.is_parser_created()),
non_blocking: Cell::new(!creator.is_parser_created()),
parser_document: Dom::from_ref(document),
line_number: creator.return_line_number(),
}
}
#[allow(crown::unrooted_must_root)]
pub fn new(
local_name: LocalName,
prefix: Option<Prefix>,
document: &Document,
proto: Option<HandleObject>,
creator: ElementCreator,
) -> DomRoot<HTMLScriptElement> {
Node::reflect_node_with_proto(
Box::new(HTMLScriptElement::new_inherited(
local_name, prefix, document, creator,
)),
document,
proto,
)
}
pub fn get_script_id(&self) -> ScriptId {
self.id
}
}
/// Supported script types as defined by
/// <https://html.spec.whatwg.org/multipage/#javascript-mime-type>.
pub static SCRIPT_JS_MIMES: StaticStringVec = &[
"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",
];
#[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)]
pub enum ScriptType {
Classic,
Module,
}
#[derive(JSTraceable, MallocSizeOf)]
pub struct CompiledSourceCode {
#[ignore_malloc_size_of = "SM handles JS values"]
pub source_code: Stencil,
#[ignore_malloc_size_of = "Rc is hard"]
pub original_text: Rc<DOMString>,
}
#[derive(JSTraceable)]
pub enum SourceCode {
Text(Rc<DOMString>),
Compiled(CompiledSourceCode),
}
#[derive(JSTraceable, MallocSizeOf)]
pub struct ScriptOrigin {
#[ignore_malloc_size_of = "Rc is hard"]
code: SourceCode,
#[no_trace]
url: ServoUrl,
external: bool,
fetch_options: ScriptFetchOptions,
type_: ScriptType,
}
impl ScriptOrigin {
pub fn internal(
text: Rc<DOMString>,
url: ServoUrl,
fetch_options: ScriptFetchOptions,
type_: ScriptType,
) -> ScriptOrigin {
ScriptOrigin {
code: SourceCode::Text(text),
url,
external: false,
fetch_options,
type_,
}
}
pub fn external(
text: Rc<DOMString>,
url: ServoUrl,
fetch_options: ScriptFetchOptions,
type_: ScriptType,
) -> ScriptOrigin {
ScriptOrigin {
code: SourceCode::Text(text),
url,
external: true,
fetch_options,
type_,
}
}
pub fn text(&self) -> Rc<DOMString> {
match &self.code {
SourceCode::Text(text) => Rc::clone(text),
SourceCode::Compiled(compiled_script) => Rc::clone(&compiled_script.original_text),
}
}
}
/// Final steps of <https://html.spec.whatwg.org/multipage/#fetch-a-classic-script>
fn finish_fetching_a_classic_script(
elem: &HTMLScriptElement,
script_kind: ExternalScriptKind,
url: ServoUrl,
load: ScriptResult,
can_gc: CanGc,
) {
// Step 11, Asynchronously complete this algorithm with script,
// which refers to step 26.6 "When the chosen algorithm asynchronously completes",
// of https://html.spec.whatwg.org/multipage/#prepare-a-script
let document = document_from_node(elem);
match script_kind {
ExternalScriptKind::Asap => document.asap_script_loaded(elem, load),
ExternalScriptKind::AsapInOrder => document.asap_in_order_script_loaded(elem, load),
ExternalScriptKind::Deferred => document.deferred_script_loaded(elem, load),
ExternalScriptKind::ParsingBlocking => {
document.pending_parsing_blocking_script_loaded(elem, load, can_gc)
},
}
document.finish_load(LoadType::Script(url), CanGc::note());
}
pub type ScriptResult = Result<ScriptOrigin, NoTrace<NetworkError>>;
/// The context required for asynchronously loading an external script source.
struct ClassicContext {
/// The element that initiated the request.
elem: Trusted<HTMLScriptElement>,
/// The kind of external script.
kind: ExternalScriptKind,
/// The (fallback) character encoding argument to the "fetch a classic
/// script" algorithm.
character_encoding: &'static Encoding,
/// The response body received to date.
data: Vec<u8>,
/// The response metadata received to date.
metadata: Option<Metadata>,
/// The initial URL requested.
url: ServoUrl,
/// Indicates whether the request failed, and why
status: Result<(), NetworkError>,
/// The fetch options of the script
fetch_options: ScriptFetchOptions,
/// Timing object for this resource
resource_timing: ResourceFetchTiming,
}
impl FetchResponseListener for ClassicContext {
// TODO(KiChjang): Perhaps add custom steps to perform fetch here?
fn process_request_body(&mut self, _: RequestId) {}
// TODO(KiChjang): Perhaps add custom steps to perform fetch here?
fn process_request_eof(&mut self, _: RequestId) {}
fn process_response(&mut self, _: RequestId, metadata: Result<FetchMetadata, NetworkError>) {
self.metadata = metadata.ok().map(|meta| match meta {
FetchMetadata::Unfiltered(m) => m,
FetchMetadata::Filtered { unsafe_, .. } => unsafe_,
});
let status = self
.metadata
.as_ref()
.map(|m| m.status.clone())
.unwrap_or_else(HttpStatus::new_error);
self.status = {
if status.is_error() {
Err(NetworkError::Internal(
"No http status code received".to_owned(),
))
} else if status.is_success() {
Ok(())
} else {
Err(NetworkError::Internal(format!(
"HTTP error code {}",
status.code()
)))
}
};
}
fn process_response_chunk(&mut self, _: RequestId, mut chunk: Vec<u8>) {
if self.status.is_ok() {
self.data.append(&mut chunk);
}
}
/// <https://html.spec.whatwg.org/multipage/#fetch-a-classic-script>
/// step 4-9
#[allow(unsafe_code)]
fn process_response_eof(
&mut self,
_: RequestId,
response: Result<ResourceFetchTiming, NetworkError>,
) {
let (source_text, final_url) = match (response.as_ref(), self.status.as_ref()) {
(Err(err), _) | (_, Err(err)) => {
// Step 6, response is an error.
finish_fetching_a_classic_script(
&self.elem.root(),
self.kind,
self.url.clone(),
Err(NoTrace(err.clone())),
CanGc::note(),
);
return;
},
(Ok(_), Ok(_)) => {
let metadata = self.metadata.take().unwrap();
// Step 7.
let encoding = metadata
.charset
.and_then(|encoding| Encoding::for_label(encoding.as_bytes()))
.unwrap_or(self.character_encoding);
// Step 8.
let (source_text, _, _) = encoding.decode(&self.data);
(source_text, metadata.final_url)
},
};
let elem = self.elem.root();
let global = elem.global();
//let cx = GlobalScope::get_cx();
let _ar = enter_realm(&*global);
/*
let options = unsafe { CompileOptionsWrapper::new(*cx, final_url.as_str(), 1) };
let can_compile_off_thread = pref!(dom.script.asynch) &&
unsafe { CanCompileOffThread(*cx, options.ptr as *const _, source_text.len()) };
if can_compile_off_thread {
let source_string = source_text.to_string();
let context = Box::new(OffThreadCompilationContext {
script_element: self.elem.clone(),
script_kind: self.kind,
final_url,
url: self.url.clone(),
task_source: global.dom_manipulation_task_source(),
canceller: global.task_canceller(TaskSourceName::DOMManipulation),
script_text: source_string,
fetch_options: self.fetch_options.clone(),
});
unsafe {
assert!(!CompileToStencilOffThread1(
*cx,
options.ptr as *const _,
&mut transform_str_to_source_text(&context.script_text) as *mut _,
Some(off_thread_compilation_callback),
Box::into_raw(context) as *mut c_void,
)
.is_null());
}
} else {*/
let load = ScriptOrigin::external(
Rc::new(DOMString::from(source_text)),
final_url.clone(),
self.fetch_options.clone(),
ScriptType::Classic,
);
finish_fetching_a_classic_script(
&elem,
self.kind,
self.url.clone(),
Ok(load),
CanGc::note(),
);
//}
}
fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming {
&mut self.resource_timing
}
fn resource_timing(&self) -> &ResourceFetchTiming {
&self.resource_timing
}
fn submit_resource_timing(&mut self) {
network_listener::submit_timing(self)
}
}
impl ResourceTimingListener for ClassicContext {
fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
let initiator_type = InitiatorType::LocalName(
self.elem
.root()
.upcast::<Element>()
.local_name()
.to_string(),
);
(initiator_type, self.url.clone())
}
fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
document_from_node(&*self.elem.root()).global()
}
}
impl PreInvoke for ClassicContext {}
/// Steps 1-2 of <https://html.spec.whatwg.org/multipage/#fetch-a-classic-script>
// This function is also used to prefetch a script in `script::dom::servoparser::prefetch`.
pub(crate) fn script_fetch_request(
url: ServoUrl,
cors_setting: Option<CorsSettings>,
origin: ImmutableOrigin,
pipeline_id: PipelineId,
options: ScriptFetchOptions,
) -> RequestBuilder {
// We intentionally ignore options' credentials_mode member for classic scripts.
// The mode is initialized by create_a_potential_cors_request.
create_a_potential_cors_request(
url,
Destination::Script,
cors_setting,
None,
options.referrer,
)
.origin(origin)
.pipeline_id(Some(pipeline_id))
.parser_metadata(options.parser_metadata)
.integrity_metadata(options.integrity_metadata.clone())
.referrer_policy(options.referrer_policy)
}
/// <https://html.spec.whatwg.org/multipage/#fetch-a-classic-script>
fn fetch_a_classic_script(
script: &HTMLScriptElement,
kind: ExternalScriptKind,
url: ServoUrl,
cors_setting: Option<CorsSettings>,
options: ScriptFetchOptions,
character_encoding: &'static Encoding,
) {
// Step 1, 2.
let doc = document_from_node(script);
let request = script_fetch_request(
url.clone(),
cors_setting,
doc.origin().immutable().clone(),
script.global().pipeline_id(),
options.clone(),
);
let request = doc.prepare_request(request);
// TODO: Step 3, Add custom steps to perform fetch
let context = ClassicContext {
elem: Trusted::new(script),
kind,
character_encoding,
data: vec![],
metadata: None,
url: url.clone(),
status: Ok(()),
fetch_options: options,
resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource),
};
doc.fetch(LoadType::Script(url), request, context);
}
impl HTMLScriptElement {
/// <https://html.spec.whatwg.org/multipage/#prepare-a-script>
pub fn prepare(&self) {
// Step 1.
if self.already_started.get() {
return;
}
// Step 2.
let was_parser_inserted = self.parser_inserted.get();
self.parser_inserted.set(false);
// Step 4.
let element = self.upcast::<Element>();
let asynch = element.has_attribute(&local_name!("async"));
// Note: confusingly, this is done if the element does *not* have an "async" attribute.
if was_parser_inserted && !asynch {
self.non_blocking.set(true);
}
// Step 5-6.
let text = self.Text();
if text.is_empty() && !element.has_attribute(&local_name!("src")) {
return;
}
// Step 7.
if !self.upcast::<Node>().is_connected() {
return;
}
let script_type = if let Some(ty) = self.get_script_type() {
ty
} else {
// Step 7.
return;
};
// Step 8.
if was_parser_inserted {
self.parser_inserted.set(true);
self.non_blocking.set(false);
}
// Step 10.
self.already_started.set(true);
// Step 12.
let doc = document_from_node(self);
if self.parser_inserted.get() && *self.parser_document != *doc {
return;
}
// Step 13.
if !doc.is_scripting_enabled() {
return;
}
// Step 14
if element.has_attribute(&local_name!("nomodule")) && script_type == ScriptType::Classic {
return;
}
// Step 15.
if !element.has_attribute(&local_name!("src")) &&
doc.should_elements_inline_type_behavior_be_blocked(
element,
csp::InlineCheckType::Script,
&text,
) == csp::CheckResult::Blocked
{
warn!("Blocking inline script due to CSP");
return;
}
// Step 16.
if script_type == ScriptType::Classic {
let for_attribute = element.get_attribute(&ns!(), &local_name!("for"));
let event_attribute = element.get_attribute(&ns!(), &local_name!("event"));
if let (Some(ref for_attribute), Some(ref event_attribute)) =
(for_attribute, event_attribute)
{
let for_value = for_attribute.value().to_ascii_lowercase();
let for_value = for_value.trim_matches(HTML_SPACE_CHARACTERS);
if for_value != "window" {
return;
}
let event_value = event_attribute.value().to_ascii_lowercase();
let event_value = event_value.trim_matches(HTML_SPACE_CHARACTERS);
if event_value != "onload" && event_value != "onload()" {
return;
}
}
}
// Step 17.
let encoding = element
.get_attribute(&ns!(), &local_name!("charset"))
.and_then(|charset| Encoding::for_label(charset.value().as_bytes()))
.unwrap_or_else(|| doc.encoding());
// Step 18.
let cors_setting = cors_setting_for_element(element);
// Step 19.
let module_credentials_mode = match script_type {
ScriptType::Classic => CredentialsMode::CredentialsSameOrigin,
ScriptType::Module => reflect_cross_origin_attribute(element).map_or(
CredentialsMode::CredentialsSameOrigin,
|attr| match &*attr {
"use-credentials" => CredentialsMode::Include,
"anonymous" => CredentialsMode::CredentialsSameOrigin,
_ => CredentialsMode::CredentialsSameOrigin,
},
),
};
// TODO: Step 20: Nonce.
// Step 21: Integrity metadata.
let im_attribute = element.get_attribute(&ns!(), &local_name!("integrity"));
let integrity_val = im_attribute.as_ref().map(|a| a.value());
let integrity_metadata = match integrity_val {
Some(ref value) => &***value,
None => "",
};
// TODO: Step 22: referrer policy
// Step 23
let parser_metadata = if self.parser_inserted.get() {
ParserMetadata::ParserInserted
} else {
ParserMetadata::NotParserInserted
};
// Step 24.
let options = ScriptFetchOptions {
cryptographic_nonce: "".into(),
integrity_metadata: integrity_metadata.to_owned(),
parser_metadata,
referrer: self.global().get_referrer(),
referrer_policy: referrer_policy_for_element(self.upcast::<Element>()),
credentials_mode: module_credentials_mode,
};
// TODO: Step 23: environment settings object.
let base_url = doc.base_url();
if let Some(src) = element.get_attribute(&ns!(), &local_name!("src")) {
// Step 26.
// Step 26.1.
let src = src.value();
// Step 26.2.
if src.is_empty() {
self.queue_error_event();
return;
}
// Step 26.3: The "from an external file"" flag is stored in ScriptOrigin.
// Step 26.4-26.5.
let url = match base_url.join(&src) {
Ok(url) => url,
Err(_) => {
warn!("error parsing URL for script {}", &**src);
self.queue_error_event();
return;
},
};
// Step 26.6.
match script_type {
ScriptType::Classic => {
// Preparation for step 26.
let kind = if element.has_attribute(&local_name!("defer")) &&
was_parser_inserted &&
!asynch
{
// Step 26.a: classic, has src, has defer, was parser-inserted, is not async.
ExternalScriptKind::Deferred
} else if was_parser_inserted && !asynch {
// Step 26.c: classic, has src, was parser-inserted, is not async.
ExternalScriptKind::ParsingBlocking
} else if !asynch && !self.non_blocking.get() {
// Step 26.d: classic, has src, is not async, is not non-blocking.
ExternalScriptKind::AsapInOrder
} else {
// Step 26.f: classic, has src.
ExternalScriptKind::Asap
};
// Step 24.6.
fetch_a_classic_script(self, kind, url, cors_setting, options, encoding);
// Step 23.
match kind {
ExternalScriptKind::Deferred => doc.add_deferred_script(self),
ExternalScriptKind::ParsingBlocking => {
doc.set_pending_parsing_blocking_script(self, None)
},
ExternalScriptKind::AsapInOrder => doc.push_asap_in_order_script(self),
ExternalScriptKind::Asap => doc.add_asap_script(self),
}
},
ScriptType::Module => {
fetch_external_module_script(
ModuleOwner::Window(Trusted::new(self)),
url.clone(),
Destination::Script,
options,
);
if !asynch && was_parser_inserted {
doc.add_deferred_script(self);
} else if !asynch && !self.non_blocking.get() {
doc.push_asap_in_order_script(self);
} else {
doc.add_asap_script(self);
};
},
}
} else {
// Step 27.
assert!(!text.is_empty());
let text_rc = Rc::new(text);
// Step 27-1. & 27-2.
let result = Ok(ScriptOrigin::internal(
Rc::clone(&text_rc),
base_url.clone(),
options.clone(),
script_type,
));
// Step 27-2.
match script_type {
ScriptType::Classic => {
if was_parser_inserted &&
doc.get_current_parser()
.is_some_and(|parser| parser.script_nesting_level() <= 1) &&
doc.get_script_blocking_stylesheets_count() > 0
{
// Step 27.h: classic, has no src, was parser-inserted, is blocked on stylesheet.
doc.set_pending_parsing_blocking_script(self, Some(result));
} else {
// Step 27.i: otherwise.
self.execute(result);
}
},
ScriptType::Module => {
// We should add inline module script elements
// into those vectors in case that there's no
// descendants in the inline module script.
if !asynch && was_parser_inserted {
doc.add_deferred_script(self);
} else if !asynch && !self.non_blocking.get() {
doc.push_asap_in_order_script(self);
} else {
doc.add_asap_script(self);
};
fetch_inline_module_script(
ModuleOwner::Window(Trusted::new(self)),
text_rc,
base_url.clone(),
self.id,
options,
);
},
}
}
}
fn unminify_js(&self, script: &mut ScriptOrigin) {
if !self.parser_document.window().unminify_js() {
return;
}
// Write the minified code to a temporary file and pass its path as an argument
// to js-beautify to read from. Meanwhile, redirect the process' stdout into
// another temporary file and read that into a string. This avoids some hangs
// observed on macOS when using direct input/output pipes with very large
// unminified content.
let (input, output) = (tempfile::NamedTempFile::new(), tempfile::tempfile());
if let (Ok(mut input), Ok(mut output)) = (input, output) {
match &script.code {
SourceCode::Text(text) => {
input.write_all(text.as_bytes()).unwrap();
},
SourceCode::Compiled(compiled_source_code) => {
input
.write_all(compiled_source_code.original_text.as_bytes())
.unwrap();
},
}
match Command::new("js-beautify")
.arg(input.path())
.stdout(output.try_clone().unwrap())
.status()
{
Ok(status) if status.success() => {
let mut script_content = String::new();
output.seek(std::io::SeekFrom::Start(0)).unwrap();
output.read_to_string(&mut script_content).unwrap();
script.code = SourceCode::Text(Rc::new(DOMString::from(script_content)));
},
_ => {
warn!("Failed to execute js-beautify. Will store unmodified script");
},
}
} else {
warn!("Error creating input and output files for unminify");
}
let path = match window_from_node(self).unminified_js_dir() {
Some(unminified_js_dir) => PathBuf::from(unminified_js_dir),
None => {
warn!("Unminified script directory not found");
return;
},
};
let (base, has_name) = match script.url.as_str().ends_with('/') {
true => (
path.join(&script.url[url::Position::BeforeHost..])
.as_path()
.to_owned(),
false,
),
false => (
path.join(&script.url[url::Position::BeforeHost..])
.parent()
.unwrap()
.to_owned(),
true,
),
};
match create_dir_all(base.clone()) {
Ok(()) => debug!("Created base dir: {:?}", base),
Err(e) => {
debug!("Failed to create base dir: {:?}, {:?}", base, e);
return;
},
}
let path = if script.external && has_name {
// External script.
path.join(&script.url[url::Position::BeforeHost..])
} else {
// Inline script or url ends with '/'
base.join(Uuid::new_v4().to_string())
};
debug!("script will be stored in {:?}", path);
match File::create(&path) {
Ok(mut file) => match &script.code {
SourceCode::Text(text) => file.write_all(text.as_bytes()).unwrap(),
SourceCode::Compiled(compiled_source_code) => {
file.write_all(compiled_source_code.original_text.as_bytes())
.unwrap();
},
},
Err(why) => warn!("Could not store script {:?}", why),
}
}
fn substitute_with_local_script(&self, script: &mut ScriptOrigin) {
if self
.parser_document
.window()
.local_script_source()
.is_none() ||
!script.external
{
return;
}
let mut path = PathBuf::from(
self.parser_document
.window()
.local_script_source()
.clone()
.unwrap(),
);
path = path.join(&script.url[url::Position::BeforeHost..]);
debug!("Attempting to read script stored at: {:?}", path);
match read_to_string(path.clone()) {
Ok(local_script) => {
debug!("Found script stored at: {:?}", path);
script.code = SourceCode::Text(Rc::new(DOMString::from(local_script)));
},
Err(why) => warn!("Could not restore script from file {:?}", why),
}
}
/// <https://html.spec.whatwg.org/multipage/#execute-the-script-block>
pub fn execute(&self, result: ScriptResult) {
// Step 1.
let doc = document_from_node(self);
if self.parser_inserted.get() && *doc != *self.parser_document {
return;
}
let mut script = match result {
// Step 2.
Err(e) => {
warn!("error loading script {:?}", e);
self.dispatch_error_event(CanGc::note());
return;
},
Ok(script) => script,
};
if script.type_ == ScriptType::Classic {
self.unminify_js(&mut script);
self.substitute_with_local_script(&mut script);
}
// Step 3.
let neutralized_doc = if script.external || script.type_ == ScriptType::Module {
debug!("loading external script, url = {}", script.url);
let doc = document_from_node(self);
doc.incr_ignore_destructive_writes_counter();
Some(doc)
} else {
None
};
// Step 4.
let document = document_from_node(self);
let old_script = document.GetCurrentScript();
match script.type_ {
ScriptType::Classic => document.set_current_script(Some(self)),
ScriptType::Module => document.set_current_script(None),
}
match script.type_ {
ScriptType::Classic => {
self.run_a_classic_script(&script);
document.set_current_script(old_script.as_deref());
},
ScriptType::Module => {
assert!(document.GetCurrentScript().is_none());
self.run_a_module_script(&script, false);
},
}
// Step 5.
if let Some(doc) = neutralized_doc {
doc.decr_ignore_destructive_writes_counter();
}
// Step 6.
if script.external {
self.dispatch_load_event(CanGc::note());
}
}
// https://html.spec.whatwg.org/multipage/#run-a-classic-script
pub fn run_a_classic_script(&self, script: &ScriptOrigin) {
// TODO use a settings object rather than this element's document/window
// Step 2
let document = document_from_node(self);
if !document.is_fully_active() || !document.is_scripting_enabled() {
return;
}
// Steps 4-10
let window = window_from_node(self);
let line_number = if script.external {
1
} else {
self.line_number as u32
};
rooted!(in(*GlobalScope::get_cx()) let mut rval = UndefinedValue());
let global = window.upcast::<GlobalScope>();
global.evaluate_script_on_global_with_result(
&script.code,
script.url.as_str(),
rval.handle_mut(),
line_number,
script.fetch_options.clone(),
script.url.clone(),
);
}
#[allow(unsafe_code)]
/// <https://html.spec.whatwg.org/multipage/#run-a-module-script>
pub fn run_a_module_script(&self, script: &ScriptOrigin, _rethrow_errors: bool) {
// TODO use a settings object rather than this element's document/window
// Step 2
let document = document_from_node(self);
if !document.is_fully_active() || !document.is_scripting_enabled() {
return;
}
// Step 4
let window = window_from_node(self);
let global = window.upcast::<GlobalScope>();
let _aes = AutoEntryScript::new(global);
let tree = if script.external {
global.get_module_map().borrow().get(&script.url).cloned()
} else {
global
.get_inline_module_map()
.borrow()
.get(&self.id.clone())
.cloned()
};
if let Some(module_tree) = tree {
// Step 6.
{
let module_error = module_tree.get_rethrow_error().borrow();
let network_error = module_tree.get_network_error().borrow();
if module_error.is_some() && network_error.is_none() {
module_tree.report_error(global);
return;
}
}
let record = module_tree
.get_record()
.borrow()
.as_ref()
.map(|record| record.handle());
if let Some(record) = record {
rooted!(in(*GlobalScope::get_cx()) let mut rval = UndefinedValue());
let evaluated =
module_tree.execute_module(global, record, rval.handle_mut().into());
if let Err(exception) = evaluated {
module_tree.set_rethrow_error(exception);
module_tree.report_error(global);
}
}
}
}
pub fn queue_error_event(&self) {
let window = window_from_node(self);
window
.task_manager()
.dom_manipulation_task_source()
.queue_simple_event(self.upcast(), atom!("error"), &window);
}
pub fn dispatch_load_event(&self, can_gc: CanGc) {
self.dispatch_event(
atom!("load"),
EventBubbles::DoesNotBubble,
EventCancelable::NotCancelable,
can_gc,
);
}
pub fn dispatch_error_event(&self, can_gc: CanGc) {
self.dispatch_event(
atom!("error"),
EventBubbles::DoesNotBubble,
EventCancelable::NotCancelable,
can_gc,
);
}
// https://html.spec.whatwg.org/multipage/#prepare-a-script Step 7.
pub fn get_script_type(&self) -> Option<ScriptType> {
let element = self.upcast::<Element>();
let type_attr = element.get_attribute(&ns!(), &local_name!("type"));
let language_attr = element.get_attribute(&ns!(), &local_name!("language"));
let script_type = match (
type_attr.as_ref().map(|t| t.value()),
language_attr.as_ref().map(|l| l.value()),
) {
(Some(ref ty), _) if ty.is_empty() => {
debug!("script type empty, inferring js");
Some(ScriptType::Classic)
},
(None, Some(ref lang)) if lang.is_empty() => {
debug!("script type empty, inferring js");
Some(ScriptType::Classic)
},
(None, None) => {
debug!("script type empty, inferring js");
Some(ScriptType::Classic)
},
(None, Some(ref lang)) => {
debug!("script language={}", &***lang);
let language = format!("text/{}", &***lang);
if SCRIPT_JS_MIMES.contains(&language.to_ascii_lowercase().as_str()) {
Some(ScriptType::Classic)
} else {
None
}
},
(Some(ref ty), _) => {
debug!("script type={}", &***ty);
if ty.to_ascii_lowercase().trim_matches(HTML_SPACE_CHARACTERS) == "module" {
return Some(ScriptType::Module);
}
if SCRIPT_JS_MIMES
.contains(&ty.to_ascii_lowercase().trim_matches(HTML_SPACE_CHARACTERS))
{
Some(ScriptType::Classic)
} else {
None
}
},
};
// https://github.com/rust-lang/rust/issues/21114
script_type
}
pub fn set_parser_inserted(&self, parser_inserted: bool) {
self.parser_inserted.set(parser_inserted);
}
pub fn get_parser_inserted(&self) -> bool {
self.parser_inserted.get()
}
pub fn set_already_started(&self, already_started: bool) {
self.already_started.set(already_started);
}
pub fn get_non_blocking(&self) -> bool {
self.non_blocking.get()
}
fn dispatch_event(
&self,
type_: Atom,
bubbles: EventBubbles,
cancelable: EventCancelable,
can_gc: CanGc,
) -> EventStatus {
let window = window_from_node(self);
let event = Event::new(window.upcast(), type_, bubbles, cancelable, can_gc);
event.fire(self.upcast())
}
}
impl VirtualMethods for HTMLScriptElement {
fn super_type(&self) -> Option<&dyn VirtualMethods> {
Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
}
fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) {
self.super_type().unwrap().attribute_mutated(attr, mutation);
if *attr.local_name() == local_name!("src") {
if let AttributeMutation::Set(_) = mutation {
if !self.parser_inserted.get() && self.upcast::<Node>().is_connected() {
self.prepare();
}
}
}
}
fn children_changed(&self, mutation: &ChildrenMutation) {
if let Some(s) = self.super_type() {
s.children_changed(mutation);
}
if !self.parser_inserted.get() && self.upcast::<Node>().is_connected() {
self.prepare();
}
}
fn bind_to_tree(&self, context: &BindContext) {
if let Some(s) = self.super_type() {
s.bind_to_tree(context);
}
if context.tree_connected && !self.parser_inserted.get() {
let script = Trusted::new(self);
document_from_node(self).add_delayed_task(task!(ScriptDelayedInitialize: move || {
script.root().prepare();
}));
}
}
fn cloning_steps(
&self,
copy: &Node,
maybe_doc: Option<&Document>,
clone_children: CloneChildrenFlag,
) {
if let Some(s) = self.super_type() {
s.cloning_steps(copy, maybe_doc, clone_children);
}
// https://html.spec.whatwg.org/multipage/#already-started
if self.already_started.get() {
copy.downcast::<HTMLScriptElement>()
.unwrap()
.set_already_started(true);
}
}
}
impl HTMLScriptElementMethods for HTMLScriptElement {
// https://html.spec.whatwg.org/multipage/#dom-script-src
make_url_getter!(Src, "src");
// https://html.spec.whatwg.org/multipage/#dom-script-src
make_url_setter!(SetSrc, "src");
// https://html.spec.whatwg.org/multipage/#dom-script-type
make_getter!(Type, "type");
// https://html.spec.whatwg.org/multipage/#dom-script-type
make_setter!(SetType, "type");
// https://html.spec.whatwg.org/multipage/#dom-script-charset
make_getter!(Charset, "charset");
// https://html.spec.whatwg.org/multipage/#dom-script-charset
make_setter!(SetCharset, "charset");
// https://html.spec.whatwg.org/multipage/#dom-script-async
fn Async(&self) -> bool {
self.non_blocking.get() ||
self.upcast::<Element>()
.has_attribute(&local_name!("async"))
}
// https://html.spec.whatwg.org/multipage/#dom-script-async
fn SetAsync(&self, value: bool) {
self.non_blocking.set(false);
self.upcast::<Element>()
.set_bool_attribute(&local_name!("async"), value);
}
// https://html.spec.whatwg.org/multipage/#dom-script-defer
make_bool_getter!(Defer, "defer");
// https://html.spec.whatwg.org/multipage/#dom-script-defer
make_bool_setter!(SetDefer, "defer");
// https://html.spec.whatwg.org/multipage/#dom-script-nomodule
make_bool_getter!(NoModule, "nomodule");
// https://html.spec.whatwg.org/multipage/#dom-script-nomodule
make_bool_setter!(SetNoModule, "nomodule");
// https://html.spec.whatwg.org/multipage/#dom-script-integrity
make_getter!(Integrity, "integrity");
// https://html.spec.whatwg.org/multipage/#dom-script-integrity
make_setter!(SetIntegrity, "integrity");
// https://html.spec.whatwg.org/multipage/#dom-script-event
make_getter!(Event, "event");
// https://html.spec.whatwg.org/multipage/#dom-script-event
make_setter!(SetEvent, "event");
// https://html.spec.whatwg.org/multipage/#dom-script-htmlfor
make_getter!(HtmlFor, "for");
// https://html.spec.whatwg.org/multipage/#dom-script-htmlfor
make_setter!(SetHtmlFor, "for");
// https://html.spec.whatwg.org/multipage/#dom-script-crossorigin
fn GetCrossOrigin(&self) -> Option<DOMString> {
reflect_cross_origin_attribute(self.upcast::<Element>())
}
// https://html.spec.whatwg.org/multipage/#dom-script-crossorigin
fn SetCrossOrigin(&self, value: Option<DOMString>) {
set_cross_origin_attribute(self.upcast::<Element>(), value);
}
// https://html.spec.whatwg.org/multipage/#dom-script-referrerpolicy
fn ReferrerPolicy(&self) -> DOMString {
reflect_referrer_policy_attribute(self.upcast::<Element>())
}
// https://html.spec.whatwg.org/multipage/#dom-script-referrerpolicy
make_setter!(SetReferrerPolicy, "referrerpolicy");
// https://html.spec.whatwg.org/multipage/#dom-script-text
fn Text(&self) -> DOMString {
self.upcast::<Node>().child_text_content()
}
// https://html.spec.whatwg.org/multipage/#dom-script-text
fn SetText(&self, value: DOMString) {
self.upcast::<Node>().SetTextContent(Some(value))
}
}
#[derive(Clone, Copy)]
enum ExternalScriptKind {
Deferred,
ParsingBlocking,
AsapInOrder,
Asap,
}