mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
Allow navigation from a blob URL.
These are used a lot in WPT test.
This commit is contained in:
@@ -404,6 +404,18 @@ pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
|
|||||||
return std.mem.startsWith(u8, url, current_origin);
|
return std.mem.startsWith(u8, url, current_origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Look up a blob URL in this page's registry, walking up the parent chain.
|
||||||
|
pub fn lookupBlobUrl(self: *Page, url: []const u8) ?*Blob {
|
||||||
|
var current: ?*Page = self;
|
||||||
|
while (current) |page| {
|
||||||
|
if (page._blob_urls.get(url)) |blob| {
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
current = page.parent;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void {
|
pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void {
|
||||||
lp.assert(self._load_state == .waiting, "page.renavigate", .{});
|
lp.assert(self._load_state == .waiting, "page.renavigate", .{});
|
||||||
const session = self._session;
|
const session = self._session;
|
||||||
@@ -419,12 +431,17 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
|||||||
.type = self._type,
|
.type = self._type,
|
||||||
});
|
});
|
||||||
|
|
||||||
// if the url is about:blank, we load an empty HTML document in the
|
// Handle synthetic navigations: about:blank and blob: URLs
|
||||||
// page and dispatch the events.
|
const is_about_blank = std.mem.eql(u8, "about:blank", request_url);
|
||||||
if (std.mem.eql(u8, "about:blank", request_url)) {
|
const is_blob = !is_about_blank and std.mem.startsWith(u8, request_url, "blob:");
|
||||||
self.url = "about:blank";
|
|
||||||
|
|
||||||
if (self.parent) |parent| {
|
if (is_about_blank or is_blob) {
|
||||||
|
self.url = if (is_about_blank) "about:blank" else try self.arena.dupeZ(u8, request_url);
|
||||||
|
|
||||||
|
if (is_blob) {
|
||||||
|
// strip out blob:
|
||||||
|
self.origin = try URL.getOrigin(self.arena, request_url[5.. :0]);
|
||||||
|
} else if (self.parent) |parent| {
|
||||||
self.origin = parent.origin;
|
self.origin = parent.origin;
|
||||||
} else {
|
} else {
|
||||||
self.origin = null;
|
self.origin = null;
|
||||||
@@ -435,10 +452,22 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
|||||||
// It's important to force a reset during the following navigation.
|
// It's important to force a reset during the following navigation.
|
||||||
self._parse_state = .complete;
|
self._parse_state = .complete;
|
||||||
|
|
||||||
|
// Content injection
|
||||||
|
if (is_blob) {
|
||||||
|
const blob = self.lookupBlobUrl(request_url) orelse {
|
||||||
|
log.warn(.js, "invalid blob", .{ .url = request_url });
|
||||||
|
return error.BlobNotFound;
|
||||||
|
};
|
||||||
|
const parse_arena = try self.getArena(.{ .debug = "Page.parseBlob" });
|
||||||
|
defer self.releaseArena(parse_arena);
|
||||||
|
var parser = Parser.init(parse_arena, self.document.asNode(), self);
|
||||||
|
parser.parse(blob._slice);
|
||||||
|
} else {
|
||||||
self.document.injectBlank(self) catch |err| {
|
self.document.injectBlank(self) catch |err| {
|
||||||
log.err(.browser, "inject blank", .{ .err = err });
|
log.err(.browser, "inject blank", .{ .err = err });
|
||||||
return error.InjectBlankFailed;
|
return error.InjectBlankFailed;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
self.documentIsComplete();
|
self.documentIsComplete();
|
||||||
|
|
||||||
session.notification.dispatch(.page_navigate, &.{
|
session.notification.dispatch(.page_navigate, &.{
|
||||||
@@ -452,7 +481,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
|
|||||||
// Record telemetry for navigation
|
// Record telemetry for navigation
|
||||||
session.browser.app.telemetry.record(.{
|
session.browser.app.telemetry.record(.{
|
||||||
.navigate = .{
|
.navigate = .{
|
||||||
.tls = false, // about:blank is not TLS
|
.tls = false, // about:blank and blob: are not TLS
|
||||||
.proxy = session.browser.app.config.httpProxy() != null,
|
.proxy = session.browser.app.config.httpProxy() != null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -277,6 +277,11 @@ pub fn isCompleteHTTPUrl(url: []const u8) bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// blob: and data: URLs are complete but don't follow scheme:// pattern
|
||||||
|
if (std.mem.startsWith(u8, url, "blob:") or std.mem.startsWith(u8, url, "data:")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if there's a scheme (protocol) ending with ://
|
// Check if there's a scheme (protocol) ending with ://
|
||||||
const colon_pos = std.mem.indexOfScalar(u8, url, ':') orelse return false;
|
const colon_pos = std.mem.indexOfScalar(u8, url, ':') orelse return false;
|
||||||
|
|
||||||
|
|||||||
41
src/browser/tests/page/blob.html
Normal file
41
src/browser/tests/page/blob.html
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<body></body>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
|
||||||
|
<script id="basic_blob_navigation">
|
||||||
|
{
|
||||||
|
const html = '<html><head></head><body><div id="test">Hello Blob</div></body></html>';
|
||||||
|
const blob = new Blob([html], { type: 'text/html' });
|
||||||
|
const blob_url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const iframe = document.createElement('iframe');
|
||||||
|
document.body.appendChild(iframe);
|
||||||
|
iframe.src = blob_url;
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual('Hello Blob', iframe.contentDocument.getElementById('test').textContent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="multiple_blobs">
|
||||||
|
{
|
||||||
|
const blob1 = new Blob(['<html><body>First</body></html>'], { type: 'text/html' });
|
||||||
|
const blob2 = new Blob(['<html><body>Second</body></html>'], { type: 'text/html' });
|
||||||
|
const url1 = URL.createObjectURL(blob1);
|
||||||
|
const url2 = URL.createObjectURL(blob2);
|
||||||
|
|
||||||
|
const iframe1 = document.createElement('iframe');
|
||||||
|
document.body.appendChild(iframe1);
|
||||||
|
iframe1.src = url1;
|
||||||
|
|
||||||
|
const iframe2 = document.createElement('iframe');
|
||||||
|
document.body.appendChild(iframe2);
|
||||||
|
iframe2.src = url2;
|
||||||
|
|
||||||
|
testing.eventually(() => {
|
||||||
|
testing.expectEqual('First', iframe1.contentDocument.body.textContent);
|
||||||
|
testing.expectEqual('Second', iframe2.contentDocument.body.textContent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -249,6 +249,8 @@ pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 {
|
|||||||
.{ page.origin orelse "null", uuid_buf },
|
.{ page.origin orelse "null", uuid_buf },
|
||||||
);
|
);
|
||||||
try page._blob_urls.put(page.arena, blob_url, blob);
|
try page._blob_urls.put(page.arena, blob_url, blob);
|
||||||
|
// prevent GC from cleaning up the blob while it's in the registry
|
||||||
|
page.js.strongRef(blob);
|
||||||
return blob_url;
|
return blob_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,8 +260,10 @@ pub fn revokeObjectURL(url: []const u8, page: *Page) void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from registry (no-op if not found)
|
// Remove from registry and release strong ref (no-op if not found)
|
||||||
_ = page._blob_urls.remove(url);
|
if (page._blob_urls.fetchRemove(url)) |entry| {
|
||||||
|
page.js.weakRef(entry.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const JsApi = struct {
|
pub const JsApi = struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user