using AngleSharp.Dom;
namespace XFESystem.Browser;
public sealed partial class BrowserService
{
private readonly HashSet<IElement> executedScripts = new(ReferenceEqualityComparer.Instance);
private bool drainingResources;
private bool scriptBudgetExhausted;
private readonly Dictionary<IElement, string> styleAddresses = new(ReferenceEqualityComparer.Instance);
private int pageScriptSequence;
private BrowserDomHost CreateDomHost() => new()
{
Mutated = InvalidateDocument,
AttributeChanged = (_, name) => { if (layoutDirty || layoutEngine.AttributeAffectsLayout(name)) InvalidateDocument(); },
FocusChanged = _ => InvalidateDocument(),
SubmitForm = (form, submitter) => SubmitForm(form, submitter, fireEvent: false),
Navigate = reference => pendingNavigation = ResourceAddress(reference).AbsoluteUri,
ResourceRequested = element =>
{
if (scheduledResources.Add(element)) pendingResources.Enqueue(element);
},
Measure = node => EnsureLayout().GetBoundingClientRect(node),
MeasureRange = (start, startOffset, end, endOffset) => EnsureLayout().GetRangeRects(start,startOffset,end,endOffset),
HitTest = (x,y) => EnsureLayout().HitTest(x,y)?.Node,
ComputedStyle = (node, property) => EnsureLayout().GetComputedStyle(node)?.GetPropertyValue(property),
MeasureText = (text, font) => textMetrics.Measure(text, font),
RasterizeText = (text, font) => metricCanvas?.RasterizeWebText(text, font.Size, font.Family, font.Weight)
?? throw new NotSupportedException("The platform did not supply a real Canvas text rasterizer."),
ImageInfo = element =>
{
string source = element.GetAttribute("src") ?? "";
var image = FindImage(source);
bool complete = source.Length == 0;
if (!complete)
{
try { complete = images.ContainsKey(ImageKey(source)); }
catch (Exception error) when (error is UriFormatException or NotSupportedException) { complete = true; }
}
return (complete, image?.Width ?? 0, image?.Height ?? 0);
},
ReadCookies = () => transport is IWebCookieStore store ? store.ReadDocumentCookies(currentAddress!) : pageCookies.ReadDocumentCookies(currentAddress!),
WriteCookie = value => { if (transport is IWebCookieStore store) store.WriteDocumentCookie(currentAddress!, value); else pageCookies.WriteDocumentCookie(currentAddress!, value); },
GetResourceWithHeaders = (reference, headers) =>
{
if (scriptPolicy?.BlocksAllScripts == true) throw new InvalidOperationException("The declared connection policy cannot be enforced.");
Uri target = ResourceAddress(reference);
if (target.GetLeftPart(UriPartial.Authority) != currentAddress!.GetLeftPart(UriPartial.Authority))
throw new NotSupportedException("Cross-origin script requests require CORS support.");
var response = resourceLoader!.Get(target, "fetch", headers);
if (response.Address.GetLeftPart(UriPartial.Authority) != currentAddress.GetLeftPart(UriPartial.Authority))
throw new NotSupportedException("A script request redirected outside its origin.");
return response;
},
FillRandom = transport is IBrowserRandomSource random ? random.FillRandom : null,
Viewport = () => viewport
};
private Uri ResourceAddress(string reference)
{
Uri basis = currentAddress!;
string? declared = document?.QuerySelector("base[href]")?.GetAttribute("href");
if (!string.IsNullOrEmpty(declared) && Uri.TryCreate(basis, declared, out var candidate))
{ ValidateWebAddress(candidate); if (scriptPolicy?.AllowsBase(currentAddress!, candidate) != false) basis = candidate; }
var address = new Uri(basis, reference);
ValidateWebAddress(address); return address;
}
private bool RunScript(IElement script)
{
CheckOperationCancellation();
if (scriptBudgetExhausted) return false;
if (dom!.IsInertParsedScript(script)) return true;
if (scriptPolicy?.Allows(script, dom!.IsDynamicScript(script)) == false || !executedScripts.Add(script)) return true;
string type = (script.GetAttribute("type") ?? string.Empty).Trim().ToLowerInvariant();
if (type == "module") { Diagnostic("Module scripts are not supported by the classic-script interpreter."); return true; }
if (type is not ("" or "text/javascript" or "application/javascript" or "text/ecmascript" or "application/ecmascript")) return true;
try
{
bool external = script.HasAttribute("src");
string source = external
? resourceLoader!.Get(ResourceAddress(script.GetAttribute("src")!), "script").Body : script.TextContent;
CheckOperationCancellation();
Trace?.Invoke("XFE_BROWSER_SCRIPT_BEGIN|" + (script.GetAttribute("src") ?? "Inline") + "|Characters|" + source.Length + "\n");
CheckOperationCancellation();
long previousRevision = Revision;
long previousLayouts = LayoutPassCount, previousBudgetChecks = dom!.BudgetValidationCount;
dom!.CurrentScript = script;
executingPageScript = true;
long scriptStarted = System.Diagnostics.Stopwatch.GetTimestamp();
var result = external && resourceLoader!.LastResponsePackaged
? runtime!.EvaluatePackaged(source, ResourceAddress(script.GetAttribute("src")!).AbsoluteUri)
: runtime!.EvaluateNamed(source, external ? ResourceAddress(script.GetAttribute("src")!).AbsoluteUri : currentAddress!.AbsoluteUri + "#inline-" + (++pageScriptSequence));
dom.CurrentScript = null;
executingPageScript = false;
Trace?.Invoke("XFE_BROWSER_SCRIPT_END|" + (script.GetAttribute("src") ?? "Inline") + "|Success|" + result.Success + "\n");
Trace?.Invoke("XFE_BROWSER_SCRIPT_TIMING|" + (script.GetAttribute("src") ?? "Inline") +
"|Ticks|" + (System.Diagnostics.Stopwatch.GetTimestamp() - scriptStarted) +
"|PreparationTicks|" + runtime!.LastScriptTiming.PreparationTicks +
"|AdmissionTicks|" + runtime.LastScriptTiming.AdmissionTicks +
"|ExecutionTicks|" + runtime.LastScriptTiming.ExecutionTicks +
"|CacheHit|" + runtime.LastScriptTiming.CacheHit +
"|LayoutPasses|" + (LayoutPassCount - previousLayouts) +
"|DomBudgetValidations|" + (dom.BudgetValidationCount - previousBudgetChecks) +
"|Frequency|" + System.Diagnostics.Stopwatch.Frequency + "\n");
CheckOperationCancellation();
if (result.ConsoleOutput.Length != 0) Diagnostic(result.ConsoleOutput);
if (!result.Success)
{
firstScriptFailure ??= result.Error;
Diagnostic("JavaScript error: " + result.Error);
}
if (script.HasAttribute("src")) dom!.DispatchEvent(script, result.Success ? "load" : "error", new() { Bubbles = false, Cancelable = false });
scriptBudgetExhausted |= result.BudgetExceeded;
// DOM mutation and event callbacks already invalidate their real
// changes. Loading a pure library need not re-rasterize the page.
if (!documentEnhancementsPending || Revision != previousRevision) InvalidateDocument();
return !result.BudgetExceeded;
}
catch (OperationCanceledException) when (OperationCancelled) { throw; }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
executingPageScript = false;
dom!.CurrentScript = null;
if (error is BrowserLimitException)
Trace?.Invoke("XFE_BROWSER_SCRIPT_REJECTED|" + BoundedText.Safe(script.GetAttribute("src") ?? "Inline", 256) + "|Reason|ResourceBudget\n");
Diagnostic("External script failed: " + error.Message);
firstScriptFailure ??= error.Message;
dom!.DispatchEvent(script, "error", new() { Bubbles = false, Cancelable = false });
return true;
}
}
private void LoadStyle(IElement link)
{
CheckOperationCancellation();
if (cspBlocksAuthorStyles || !(link.GetAttribute("rel") ?? "").Split(' ', StringSplitOptions.RemoveEmptyEntries).Contains("stylesheet", StringComparer.OrdinalIgnoreCase)) return;
try
{
string address = ResourceAddress(link.GetAttribute("href") ?? "").AbsoluteUri;
if (styleAddresses.GetValueOrDefault(link) == address) return;
linkedStyles.RemoveAll(item => !IsConnected(item.Owner));
if (linkedStyles.Count(item => !ReferenceEquals(item.Owner, link)) >= activeLimits.MaximumStyleSheets) throw new BrowserLimitException("The stylesheet count limit was reached.");
var response = resourceLoader!.Get(ResourceAddress(link.GetAttribute("href") ?? ""), "style");
CheckOperationCancellation();
Trace?.Invoke("XFE_BROWSER_STYLE|" + response.Address + "|Characters|" + response.Body.Length + "\n");
linkedStyles.RemoveAll(item => ReferenceEquals(item.Owner, link));
linkedStyles.Add((response.Body, link)); InvalidateDocument();
styleAddresses[link] = address;
dom?.DispatchEvent(link, "load", new() { Bubbles = false, Cancelable = false });
}
catch (OperationCanceledException) when (OperationCancelled) { throw; }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ Diagnostic("Stylesheet failed: " + error.Message); dom?.DispatchEvent(link, "error", new() { Bubbles = false, Cancelable = false }); }
}
private void DrainResources(int maximum = int.MaxValue)
{
if (drainingResources || dom is null) return;
if (documentEnhancementsPending) maximum = Math.Min(maximum, 1);
maximum = Math.Min(maximum, activeLimits.MaximumEventCallbacksPerPump);
drainingResources = true;
// Explicit success/catch cleanup is needed by the current native runtime.
try
{
int count = 0;
while (count < maximum && (pendingResources.Count != 0 || dom.HasNetworkRequests))
{
CheckOperationCancellation();
if (pendingResources.Count == 0)
{
count++;
try { dom.ProcessNetworkRequest(); CaptureEventOutput(); }
catch (OperationCanceledException) when (OperationCancelled) { throw; }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ Diagnostic("Network callback failed: " + error.Message); }
continue;
}
var element = pendingResources.Dequeue();
scheduledResources.Remove(element);
if (++count > activeLimits.MaximumExternalResources) throw new BrowserLimitException("The dynamic resource queue limit was reached.");
if (element.Owner != document || !IsConnected(element) && element.LocalName != "img") continue;
if (element.LocalName == "script") RunScript(element); else if (element.LocalName == "link") LoadStyle(element); else if (element.LocalName == "img") LoadImage(element);
}
}
catch { drainingResources = false; throw; }
drainingResources = false;
}
private static bool IsConnected(INode node)
{ while (node.Parent is not null) node = node.Parent; return node.NodeType == NodeType.Document; }
private void Diagnostic(string message) => pageDiagnostics?.Append(BoundedText.Safe(message, 2_048) + "\n");
}
using AngleSharp.Dom;
namespace XFESystem.Browser;
public sealed partial class BrowserService
{
private readonly HashSet<IElement> executedScripts = new(ReferenceEqualityComparer.Instance);
private bool drainingResources;
private bool scriptBudgetExhausted;
private readonly Dictionary<IElement, string> styleAddresses = new(ReferenceEqualityComparer.Instance);
private int pageScriptSequence;
private BrowserDomHost CreateDomHost() => new()
{
Mutated = InvalidateDocument,
AttributeChanged = (_, name) => { if (layoutDirty || layoutEngine.AttributeAffectsLayout(name)) InvalidateDocument(); },
FocusChanged = _ => InvalidateDocument(),
SubmitForm = (form, submitter) => SubmitForm(form, submitter, fireEvent: false),
Navigate = reference => pendingNavigation = ResourceAddress(reference).AbsoluteUri,
ResourceRequested = element =>
{
if (scheduledResources.Add(element)) pendingResources.Enqueue(element);
},
Measure = node => EnsureLayout().GetBoundingClientRect(node),
MeasureRange = (start, startOffset, end, endOffset) => EnsureLayout().GetRangeRects(start,startOffset,end,endOffset),
HitTest = (x,y) => EnsureLayout().HitTest(x,y)?.Node,
ComputedStyle = (node, property) => EnsureLayout().GetComputedStyle(node)?.GetPropertyValue(property),
MeasureText = (text, font) => textMetrics.Measure(text, font),
RasterizeText = (text, font) => metricCanvas?.RasterizeWebText(text, font.Size, font.Family, font.Weight)
?? throw new NotSupportedException("The platform did not supply a real Canvas text rasterizer."),
ImageInfo = element =>
{
string source = element.GetAttribute("src") ?? "";
var image = FindImage(source);
bool complete = source.Length == 0;
if (!complete)
{
try { complete = images.ContainsKey(ImageKey(source)); }
catch (Exception error) when (error is UriFormatException or NotSupportedException) { complete = true; }
}
return (complete, image?.Width ?? 0, image?.Height ?? 0);
},
ReadCookies = () => transport is IWebCookieStore store ? store.ReadDocumentCookies(currentAddress!) : pageCookies.ReadDocumentCookies(currentAddress!),
WriteCookie = value => { if (transport is IWebCookieStore store) store.WriteDocumentCookie(currentAddress!, value); else pageCookies.WriteDocumentCookie(currentAddress!, value); },
GetResourceWithHeaders = (reference, headers) =>
{
if (scriptPolicy?.BlocksAllScripts == true) throw new InvalidOperationException("The declared connection policy cannot be enforced.");
Uri target = ResourceAddress(reference);
if (target.GetLeftPart(UriPartial.Authority) != currentAddress!.GetLeftPart(UriPartial.Authority))
throw new NotSupportedException("Cross-origin script requests require CORS support.");
var response = resourceLoader!.Get(target, "fetch", headers);
if (response.Address.GetLeftPart(UriPartial.Authority) != currentAddress.GetLeftPart(UriPartial.Authority))
throw new NotSupportedException("A script request redirected outside its origin.");
return response;
},
FillRandom = transport is IBrowserRandomSource random ? random.FillRandom : null,
Viewport = () => viewport
};
private Uri ResourceAddress(string reference)
{
Uri basis = currentAddress!;
string? declared = document?.QuerySelector("base[href]")?.GetAttribute("href");
if (!string.IsNullOrEmpty(declared) && Uri.TryCreate(basis, declared, out var candidate))
{ ValidateWebAddress(candidate); if (scriptPolicy?.AllowsBase(currentAddress!, candidate) != false) basis = candidate; }
var address = new Uri(basis, reference);
ValidateWebAddress(address); return address;
}
private bool RunScript(IElement script)
{
CheckOperationCancellation();
if (scriptBudgetExhausted) return false;
if (dom!.IsInertParsedScript(script)) return true;
if (scriptPolicy?.Allows(script, dom!.IsDynamicScript(script)) == false || !executedScripts.Add(script)) return true;
string type = (script.GetAttribute("type") ?? string.Empty).Trim().ToLowerInvariant();
if (type == "module") { Diagnostic("Module scripts are not supported by the classic-script interpreter."); return true; }
if (type is not ("" or "text/javascript" or "application/javascript" or "text/ecmascript" or "application/ecmascript")) return true;
try
{
bool external = script.HasAttribute("src");
string source = external
? resourceLoader!.Get(ResourceAddress(script.GetAttribute("src")!), "script").Body : script.TextContent;
CheckOperationCancellation();
Trace?.Invoke("XFE_BROWSER_SCRIPT_BEGIN|" + (script.GetAttribute("src") ?? "Inline") + "|Characters|" + source.Length + "\n");
CheckOperationCancellation();
long previousRevision = Revision;
long previousLayouts = LayoutPassCount, previousBudgetChecks = dom!.BudgetValidationCount;
dom!.CurrentScript = script;
executingPageScript = true;
long scriptStarted = System.Diagnostics.Stopwatch.GetTimestamp();
var result = external && resourceLoader!.LastResponsePackaged
? runtime!.EvaluatePackaged(source, ResourceAddress(script.GetAttribute("src")!).AbsoluteUri)
: runtime!.EvaluateNamed(source, external ? ResourceAddress(script.GetAttribute("src")!).AbsoluteUri : currentAddress!.AbsoluteUri + "#inline-" + (++pageScriptSequence));
dom.CurrentScript = null;
executingPageScript = false;
Trace?.Invoke("XFE_BROWSER_SCRIPT_END|" + (script.GetAttribute("src") ?? "Inline") + "|Success|" + result.Success + "\n");
Trace?.Invoke("XFE_BROWSER_SCRIPT_TIMING|" + (script.GetAttribute("src") ?? "Inline") +
"|Ticks|" + (System.Diagnostics.Stopwatch.GetTimestamp() - scriptStarted) +
"|PreparationTicks|" + runtime!.LastScriptTiming.PreparationTicks +
"|AdmissionTicks|" + runtime.LastScriptTiming.AdmissionTicks +
"|ExecutionTicks|" + runtime.LastScriptTiming.ExecutionTicks +
"|CacheHit|" + runtime.LastScriptTiming.CacheHit +
"|LayoutPasses|" + (LayoutPassCount - previousLayouts) +
"|DomBudgetValidations|" + (dom.BudgetValidationCount - previousBudgetChecks) +
"|Frequency|" + System.Diagnostics.Stopwatch.Frequency + "\n");
CheckOperationCancellation();
if (result.ConsoleOutput.Length != 0) Diagnostic(result.ConsoleOutput);
if (!result.Success)
{
firstScriptFailure ??= result.Error;
Diagnostic("JavaScript error: " + result.Error);
}
if (script.HasAttribute("src")) dom!.DispatchEvent(script, result.Success ? "load" : "error", new() { Bubbles = false, Cancelable = false });
scriptBudgetExhausted |= result.BudgetExceeded;
// DOM mutation and event callbacks already invalidate their real
// changes. Loading a pure library need not re-rasterize the page.
if (!documentEnhancementsPending || Revision != previousRevision) InvalidateDocument();
return !result.BudgetExceeded;
}
catch (OperationCanceledException) when (OperationCancelled) { throw; }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{
executingPageScript = false;
dom!.CurrentScript = null;
if (error is BrowserLimitException)
Trace?.Invoke("XFE_BROWSER_SCRIPT_REJECTED|" + BoundedText.Safe(script.GetAttribute("src") ?? "Inline", 256) + "|Reason|ResourceBudget\n");
Diagnostic("External script failed: " + error.Message);
firstScriptFailure ??= error.Message;
dom!.DispatchEvent(script, "error", new() { Bubbles = false, Cancelable = false });
return true;
}
}
private void LoadStyle(IElement link)
{
CheckOperationCancellation();
if (cspBlocksAuthorStyles || !(link.GetAttribute("rel") ?? "").Split(' ', StringSplitOptions.RemoveEmptyEntries).Contains("stylesheet", StringComparer.OrdinalIgnoreCase)) return;
try
{
string address = ResourceAddress(link.GetAttribute("href") ?? "").AbsoluteUri;
if (styleAddresses.GetValueOrDefault(link) == address) return;
linkedStyles.RemoveAll(item => !IsConnected(item.Owner));
if (linkedStyles.Count(item => !ReferenceEquals(item.Owner, link)) >= activeLimits.MaximumStyleSheets) throw new BrowserLimitException("The stylesheet count limit was reached.");
var response = resourceLoader!.Get(ResourceAddress(link.GetAttribute("href") ?? ""), "style");
CheckOperationCancellation();
Trace?.Invoke("XFE_BROWSER_STYLE|" + response.Address + "|Characters|" + response.Body.Length + "\n");
linkedStyles.RemoveAll(item => ReferenceEquals(item.Owner, link));
linkedStyles.Add((response.Body, link)); InvalidateDocument();
styleAddresses[link] = address;
dom?.DispatchEvent(link, "load", new() { Bubbles = false, Cancelable = false });
}
catch (OperationCanceledException) when (OperationCancelled) { throw; }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ Diagnostic("Stylesheet failed: " + error.Message); dom?.DispatchEvent(link, "error", new() { Bubbles = false, Cancelable = false }); }
}
private void DrainResources(int maximum = int.MaxValue)
{
if (drainingResources || dom is null) return;
if (documentEnhancementsPending) maximum = Math.Min(maximum, 1);
maximum = Math.Min(maximum, activeLimits.MaximumEventCallbacksPerPump);
drainingResources = true;
// Explicit success/catch cleanup is needed by the current native runtime.
try
{
int count = 0;
while (count < maximum && (pendingResources.Count != 0 || dom.HasNetworkRequests))
{
CheckOperationCancellation();
if (pendingResources.Count == 0)
{
count++;
try { dom.ProcessNetworkRequest(); CaptureEventOutput(); }
catch (OperationCanceledException) when (OperationCancelled) { throw; }
catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
{ Diagnostic("Network callback failed: " + error.Message); }
continue;
}
var element = pendingResources.Dequeue();
scheduledResources.Remove(element);
if (++count > activeLimits.MaximumExternalResources) throw new BrowserLimitException("The dynamic resource queue limit was reached.");
if (element.Owner != document || !IsConnected(element) && element.LocalName != "img") continue;
if (element.LocalName == "script") RunScript(element); else if (element.LocalName == "link") LoadStyle(element); else if (element.LocalName == "img") LoadImage(element);
}
}
catch { drainingResources = false; throw; }
drainingResources = false;
}
private static bool IsConnected(INode node)
{ while (node.Parent is not null) node = node.Parent; return node.NodeType == NodeType.Document; }
private void Diagnostic(string message) => pageDiagnostics?.Append(BoundedText.Safe(message, 2_048) + "\n");
}