Skip to content

Commit 5339c6e

Browse files
committed
URL: reimplement URL.createObjectURL()
1 parent 784b87f commit 5339c6e

6 files changed

Lines changed: 276 additions & 56 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright (c) 2002-2026 Gargoyle Software Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* https://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
package org.htmlunit;
16+
17+
import java.util.Map;
18+
import java.util.concurrent.ConcurrentHashMap;
19+
20+
import org.htmlunit.javascript.host.file.Blob;
21+
22+
/**
23+
* The user-agent-wide blob URL store defined by
24+
* <a href="https://w3c.github.io/FileAPI/#BlobURLStore">Blob URL Store</a>.
25+
*
26+
* @author Lai Quang Duong
27+
*/
28+
public class BlobUrlStore {
29+
30+
private static final class Entry {
31+
private final Blob object_;
32+
private final Page owningPage_;
33+
34+
Entry(final Blob object, final Page owningPage) {
35+
object_ = object;
36+
owningPage_ = owningPage;
37+
}
38+
}
39+
40+
private final Map<String, Entry> store_ = new ConcurrentHashMap<>();
41+
42+
/**
43+
* Adds an entry.
44+
* @param blobUrl the generated {@code blob:} URL
45+
* @param object the {@link Blob} (or {@code File}) the URL refers to
46+
* @param owningPage the page that created the URL
47+
*/
48+
public void put(final String blobUrl, final Blob object, final Page owningPage) {
49+
store_.put(blobUrl, new Entry(object, owningPage));
50+
}
51+
52+
/**
53+
* <a href="https://w3c.github.io/FileAPI/#blob-url-resolve">Resolve a blob URL</a>.
54+
* @param blobUrl the {@code blob:} URL to resolve
55+
* @return the stored {@link Blob}, or {@code null} if there is no entry
56+
*/
57+
public Blob resolve(final String blobUrl) {
58+
final Entry entry = store_.get(excludeFragment(blobUrl));
59+
return entry == null ? null : entry.object_;
60+
}
61+
62+
/**
63+
* Removes an entry.
64+
* @param blobUrl the {@code blob:} URL to remove
65+
*/
66+
public void remove(final String blobUrl) {
67+
store_.remove(excludeFragment(blobUrl));
68+
}
69+
70+
/**
71+
* See <a href="https://w3c.github.io/FileAPI/#lifeTime">Lifetime of blob URLs</a>.
72+
* @param owningPage the unloading page
73+
*/
74+
void removeForPage(final Page owningPage) {
75+
store_.values().removeIf(entry -> entry.owningPage_ == owningPage);
76+
}
77+
78+
/**
79+
* <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
80+
*
81+
* <p>Removes all entries.
82+
*/
83+
public void clear() {
84+
store_.clear();
85+
}
86+
87+
private static String excludeFragment(final String blobUrl) {
88+
final int fragmentIndex = blobUrl.indexOf('#');
89+
return fragmentIndex >= 0 ? blobUrl.substring(0, fragmentIndex) : blobUrl;
90+
}
91+
}

src/main/java/org/htmlunit/WebClient.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import java.io.BufferedInputStream;
2323
import java.io.File;
24+
import java.io.FileNotFoundException;
2425
import java.io.IOException;
2526
import java.io.InputStream;
2627
import java.io.ObjectInputStream;
@@ -233,6 +234,8 @@ public class WebClient implements Serializable, AutoCloseable {
233234
Collections.synchronizedList(new ArrayList<>());
234235
private WebWindow currentWindow_;
235236

237+
private transient BlobUrlStore blobUrlStore_ = new BlobUrlStore();
238+
236239
private HTMLParserListener htmlParserListener_;
237240
private CSSErrorHandler cssErrorHandler_ = new DefaultCssErrorHandler();
238241
private OnbeforeunloadHandler onbeforeunloadHandler_;
@@ -1019,6 +1022,14 @@ public void setCurrentWindow(final WebWindow window) {
10191022
}
10201023
}
10211024

