Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Released on Friday, November 6 2026.
- Added classes with implicit conversions standing for a WebIDL union, e.g. of a string and a sequence of strings, which fixes such conversions failing; an implicit conversion from such a class is what a script receives for it
- Added the same-origin policy: script gets `null` for a node of another origin, and for its window a `CrossOriginWindow` with only `window`, `self`, `frames`, `parent`, `top`, `closed`, `length`, `location` (only to be set, and only for frames inside the own document), `postMessage`, `focus`, `blur` and `close`
- Added `performance.mark`, `measure`, `getEntries`, `getEntriesByName`, `getEntriesByType`, `clearMarks`, `clearMeasures`, along with `PerformanceEntry`, `PerformanceMark` and `PerformanceMeasure`
- Fixed modules being read from the local disk; every module is loaded through the configured requesters (#153) @michaelthyregod

# 1.1.4

Expand Down
45 changes: 45 additions & 0 deletions src/AngleSharp.Js.Tests/EcmaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace AngleSharp.Js.Tests
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

[TestFixture]
Expand Down Expand Up @@ -493,5 +494,49 @@ public async Task MicrotasksRunAfterSynchronousWorkInOrder()
var document = await context.OpenAsync(r => r.Content(html)).WhenStable().ConfigureAwait(false);
Assert.AreEqual("sync,m1,m2", document.GetElementById("test")?.TextContent);
}

[TestCase("direct")]
[TestCase("importmap")]
[TestCase("dynamic")]
[TestCase("script")]
[TestCase("bytes")]
public async Task ModuleIsNotReadFromTheLocalDisk(String way)
{
var json = Path.GetTempFileName();
var script = Path.GetTempFileName();

try
{
File.WriteAllText(json, "{ \"secret\": \"from disk\" }");
File.WriteAllText(script, "document.getElementById('test').textContent = 'from disk';");
var jsonUrl = new Uri(json).AbsoluteUri;
var scriptUrl = new Uri(script).AbsoluteUri;
var show = "document.getElementById('test').textContent = ";
var page = way switch
{
"direct" => $"<script type=module>import data from '{jsonUrl}' with {{ type: 'json' }}; {show}data.secret;</script>",
"importmap" => $"<script type=importmap>{{ \"imports\": {{ \"secret\": \"{jsonUrl}\" }} }}</script><script type=module>import data from 'secret' with {{ type: 'json' }}; {show}data.secret;</script>",
"dynamic" => $"<script>import('{jsonUrl}', {{ with: {{ type: 'json' }} }}).then(m => {{ {show}m.default.secret; }});</script>",
"script" => $"<script type=module>import '{scriptUrl}';</script>",
_ => $"<script type=module>import data from '{jsonUrl}' with {{ type: 'bytes' }}; {show}String(data.length);</script>",
};
var config = Configuration.Default
.WithJs()
.WithEventLoop()
.With(new MockHttpClientRequester(new Dictionary<String, String>()))
.WithDefaultLoader(new LoaderOptions { IsResourceLoadingEnabled = true });
var html = "<!doctype html><div id=test>pending</div>" + page;
var document = await BrowsingContext.New(config)
.OpenAsync(r => r.Content(html).Address("https://example.com/"))
.WhenStable()
.ConfigureAwait(false);
Assert.AreEqual("pending", document.GetElementById("test").TextContent);
}
finally
{
File.Delete(json);
File.Delete(script);
}
}
}
}
53 changes: 42 additions & 11 deletions src/AngleSharp.Js/JsModuleLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,27 @@ public override ResolvedSpecifier Resolve(string referencingModuleLocation, Modu
return base.Resolve(referencingModuleLocation, moduleRequest);
}

// Every module comes through AngleSharp's resource loader, whatever its scheme, so the
// requesters the host registered decide what a page can reach. The base implementations
// of both methods would read file: URLs straight from the local disk, for any page.
protected override string LoadModuleContents(Engine engine, ResolvedSpecifier resolved)
{
if (resolved.Uri?.Scheme.IsOneOf(ProtocolNames.Http, ProtocolNames.Https) == true)
{
return FetchModule(resolved.Uri);
}
else
return FetchModule(GetModuleUrl(resolved));
}

protected override Byte[] LoadModuleContentsAsBytes(Engine engine, ResolvedSpecifier resolved)
{
return FetchModuleBytes(GetModuleUrl(resolved));
}

private static Uri GetModuleUrl(ResolvedSpecifier resolved)
Comment thread
FlorianRappl marked this conversation as resolved.
Resolved
{
if (resolved.Uri == null)
{
return base.LoadModuleContents(engine, resolved);
throw new NotSupportedException($"The module '{resolved.ModuleRequest.Specifier}' could not be resolved to a URL.");
}

return resolved.Uri;
}

private string FetchModule(Uri moduleUrl)
Expand All @@ -99,11 +110,7 @@ private string FetchModule(Uri moduleUrl)
return string.Empty;
}

var importUrl = Url.Convert(moduleUrl);

var request = new ResourceRequest(_scriptElement, importUrl);

var response = _resourceLoader.FetchAsync(request).Task.Result;
var response = Fetch(moduleUrl);

string content;

Expand All @@ -114,5 +121,29 @@ private string FetchModule(Uri moduleUrl)

return content;
}

private Byte[] FetchModuleBytes(Uri moduleUrl)
{
if (_resourceLoader == null)
{
return Array.Empty<Byte>();
}

var response = Fetch(moduleUrl);

using (var content = new MemoryStream())
{
response.Content.CopyTo(content);
return content.ToArray();
}
}

private IResponse Fetch(Uri moduleUrl)
{
var importUrl = Url.Convert(moduleUrl);
var request = new ResourceRequest(_scriptElement, importUrl);
var response = _resourceLoader.FetchAsync(request).Task.Result;
return response ?? throw new InvalidOperationException($"No requester could load the module '{importUrl.Href}'.");
}
}
}
Loading