Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
91 changes: 91 additions & 0 deletions src/main/java/org/htmlunit/BlobUrlStore.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright (c) 2002-2026 Gargoyle Software Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.htmlunit;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.htmlunit.javascript.host.file.Blob;

/**
* The user-agent-wide blob URL store defined by
* <a href="https://w3c.github.io/FileAPI/#BlobURLStore">Blob URL Store</a>.
*
* @author Lai Quang Duong
*/
public class BlobUrlStore {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great to have a separate class for this.

Maybe we should add a clear() method to support testing use cases where we like to remove urls to test error cases. I guess the page for removal is not always at hand.


private static final class Entry {
private final Blob object_;
private final Page owningPage_;

Entry(final Blob object, final Page owningPage) {
object_ = object;
owningPage_ = owningPage;
}
}

private final Map<String, Entry> store_ = new ConcurrentHashMap<>();

Check warning on line 40 in src/main/java/org/htmlunit/BlobUrlStore.java

View workflow job for this annotation

GitHub Actions / PMD

[PMD] reported by reviewdog 🐶 Fields should be declared at the top of the class, before any method declarations, constructors, initializers or inner classes. Raw Output: {"level":"warning","locations":[{"physicalLocation":{"artifactLocation":{"uri":"file:///home/runner/work/htmlunit/htmlunit/src/main/java/org/htmlunit/BlobUrlStore.java"},"region":{"endColumn":44,"endLine":40,"startColumn":38,"startLine":40}}}],"message":{"text":"Fields should be declared at the top of the class, before any method declarations, constructors, initializers or inner classes."},"ruleId":"FieldDeclarationsShouldBeAtStartOfClass","ruleIndex":0}

/**
* Adds an entry.
* @param blobUrl the generated {@code blob:} URL
* @param object the {@link Blob} (or {@code File}) the URL refers to
* @param owningPage the page that created the URL
*/
public void put(final String blobUrl, final Blob object, final Page owningPage) {
store_.put(blobUrl, new Entry(object, owningPage));
}

/**
* <a href="https://w3c.github.io/FileAPI/#blob-url-resolve">Resolve a blob URL</a>.
* @param blobUrl the {@code blob:} URL to resolve
* @return the stored {@link Blob}, or {@code null} if there is no entry
*/
public Blob resolve(final String blobUrl) {
final Entry entry = store_.get(excludeFragment(blobUrl));
return entry == null ? null : entry.object_;
}

/**
* Removes an entry.
* @param blobUrl the {@code blob:} URL to remove
*/
public void remove(final String blobUrl) {
store_.remove(excludeFragment(blobUrl));
}

/**
* See <a href="https://w3c.github.io/FileAPI/#lifeTime">Lifetime of blob URLs</a>.
* @param owningPage the unloading page
*/
void removeForPage(final Page owningPage) {
store_.values().removeIf(entry -> entry.owningPage_ == owningPage);
}

/**
* <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
*
* <p>Removes all entries.

Check failure on line 81 in src/main/java/org/htmlunit/BlobUrlStore.java

View workflow job for this annotation

GitHub Actions / CheckStyle

[checkstyle] reported by reviewdog 🐶 Unclosed HTML tag found: p Raw Output: /home/runner/work/htmlunit/htmlunit/src/main/java/org/htmlunit/BlobUrlStore.java:81:0: error: Unclosed HTML tag found: p (com.puppycrawl.tools.checkstyle.checks.javadoc.SummaryJavadocCheck)
*/
public void clear() {
store_.clear();
}

private static String excludeFragment(final String blobUrl) {
final int fragmentIndex = blobUrl.indexOf('#');
return fragmentIndex >= 0 ? blobUrl.substring(0, fragmentIndex) : blobUrl;
}
}
23 changes: 19 additions & 4 deletions src/main/java/org/htmlunit/WebClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
Expand Down Expand Up @@ -233,6 +234,8 @@ public class WebClient implements Serializable, AutoCloseable {
Collections.synchronizedList(new ArrayList<>());
private WebWindow currentWindow_;

private transient BlobUrlStore blobUrlStore_ = new BlobUrlStore();

private HTMLParserListener htmlParserListener_;
private CSSErrorHandler cssErrorHandler_ = new DefaultCssErrorHandler();
private OnbeforeunloadHandler onbeforeunloadHandler_;
Expand Down Expand Up @@ -1019,6 +1022,14 @@ public void setCurrentWindow(final WebWindow window) {
}
}

/**
* Returns the blob URL store for this client.
* @return the {@link BlobUrlStore} for this client
*/
public BlobUrlStore getBlobUrlStore() {
return blobUrlStore_;
}