1025+
/**
1026+
* Returns the blob URL store for this client.
1027+
* @return the {@link BlobUrlStore} for this client
1028+
*/
1029+
public BlobUrlStore getBlobUrlStore() {
1030+
return blobUrlStore_;
1031+
}
1032+
10221033
/**
10231034
* Adds a listener for {@link WebWindowEvent}s. All events from all windows associated with this
10241035
* client will be sent to the specified listener.
@@ -1045,6 +1056,8 @@ private void fireWindowContentChanged(final WebWindowEvent event) {
10451056
for (final WebWindowListener listener : new ArrayList<>(webWindowListeners_)) {
10461057
listener.webWindowContentChanged(event);
10471058
}
1059+
1060+
blobUrlStore_.removeForPage(event.getOldPage());
10481061
}
10491062

10501063
private void fireWindowOpened(final WebWindowEvent event) {
@@ -1065,6 +1078,8 @@ private void fireWindowClosed(final WebWindowEvent event) {
10651078
listener.webWindowClosed(event);
10661079
}
10671080

1081+
blobUrlStore_.removeForPage(event.getOldPage());
1082+
10681083
// to open a new top level window if all others are gone
10691084
if (currentWindowTracker_ != null) {
10701085
currentWindowTracker_.afterWebWindowClosedListenersProcessed(event);
@@ -1450,11 +1465,10 @@ private WebResponse makeWebResponseForFileUrl(final WebRequest webRequest) throw
14501465
return webResponse;
14511466
}
14521467

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

14601474
final List<NameValuePair> headers = new ArrayList<>();
@@ -2671,6 +2685,7 @@ private void readObject(final ObjectInputStream in) throws IOException, ClassNot
26712685
loadQueue_ = new ArrayList<>();
26722686
css3ParserPool_ = new CSS3ParserPool();
26732687
broadcastChannel_ = new HashSet<>();
2688+
blobUrlStore_ = new BlobUrlStore();
26742689
}
26752690

26762691
private static class LoadJob {

src/main/java/org/htmlunit/javascript/host/URL.java

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@
1818

1919
import java.net.MalformedURLException;
2020
import java.util.List;
21+
import java.util.UUID;
2122

2223
import org.apache.commons.lang3.StringUtils;
24+
import org.htmlunit.Page;
25+
import org.htmlunit.WebWindow;
2326
import org.htmlunit.corejs.javascript.Context;
2427
import org.htmlunit.corejs.javascript.Scriptable;
2528
import org.htmlunit.javascript.HtmlUnitScriptable;
@@ -83,32 +86,51 @@ public void jsConstructor(final String url, final Object base) {
8386
/**
8487
* The URL.createObjectURL() static method creates a DOMString containing a URL
8588
* representing the object given in parameter.
86-
* The URL lifetime is tied to the document in the window on which it was created.
87-
* The new object URL represents the specified File object or Blob object.
89+
* The new object URL represents the specified {@link File} object or {@link Blob} object
90+
* and is registered in the {@link org.htmlunit.BlobUrlStore user-agent-wide blob URL store},
91+
* so it can be resolved from any document of the same client.
8892
*
8993
* @param fileOrBlob Is a File object or a Blob object to create a object URL for.
9094
* @return the url
9195
*/
9296
@JsxStaticFunction
9397
public static String createObjectURL(final Object fileOrBlob) {
94-
if (fileOrBlob instanceof File file) {
95-
return getWindow(file).getDocument().generateBlobUrl(file);
98+
if (!(fileOrBlob instanceof Blob blob)) {
99+
throw JavaScriptEngine.typeError("URL.createObjectURL: argument 1 is not a Blob.");
96100
}
97101

98-
if (fileOrBlob instanceof Blob blob) {
99-
return getWindow(blob).getDocument().generateBlobUrl(blob);
102+
final WebWindow webWindow = getWindow(blob).getWebWindow();
103+
final Page page = webWindow.getEnclosedPage();
104+
final java.net.URL pageUrl = page.getUrl();
105+
106+
String origin = "null";
107+
if (pageUrl != UrlUtils.URL_ABOUT_BLANK) {
108+
final int port = pageUrl.getPort();
109+
if (port < 0 || port == pageUrl.getDefaultPort()) {
110+
origin = pageUrl.getProtocol() + "://" + pageUrl.getHost();
111+
}
112+
else {
113+
origin = pageUrl.getProtocol() + "://" + pageUrl.getHost() + ':' + port;
114+
}
100115
}
101116

102-
return null;
117+
final String blobUrl = "blob:" + origin + "/" + UUID.randomUUID();
118+
webWindow.getWebClient().getBlobUrlStore().put(blobUrl, blob, page);
119+
return blobUrl;
103120
}
104121

105122
/**
106-
* @param objectURL String representing the object URL that was
107-
* created by calling URL.createObjectURL().
123+
* Revokes a {@code blob:} URL, removing its entry from the blob URL store.
124+
*
125+
* @param objectURL the object URL previously returned by {@link #createObjectURL(Object)}
108126
*/
109127
@JsxStaticFunction
110128
public static void revokeObjectURL(final Scriptable objectURL) {
111-
getWindow(objectURL).getDocument().revokeBlobUrl(Context.toString(objectURL));
129+
final String url = Context.toString(objectURL);
130+
if (!url.startsWith("blob:")) {
131+
return;
132+
}
133+
getWindow(objectURL).getWebWindow().getWebClient().getBlobUrlStore().remove(url);
112134
}
113135

114136
/**

src/main/java/org/htmlunit/javascript/host/dom/Document.java

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,11 @@
3030
import java.time.format.DateTimeFormatter;
3131
import java.util.ArrayList;
3232
import java.util.Date;
33-
import java.util.HashMap;
3433
import java.util.HashSet;
3534
import java.util.List;
3635
import java.util.Locale;
3736
import java.util.Map;
3837
import java.util.Set;
39-
import java.util.UUID;
4038
import java.util.function.Predicate;
4139

4240
import org.apache.commons.logging.Log;
@@ -118,7 +116,6 @@
118116
import org.htmlunit.javascript.host.event.TextEvent;
119117
import org.htmlunit.javascript.host.event.UIEvent;
120118
import org.htmlunit.javascript.host.event.WheelEvent;
121-
import org.htmlunit.javascript.host.file.Blob;
122119
import org.htmlunit.javascript.host.html.HTMLAllCollection;
123120
import org.htmlunit.javascript.host.html.HTMLBodyElement;
124121
import org.htmlunit.javascript.host.html.HTMLCollection;
@@ -231,8 +228,6 @@ public class Document extends Node {
231228
private transient FontFaceSet fonts_;
232229
private transient StyleSheetList styleSheetList_;
233230

234-
private final Map<String, Blob> blobUrl2Blobs_ = new HashMap<>();
235-
236231
static {
237232
// commands
238233
String[] cmds = {
@@ -3583,39 +3578,4 @@ public void clear() {
35833578
public boolean contains(final Object element) {
35843579
return getDocumentElement().contains(element);
35853580
}
3586-
3587-
/**
3588-
* Generate and return the URL for the given blob.
3589-
* @param blob the Blob containing the data
3590-
* @return the URL {@link org.htmlunit.javascript.host.URL#createObjectURL(Object)}
3591-
*/
3592-
public String generateBlobUrl(final Blob blob) {
3593-
final URL url = getPage().getUrl();
3594-
3595-
String origin = "null";
3596-
if (!UrlUtils.URL_ABOUT_BLANK.equals(url)) {
3597-
origin = url.getProtocol() + "://" + url.getAuthority();
3598-
}
3599-
3600-
final String blobUrl = "blob:" + origin + "/" + UUID.randomUUID();
3601-
blobUrl2Blobs_.put(blobUrl, blob);
3602-
return blobUrl;
3603-
}
3604-
3605-
/**
3606-
* Resolves a blob URL to its associated {@link Blob}.
3607-
* @param url the url to resolve
3608-
* @return the Blob for the given URL or {@code null} if not found
3609-
*/
3610-
public Blob resolveBlobUrl(final String url) {
3611-
return blobUrl2Blobs_.get(url);
3612-
}
3613-
3614-
/**
3615-
* Revokes the URL for the given blob.
3616-
* @param url the url to revoke {@link org.htmlunit.javascript.host.URL#revokeObjectURL(Scriptable)}
3617-
*/
3618-
public void revokeBlobUrl(final String url) {
3619-
blobUrl2Blobs_.remove(url);
3620-
}
36213581
}

0 commit comments

Comments
 (0)