Merge remote-tracking branch 'origin/master'

This commit is contained in:
kennycud
2025-05-13 11:14:08 -07:00
5 changed files with 47 additions and 21 deletions

View File

@@ -1,14 +1,13 @@
package org.qortal.api; package org.qortal.api;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.select.Elements; import org.jsoup.select.Elements;
import org.qortal.arbitrary.misc.Service; import org.qortal.arbitrary.misc.Service;
import java.util.Objects; import java.util.Objects;
public class HTMLParser { public class HTMLParser {
private static final Logger LOGGER = LogManager.getLogger(HTMLParser.class); private static final Logger LOGGER = LogManager.getLogger(HTMLParser.class);
@@ -22,10 +21,11 @@ public class HTMLParser {
private String identifier; private String identifier;
private String path; private String path;
private String theme; private String theme;
private String lang;
private boolean usingCustomRouting; private boolean usingCustomRouting;
public HTMLParser(String resourceId, String inPath, String prefix, boolean includeResourceIdInPrefix, byte[] data, public HTMLParser(String resourceId, String inPath, String prefix, boolean includeResourceIdInPrefix, byte[] data,
String qdnContext, Service service, String identifier, String theme, boolean usingCustomRouting) { String qdnContext, Service service, String identifier, String theme, boolean usingCustomRouting, String lang) {
String inPathWithoutFilename = inPath.contains("/") ? inPath.substring(0, inPath.lastIndexOf('/')) : String.format("/%s",inPath); String inPathWithoutFilename = inPath.contains("/") ? inPath.substring(0, inPath.lastIndexOf('/')) : String.format("/%s",inPath);
this.qdnBase = includeResourceIdInPrefix ? String.format("%s/%s", prefix, resourceId) : prefix; this.qdnBase = includeResourceIdInPrefix ? String.format("%s/%s", prefix, resourceId) : prefix;
this.qdnBaseWithPath = includeResourceIdInPrefix ? String.format("%s/%s%s", prefix, resourceId, inPathWithoutFilename) : String.format("%s%s", prefix, inPathWithoutFilename); this.qdnBaseWithPath = includeResourceIdInPrefix ? String.format("%s/%s%s", prefix, resourceId, inPathWithoutFilename) : String.format("%s%s", prefix, inPathWithoutFilename);
@@ -36,6 +36,7 @@ public class HTMLParser {
this.identifier = identifier; this.identifier = identifier;
this.path = inPath; this.path = inPath;
this.theme = theme; this.theme = theme;
this.lang = lang;
this.usingCustomRouting = usingCustomRouting; this.usingCustomRouting = usingCustomRouting;
} }
@@ -61,9 +62,13 @@ public class HTMLParser {
String identifier = this.identifier != null ? this.identifier.replace("\\", "").replace("\"","\\\"") : ""; String identifier = this.identifier != null ? this.identifier.replace("\\", "").replace("\"","\\\"") : "";
String path = this.path != null ? this.path.replace("\\", "").replace("\"","\\\"") : ""; String path = this.path != null ? this.path.replace("\\", "").replace("\"","\\\"") : "";
String theme = this.theme != null ? this.theme.replace("\\", "").replace("\"","\\\"") : ""; String theme = this.theme != null ? this.theme.replace("\\", "").replace("\"","\\\"") : "";
String lang = this.lang != null ? this.lang.replace("\\", "").replace("\"", "\\\"") : "";
String qdnBase = this.qdnBase != null ? this.qdnBase.replace("\\", "").replace("\"","\\\"") : ""; String qdnBase = this.qdnBase != null ? this.qdnBase.replace("\\", "").replace("\"","\\\"") : "";
String qdnBaseWithPath = this.qdnBaseWithPath != null ? this.qdnBaseWithPath.replace("\\", "").replace("\"","\\\"") : ""; String qdnBaseWithPath = this.qdnBaseWithPath != null ? this.qdnBaseWithPath.replace("\\", "").replace("\"","\\\"") : "";
String qdnContextVar = String.format("<script>var _qdnContext=\"%s\"; var _qdnTheme=\"%s\"; var _qdnService=\"%s\"; var _qdnName=\"%s\"; var _qdnIdentifier=\"%s\"; var _qdnPath=\"%s\"; var _qdnBase=\"%s\"; var _qdnBaseWithPath=\"%s\";</script>", qdnContext, theme, service, name, identifier, path, qdnBase, qdnBaseWithPath); String qdnContextVar = String.format(
"<script>var _qdnContext=\"%s\"; var _qdnTheme=\"%s\"; var _qdnLang=\"%s\"; var _qdnService=\"%s\"; var _qdnName=\"%s\"; var _qdnIdentifier=\"%s\"; var _qdnPath=\"%s\"; var _qdnBase=\"%s\"; var _qdnBaseWithPath=\"%s\";</script>",
qdnContext, theme, lang, service, name, identifier, path, qdnBase, qdnBaseWithPath
);
head.get(0).prepend(qdnContextVar); head.get(0).prepend(qdnContextVar);
// Add base href tag // Add base href tag

View File

@@ -142,10 +142,20 @@ public class DevProxyServerResource {
} }
} }
String lang = request.getParameter("lang");
if (lang == null || lang.isBlank()) {
lang = "en"; // fallback
}
String theme = request.getParameter("theme");
if (theme == null || theme.isBlank()) {
theme = "light";
}
// Parse and modify output if needed // Parse and modify output if needed
if (HTMLParser.isHtmlFile(filename)) { if (HTMLParser.isHtmlFile(filename)) {
// HTML file - needs to be parsed // HTML file - needs to be parsed
HTMLParser htmlParser = new HTMLParser("", inPath, "", false, data, "proxy", Service.APP, null, "light", true); HTMLParser htmlParser = new HTMLParser("", inPath, "", false, data, "proxy", Service.APP, null, theme , true, lang);
htmlParser.addAdditionalHeaderTags(); htmlParser.addAdditionalHeaderTags();
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' data: blob:; img-src 'self' data: blob:; connect-src 'self' ws:; font-src 'self' data:;"); response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' data: blob:; img-src 'self' data: blob:; connect-src 'self' ws:; font-src 'self' data:;");
response.setContentType(con.getContentType()); response.setContentType(con.getContentType());

View File

@@ -71,33 +71,33 @@ public class RenderResource {
@Path("/signature/{signature}") @Path("/signature/{signature}")
@SecurityRequirement(name = "apiKey") @SecurityRequirement(name = "apiKey")
public HttpServletResponse getIndexBySignature(@PathParam("signature") String signature, public HttpServletResponse getIndexBySignature(@PathParam("signature") String signature,
@QueryParam("theme") String theme) { @QueryParam("theme") String theme, @QueryParam("lang") String lang) {
if (!Settings.getInstance().isQDNAuthBypassEnabled()) if (!Settings.getInstance().isQDNAuthBypassEnabled())
Security.requirePriorAuthorization(request, signature, Service.WEBSITE, null); Security.requirePriorAuthorization(request, signature, Service.WEBSITE, null);
return this.get(signature, ResourceIdType.SIGNATURE, null, null, "/", null, "/render/signature", true, true, theme); return this.get(signature, ResourceIdType.SIGNATURE, null, null, "/", null, "/render/signature", true, true, theme, lang);
} }
@GET @GET
@Path("/signature/{signature}/{path:.*}") @Path("/signature/{signature}/{path:.*}")
@SecurityRequirement(name = "apiKey") @SecurityRequirement(name = "apiKey")
public HttpServletResponse getPathBySignature(@PathParam("signature") String signature, @PathParam("path") String inPath, public HttpServletResponse getPathBySignature(@PathParam("signature") String signature, @PathParam("path") String inPath,
@QueryParam("theme") String theme) { @QueryParam("theme") String theme, @QueryParam("lang") String lang) {
if (!Settings.getInstance().isQDNAuthBypassEnabled()) if (!Settings.getInstance().isQDNAuthBypassEnabled())
Security.requirePriorAuthorization(request, signature, Service.WEBSITE, null); Security.requirePriorAuthorization(request, signature, Service.WEBSITE, null);
return this.get(signature, ResourceIdType.SIGNATURE, null, null, inPath,null, "/render/signature", true, true, theme); return this.get(signature, ResourceIdType.SIGNATURE, null, null, inPath,null, "/render/signature", true, true, theme, lang);
} }
@GET @GET
@Path("/hash/{hash}") @Path("/hash/{hash}")
@SecurityRequirement(name = "apiKey") @SecurityRequirement(name = "apiKey")
public HttpServletResponse getIndexByHash(@PathParam("hash") String hash58, @QueryParam("secret") String secret58, public HttpServletResponse getIndexByHash(@PathParam("hash") String hash58, @QueryParam("secret") String secret58,
@QueryParam("theme") String theme) { @QueryParam("theme") String theme, @QueryParam("lang") String lang) {
if (!Settings.getInstance().isQDNAuthBypassEnabled()) if (!Settings.getInstance().isQDNAuthBypassEnabled())
Security.requirePriorAuthorization(request, hash58, Service.WEBSITE, null); Security.requirePriorAuthorization(request, hash58, Service.WEBSITE, null);
return this.get(hash58, ResourceIdType.FILE_HASH, Service.ARBITRARY_DATA, null, "/", secret58, "/render/hash", true, false, theme); return this.get(hash58, ResourceIdType.FILE_HASH, Service.ARBITRARY_DATA, null, "/", secret58, "/render/hash", true, false, theme, lang);
} }
@GET @GET
@@ -105,11 +105,11 @@ public class RenderResource {
@SecurityRequirement(name = "apiKey") @SecurityRequirement(name = "apiKey")
public HttpServletResponse getPathByHash(@PathParam("hash") String hash58, @PathParam("path") String inPath, public HttpServletResponse getPathByHash(@PathParam("hash") String hash58, @PathParam("path") String inPath,
@QueryParam("secret") String secret58, @QueryParam("secret") String secret58,
@QueryParam("theme") String theme) { @QueryParam("theme") String theme, @QueryParam("lang") String lang) {
if (!Settings.getInstance().isQDNAuthBypassEnabled()) if (!Settings.getInstance().isQDNAuthBypassEnabled())
Security.requirePriorAuthorization(request, hash58, Service.WEBSITE, null); Security.requirePriorAuthorization(request, hash58, Service.WEBSITE, null);
return this.get(hash58, ResourceIdType.FILE_HASH, Service.ARBITRARY_DATA, null, inPath, secret58, "/render/hash", true, false, theme); return this.get(hash58, ResourceIdType.FILE_HASH, Service.ARBITRARY_DATA, null, inPath, secret58, "/render/hash", true, false, theme, lang);
} }
@GET @GET
@@ -119,12 +119,12 @@ public class RenderResource {
@PathParam("name") String name, @PathParam("name") String name,
@PathParam("path") String inPath, @PathParam("path") String inPath,
@QueryParam("identifier") String identifier, @QueryParam("identifier") String identifier,
@QueryParam("theme") String theme) { @QueryParam("theme") String theme, @QueryParam("lang") String lang) {
if (!Settings.getInstance().isQDNAuthBypassEnabled()) if (!Settings.getInstance().isQDNAuthBypassEnabled())
Security.requirePriorAuthorization(request, name, service, null); Security.requirePriorAuthorization(request, name, service, null);
String prefix = String.format("/render/%s", service); String prefix = String.format("/render/%s", service);
return this.get(name, ResourceIdType.NAME, service, identifier, inPath, null, prefix, true, true, theme); return this.get(name, ResourceIdType.NAME, service, identifier, inPath, null, prefix, true, true, theme, lang);
} }
@GET @GET
@@ -133,18 +133,18 @@ public class RenderResource {
public HttpServletResponse getIndexByName(@PathParam("service") Service service, public HttpServletResponse getIndexByName(@PathParam("service") Service service,
@PathParam("name") String name, @PathParam("name") String name,
@QueryParam("identifier") String identifier, @QueryParam("identifier") String identifier,
@QueryParam("theme") String theme) { @QueryParam("theme") String theme, @QueryParam("lang") String lang) {
if (!Settings.getInstance().isQDNAuthBypassEnabled()) if (!Settings.getInstance().isQDNAuthBypassEnabled())
Security.requirePriorAuthorization(request, name, service, null); Security.requirePriorAuthorization(request, name, service, null);
String prefix = String.format("/render/%s", service); String prefix = String.format("/render/%s", service);
return this.get(name, ResourceIdType.NAME, service, identifier, "/", null, prefix, true, true, theme); return this.get(name, ResourceIdType.NAME, service, identifier, "/", null, prefix, true, true, theme, lang);
} }
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String identifier, private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String identifier,
String inPath, String secret58, String prefix, boolean includeResourceIdInPrefix, boolean async, String theme) { String inPath, String secret58, String prefix, boolean includeResourceIdInPrefix, boolean async, String theme, String lang) {
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, identifier, inPath, ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, identifier, inPath,
secret58, prefix, includeResourceIdInPrefix, async, "render", request, response, context); secret58, prefix, includeResourceIdInPrefix, async, "render", request, response, context);
@@ -152,6 +152,9 @@ public class RenderResource {
if (theme != null) { if (theme != null) {
renderer.setTheme(theme); renderer.setTheme(theme);
} }
if (lang != null) {
renderer.setLang(lang);
}
return renderer.render(); return renderer.render();
} }

View File

@@ -37,6 +37,7 @@ public class ArbitraryDataRenderer {
private final Service service; private final Service service;
private final String identifier; private final String identifier;
private String theme = "light"; private String theme = "light";
private String lang = "en";
private String inPath; private String inPath;
private final String secret58; private final String secret58;
private final String prefix; private final String prefix;
@@ -166,7 +167,7 @@ public class ArbitraryDataRenderer {
if (HTMLParser.isHtmlFile(filename)) { if (HTMLParser.isHtmlFile(filename)) {
// HTML file - needs to be parsed // HTML file - needs to be parsed
byte[] data = Files.readAllBytes(filePath); // TODO: limit file size that can be read into memory byte[] data = Files.readAllBytes(filePath); // TODO: limit file size that can be read into memory
HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, includeResourceIdInPrefix, data, qdnContext, service, identifier, theme, usingCustomRouting); HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, includeResourceIdInPrefix, data, qdnContext, service, identifier, theme, usingCustomRouting, lang);
htmlParser.addAdditionalHeaderTags(); htmlParser.addAdditionalHeaderTags();
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; media-src 'self' data: blob:; img-src 'self' data: blob:; connect-src 'self' wss:;"); response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; font-src 'self' data:; media-src 'self' data: blob:; img-src 'self' data: blob:; connect-src 'self' wss:;");
response.setContentType(context.getMimeType(filename)); response.setContentType(context.getMimeType(filename));
@@ -256,5 +257,8 @@ public class ArbitraryDataRenderer {
public void setTheme(String theme) { public void setTheme(String theme) {
this.theme = theme; this.theme = theme;
} }
public void setLang(String lang) {
this.lang = lang;
}
} }

View File

@@ -45,6 +45,7 @@ function parseUrl(url) {
// Remove theme, identifier, and time queries if they exist // Remove theme, identifier, and time queries if they exist
parsedUrl.searchParams.delete("theme"); parsedUrl.searchParams.delete("theme");
parsedUrl.searchParams.delete("lang");
parsedUrl.searchParams.delete("identifier"); parsedUrl.searchParams.delete("identifier");
parsedUrl.searchParams.delete("time"); parsedUrl.searchParams.delete("time");
parsedUrl.searchParams.delete("isManualNavigation"); parsedUrl.searchParams.delete("isManualNavigation");
@@ -213,8 +214,11 @@ function buildResourceUrl(service, name, identifier, path, isLink) {
if (path != null) url = url.concat((path.startsWith("/") ? "" : "/") + path); if (path != null) url = url.concat((path.startsWith("/") ? "" : "/") + path);
} }
if (isLink) url = url.concat((url.includes("?") ? "" : "?") + "&theme=" + _qdnTheme); if (isLink) {
const hasQuery = url.includes("?");
const queryPrefix = hasQuery ? "&" : "?";
url += queryPrefix + "theme=" + _qdnTheme + "&lang=" + _qdnLang;
}
return url; return url;
} }