/**
* Adds a listener for {@link WebWindowEvent}s. All events from all windows associated with this
* client will be sent to the specified listener.
Expand All @@ -1045,6 +1056,8 @@ private void fireWindowContentChanged(final WebWindowEvent event) {
for (final WebWindowListener listener : new ArrayList<>(webWindowListeners_)) {
listener.webWindowContentChanged(event);
}

blobUrlStore_.removeForPage(event.getOldPage());
}

private void fireWindowOpened(final WebWindowEvent event) {
Expand All @@ -1065,6 +1078,8 @@ private void fireWindowClosed(final WebWindowEvent event) {
listener.webWindowClosed(event);
}

blobUrlStore_.removeForPage(event.getOldPage());

// to open a new top level window if all others are gone
if (currentWindowTracker_ != null) {
currentWindowTracker_.afterWebWindowClosedListenersProcessed(event);
Expand Down Expand Up @@ -1450,11 +1465,10 @@ private WebResponse makeWebResponseForFileUrl(final WebRequest webRequest) throw
return webResponse;
}

private WebResponse makeWebResponseForBlobUrl(final WebRequest webRequest) {
final Window window = getCurrentWindow().getScriptableObject();
final Blob fileOrBlob = window.getDocument().resolveBlobUrl(webRequest.getUrl().toString());
private WebResponse makeWebResponseForBlobUrl(final WebRequest webRequest) throws IOException {
final Blob fileOrBlob = blobUrlStore_.resolve(webRequest.getUrl().toString());
if (fileOrBlob == null) {
throw JavaScriptEngine.typeError("Cannot load data from " + webRequest.getUrl());
throw new FileNotFoundException("No entry for '" + webRequest.getUrl() + "' in the BlobUrlStore.");
}

final List<NameValuePair> headers = new ArrayList<>();
Expand Down Expand Up @@ -2671,6 +2685,7 @@ private void readObject(final ObjectInputStream in) throws IOException, ClassNot
loadQueue_ = new ArrayList<>();
css3ParserPool_ = new CSS3ParserPool();
broadcastChannel_ = new HashSet<>();
blobUrlStore_ = new BlobUrlStore();
}

private static class LoadJob {
Expand Down
42 changes: 32 additions & 10 deletions src/main/java/org/htmlunit/javascript/host/URL.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@

import java.net.MalformedURLException;
import java.util.List;
import java.util.UUID;

import org.apache.commons.lang3.StringUtils;
import org.htmlunit.Page;
import org.htmlunit.WebWindow;
import org.htmlunit.corejs.javascript.Context;
import org.htmlunit.corejs.javascript.Scriptable;
import org.htmlunit.javascript.HtmlUnitScriptable;
Expand Down Expand Up @@ -83,32 +86,51 @@
/**
* The URL.createObjectURL() static method creates a DOMString containing a URL
* representing the object given in parameter.
* The URL lifetime is tied to the document in the window on which it was created.
* The new object URL represents the specified File object or Blob object.
* The new object URL represents the specified {@link File} object or {@link Blob} object
* and is registered in the {@link org.htmlunit.BlobUrlStore user-agent-wide blob URL store},
* so it can be resolved from any document of the same client.
*
* @param fileOrBlob Is a File object or a Blob object to create a object URL for.
* @return the url
*/
@JsxStaticFunction
public static String createObjectURL(final Object fileOrBlob) {
if (fileOrBlob instanceof File file) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe i'm blind but looks like the file support is no longer there?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh the support is still there. The previous implementation doing both check was redundant because File itself is a Blob

return getWindow(file).getDocument().generateBlobUrl(file);
if (!(fileOrBlob instanceof Blob blob)) {
throw JavaScriptEngine.typeError("URL.createObjectURL: argument 1 is not a Blob.");
}

if (fileOrBlob instanceof Blob blob) {
return getWindow(blob).getDocument().generateBlobUrl(blob);
final WebWindow webWindow = getWindow(blob).getWebWindow();
final Page page = webWindow.getEnclosedPage();
final java.net.URL pageUrl = page.getUrl();

String origin = "null";
if (pageUrl != UrlUtils.URL_ABOUT_BLANK) {
final int port = pageUrl.getPort();
if (port < 0 || port == pageUrl.getDefaultPort()) {
origin = pageUrl.getProtocol() + "://" + pageUrl.getHost();

Check warning on line 110 in src/main/java/org/htmlunit/javascript/host/URL.java

View workflow job for this annotation

GitHub Actions / PMD

[PMD] reported by reviewdog 🐶 The String literal "://" appears 4 times in this file; the first occurrence is on line 110 Raw Output: {"level":"warning","locations":[{"physicalLocation":{"artifactLocation":{"uri":"file:///home/runner/work/htmlunit/htmlunit/src/main/java/org/htmlunit/javascript/host/URL.java"},"region":{"endColumn":55,"endLine":110,"startColumn":50,"startLine":110}}}],"message":{"text":"The String literal \"://\" appears 4 times in this file; the first occurrence is on line 110"},"ruleId":"AvoidDuplicateLiterals","ruleIndex":249}
}
else {
origin = pageUrl.getProtocol() + "://" + pageUrl.getHost() + ':' + port;
}
}

return null;
final String blobUrl = "blob:" + origin + "/" + UUID.randomUUID();
webWindow.getWebClient().getBlobUrlStore().put(blobUrl, blob, page);
return blobUrl;
}

/**
* @param objectURL String representing the object URL that was
* created by calling URL.createObjectURL().
* Revokes a {@code blob:} URL, removing its entry from the blob URL store.
*
* @param objectURL the object URL previously returned by {@link #createObjectURL(Object)}
*/
@JsxStaticFunction
public static void revokeObjectURL(final Scriptable objectURL) {
getWindow(objectURL).getDocument().revokeBlobUrl(Context.toString(objectURL));
final String url = Context.toString(objectURL);
if (!url.startsWith("blob:")) {
return;
}
getWindow(objectURL).getWebWindow().getWebClient().getBlobUrlStore().remove(url);
}

/**
Expand Down
40 changes: 0 additions & 40 deletions src/main/java/org/htmlunit/javascript/host/dom/Document.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,11 @@
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Predicate;

import org.apache.commons.logging.Log;
Expand Down Expand Up @@ -118,7 +116,6 @@
import org.htmlunit.javascript.host.event.TextEvent;
import org.htmlunit.javascript.host.event.UIEvent;
import org.htmlunit.javascript.host.event.WheelEvent;
import org.htmlunit.javascript.host.file.Blob;
import org.htmlunit.javascript.host.html.HTMLAllCollection;
import org.htmlunit.javascript.host.html.HTMLBodyElement;
import org.htmlunit.javascript.host.html.HTMLCollection;
Expand Down Expand Up @@ -231,8 +228,6 @@ public class Document extends Node {
private transient FontFaceSet fonts_;
private transient StyleSheetList styleSheetList_;

private final Map<String, Blob> blobUrl2Blobs_ = new HashMap<>();

static {
// commands
String[] cmds = {
Expand Down Expand Up @@ -3583,39 +3578,4 @@ public void clear() {
public boolean contains(final Object element) {
return getDocumentElement().contains(element);
}

/**
* Generate and return the URL for the given blob.
* @param blob the Blob containing the data
* @return the URL {@link org.htmlunit.javascript.host.URL#createObjectURL(Object)}
*/
public String generateBlobUrl(final Blob blob) {
final URL url = getPage().getUrl();

String origin = "null";
if (!UrlUtils.URL_ABOUT_BLANK.equals(url)) {
origin = url.getProtocol() + "://" + url.getAuthority();
}

final String blobUrl = "blob:" + origin + "/" + UUID.randomUUID();
blobUrl2Blobs_.put(blobUrl, blob);
return blobUrl;
}

/**
* Resolves a blob URL to its associated {@link Blob}.
* @param url the url to resolve
* @return the Blob for the given URL or {@code null} if not found
*/
public Blob resolveBlobUrl(final String url) {
return blobUrl2Blobs_.get(url);
}

/**
* Revokes the URL for the given blob.
* @param url the url to revoke {@link org.htmlunit.javascript.host.URL#revokeObjectURL(Scriptable)}
*/
public void revokeBlobUrl(final String url) {
blobUrl2Blobs_.remove(url);
}
}
14 changes: 12 additions & 2 deletions src/main/java/org/htmlunit/javascript/host/xml/XMLHttpRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -720,10 +720,20 @@ public void open(final String method, final Object urlParam, final Object asyncP
return;
}

final boolean isDataUrl = "data".equals(fullUrl.getProtocol());
if (isDataUrl) {
if ("data".equals(fullUrl.getProtocol())) {
isSameOrigin_ = true;
}
else if ("blob".equals(fullUrl.getProtocol())) {
boolean sameOrigin = false;
try {
final URL blobOrigin = UrlUtils.toUrlUnsafe(fullUrl.toExternalForm().substring("blob:".length()));
sameOrigin = UrlUtils.isSameOrigin(pageUrl, blobOrigin);
}
catch (final MalformedURLException ignored) {
// keep sameOrigin = false
}
isSameOrigin_ = sameOrigin;
}
else {
isSameOrigin_ = UrlUtils.isSameOrigin(pageUrl, fullUrl);
final boolean alwaysAddOrigin = HttpMethod.GET != request.getHttpMethod()
Expand Down
Loading
Loading