From 09279cbb666b4bd2199eae8582c74f5cd44ba70a Mon Sep 17 00:00:00 2001
From: Aurora Lahtela <24460436+AuroraLS3@users.noreply.github.com>
Date: Sun, 5 Feb 2023 12:08:29 +0200
Subject: [PATCH] React html customization / public_html folder (#2862)
* Add public_html folder, configuration and access methods to it
* Make Frontend BETA static resource resolution prefer public_html
* Add resolver for getting any file in public_html from webserver
* Test customized bundle loading from public_html
* Update gradle wrapper to 7.6
* Wrote scripts to React build or run dev server through gradle
* Disable cyclomatic-complexity check on PublicHtmlResolver
* Throw bad request exception on IllegalPathException
* Throw bad request exception on bad chars in URI query
---
.../plan/delivery/web/resolver/MimeType.java | 2 +-
.../web/resolver/request/URIQuery.java | 3 +
Plan/common/build.gradle | 8 +
.../plan/delivery/DeliveryUtilities.java | 10 +-
.../delivery/rendering/pages/PageFactory.java | 24 +-
.../plan/delivery/web/ResolverSvc.java | 17 +-
.../plan/delivery/web/ResourceSvc.java | 10 +-
.../web/WebAssetVersionCheckTask.java | 7 +-
.../delivery/webserver/ResponseFactory.java | 140 +++++----
.../delivery/webserver/ResponseResolver.java | 4 +
.../resolver/PublicHtmlResolver.java | 105 +++++++
.../settings/config/ResourceSettings.java | 16 +-
.../config/paths/WebserverSettings.java | 1 +
.../plan/storage/file/PlanFiles.java | 30 +-
.../plan/storage/file/PublicHtmlFiles.java | 88 ++++++
.../resources/assets/plan/bungeeconfig.yml | 32 ++-
.../src/main/resources/assets/plan/config.yml | 32 ++-
.../plan/storage/file/PlanFilesTest.java | 17 --
.../storage/file/PublicHtmlFilesTest.java | 100 +++++++
.../java/extension/FullSystemExtension.java | 10 +
Plan/gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 61574 bytes
Plan/gradle/wrapper/gradle-wrapper.properties | 3 +-
Plan/gradlew | 269 +++++++++++-------
Plan/gradlew.bat | 15 +-
Plan/react/buildBundle.bat | 2 +
Plan/react/buildBundle.sh | 4 +
Plan/react/devServer.bat | 2 +
Plan/react/devServer.sh | 4 +
.../plan/storage/file/SpongePlanFiles.java | 6 +-
29 files changed, 682 insertions(+), 279 deletions(-)
create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/PublicHtmlResolver.java
create mode 100644 Plan/common/src/main/java/com/djrapitops/plan/storage/file/PublicHtmlFiles.java
create mode 100644 Plan/common/src/test/java/com/djrapitops/plan/storage/file/PublicHtmlFilesTest.java
create mode 100644 Plan/react/buildBundle.bat
create mode 100644 Plan/react/buildBundle.sh
create mode 100644 Plan/react/devServer.bat
create mode 100644 Plan/react/devServer.sh
diff --git a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/MimeType.java b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/MimeType.java
index b83f0ffb86..62b2a1ea59 100644
--- a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/MimeType.java
+++ b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/MimeType.java
@@ -20,7 +20,7 @@ public final class MimeType {
public static final String HTML = "text/html";
public static final String CSS = "text/css";
public static final String JSON = "application/json";
- public static final String JS = "application/javascript";
+ public static final String JS = "text/javascript";
public static final String IMAGE = "image/gif";
public static final String FAVICON = "image/x-icon";
public static final String FONT_TTF = "application/x-font-ttf";
diff --git a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/request/URIQuery.java b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/request/URIQuery.java
index 4d906838cc..28f1b23ee3 100644
--- a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/request/URIQuery.java
+++ b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/request/URIQuery.java
@@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.delivery.web.resolver.request;
+import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
import org.apache.commons.lang3.StringUtils;
import java.io.UnsupportedEncodingException;
@@ -82,6 +83,8 @@ private void parseAndPutKeyEmptyValue(Map parameters, String s)
);
} catch (UnsupportedEncodingException e) {
// If UTF-8 is unsupported, we have bigger problems
+ } catch (IllegalArgumentException badCharacter) {
+ throw new BadRequestException("URI Query contained bad character");
}
}
diff --git a/Plan/common/build.gradle b/Plan/common/build.gradle
index 9e947a1a47..5a0507d659 100644
--- a/Plan/common/build.gradle
+++ b/Plan/common/build.gradle
@@ -105,6 +105,14 @@ task yarnBundle(type: YarnTask) {
args = ['run', 'build']
}
+task yarnStart(type: YarnTask) {
+ logging.captureStandardOutput LogLevel.INFO
+ inputs.file("$rootDir/react/dashboard/package.json")
+
+ dependsOn yarn_install
+ args = ['run', 'start']
+}
+
task copyYarnBuildResults {
inputs.files(fileTree("$rootDir/react/dashboard/build"))
outputs.dir("$rootDir/common/build/resources/main/assets/plan/web")
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/DeliveryUtilities.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/DeliveryUtilities.java
index 688198b9d0..440acf5b07 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/DeliveryUtilities.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/DeliveryUtilities.java
@@ -18,6 +18,7 @@
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.json.graphs.Graphs;
+import com.djrapitops.plan.storage.file.PublicHtmlFiles;
import dagger.Lazy;
import javax.inject.Inject;
@@ -28,14 +29,16 @@ public class DeliveryUtilities {
private final Lazy formatters;
private final Lazy graphs;
+ private final Lazy publicHtmlFiles;
@Inject
public DeliveryUtilities(
Lazy formatters,
- Lazy graphs
- ) {
+ Lazy graphs,
+ Lazy publicHtmlFiles) {
this.formatters = formatters;
this.graphs = graphs;
+ this.publicHtmlFiles = publicHtmlFiles;
}
public Formatters getFormatters() {
@@ -46,4 +49,7 @@ public Graphs getGraphs() {
return graphs.get();
}
+ public PublicHtmlFiles getPublicHtmlFiles() {
+ return publicHtmlFiles.get();
+ }
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/PageFactory.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/PageFactory.java
index 1b52e66b63..7992fdbe27 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/PageFactory.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/PageFactory.java
@@ -39,6 +39,7 @@
import com.djrapitops.plan.storage.database.queries.containers.ContainerFetchQueries;
import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
import com.djrapitops.plan.storage.file.PlanFiles;
+import com.djrapitops.plan.storage.file.PublicHtmlFiles;
import com.djrapitops.plan.utilities.dev.Untrusted;
import com.djrapitops.plan.version.VersionChecker;
import dagger.Lazy;
@@ -59,6 +60,7 @@ public class PageFactory {
private final Lazy versionChecker;
private final Lazy files;
+ private final Lazy publicHtmlFiles;
private final Lazy config;
private final Lazy theme;
private final Lazy dbSystem;
@@ -73,7 +75,7 @@ public class PageFactory {
public PageFactory(
Lazy versionChecker,
Lazy files,
- Lazy config,
+ Lazy publicHtmlFiles, Lazy config,
Lazy theme,
Lazy dbSystem,
Lazy serverInfo,
@@ -85,6 +87,7 @@ public PageFactory(
) {
this.versionChecker = versionChecker;
this.files = files;
+ this.publicHtmlFiles = publicHtmlFiles;
this.config = config;
this.theme = theme;
this.dbSystem = dbSystem;
@@ -106,7 +109,8 @@ public Page playersPage() throws IOException {
}
public Page reactPage() throws IOException {
- return new ReactPage(getBasePath(), getResource("index.html"));
+ // TODO use ResourceService to apply snippets to the React index.html
+ return new ReactPage(getBasePath(), getPublicHtmlOrJarResource("index.html"));
}
private String getBasePath() {
@@ -244,16 +248,26 @@ public String getResourceAsString(String name) throws IOException {
return getResource(name).asString();
}
- public WebResource getResource(String name) throws IOException {
+ public WebResource getResource(String resourceName) throws IOException {
try {
- return ResourceService.getInstance().getResource("Plan", name,
- () -> files.get().getResourceFromJar("web/" + name).asWebResource()
+ return ResourceService.getInstance().getResource("Plan", resourceName,
+ () -> files.get().getResourceFromJar("web/" + resourceName).asWebResource()
);
} catch (UncheckedIOException readFail) {
throw readFail.getCause();
}
}
+ public WebResource getPublicHtmlOrJarResource(String resourceName) throws IOException {
+ try {
+ return publicHtmlFiles.get().findPublicHtmlResource(resourceName)
+ .orElseGet(() -> files.get().getResourceFromJar("web/" + resourceName))
+ .asWebResource();
+ } catch (UncheckedIOException readFail) {
+ throw readFail.getCause();
+ }
+ }
+
public Page loginPage() throws IOException {
if (config.get().isTrue(PluginSettings.FRONTEND_BETA)) {
return reactPage();
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/ResolverSvc.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/ResolverSvc.java
index 7b56d95d6d..0db799497e 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/ResolverSvc.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/ResolverSvc.java
@@ -17,13 +17,17 @@
package com.djrapitops.plan.delivery.web;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
+import com.djrapitops.plan.settings.config.PlanConfig;
+import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.utilities.dev.Untrusted;
+import net.playeranalytics.plugin.server.PluginLogger;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.*;
import java.util.function.Predicate;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
/**
* ResolverService Implementation.
@@ -33,11 +37,16 @@
@Singleton
public class ResolverSvc implements ResolverService {
+ private final PlanConfig config;
+ private final PluginLogger logger;
+
private final List basicResolvers;
private final List regexResolvers;
@Inject
- public ResolverSvc() {
+ public ResolverSvc(PlanConfig config, PluginLogger logger) {
+ this.config = config;
+ this.logger = logger;
basicResolvers = new ArrayList<>();
regexResolvers = new ArrayList<>();
}
@@ -78,6 +87,12 @@ public List getResolvers(@Untrusted String target) {
for (Container container : regexResolvers) {
if (container.matcher.test(target)) resolvers.add(container.resolver);
}
+ if (config.isTrue(PluginSettings.DEV_MODE)) {
+ logger.info("Match Resolvers " + target + " - " + resolvers.stream()
+ .map(Object::getClass)
+ .map(Class::getSimpleName)
+ .collect(Collectors.toList()));
+ }
return resolvers;
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/ResourceSvc.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/ResourceSvc.java
index 9efa6f387d..5e5afe5add 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/ResourceSvc.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/ResourceSvc.java
@@ -21,7 +21,7 @@
import com.djrapitops.plan.settings.config.ResourceSettings;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.PluginLang;
-import com.djrapitops.plan.storage.file.PlanFiles;
+import com.djrapitops.plan.storage.file.PublicHtmlFiles;
import com.djrapitops.plan.storage.file.Resource;
import com.djrapitops.plan.utilities.dev.Untrusted;
import com.djrapitops.plan.utilities.logging.ErrorContext;
@@ -50,7 +50,7 @@
public class ResourceSvc implements ResourceService {
public final Set snippets;
- private final PlanFiles files;
+ private final PublicHtmlFiles publicHtmlFiles;
private final ResourceSettings resourceSettings;
private final Locale locale;
private final PluginLogger logger;
@@ -58,13 +58,13 @@ public class ResourceSvc implements ResourceService {
@Inject
public ResourceSvc(
- PlanFiles files,
+ PublicHtmlFiles publicHtmlFiles,
PlanConfig config,
Locale locale,
PluginLogger logger,
ErrorLogger errorLogger
) {
- this.files = files;
+ this.publicHtmlFiles = publicHtmlFiles;
this.resourceSettings = config.getResourceSettings();
this.locale = locale;
this.logger = logger;
@@ -155,7 +155,7 @@ public WebResource getTheResource(String pluginName, @Untrusted String fileName,
}
public WebResource getOrWriteCustomized(@Untrusted String fileName, Supplier source) throws IOException {
- Optional customizedResource = files.getCustomizableResource(fileName);
+ Optional customizedResource = publicHtmlFiles.findCustomizedResource(fileName);
if (customizedResource.isPresent()) {
return readCustomized(customizedResource.get());
} else {
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/WebAssetVersionCheckTask.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/WebAssetVersionCheckTask.java
index e05cf9a142..0de54d1d86 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/WebAssetVersionCheckTask.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/web/WebAssetVersionCheckTask.java
@@ -28,6 +28,7 @@
import javax.inject.Singleton;
import java.io.File;
import java.io.IOException;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -35,8 +36,11 @@
/**
* Task in charge of checking html customized files on enable to see if they are outdated.
+ *
+ * @deprecated Html customization system will be overhauled for React version of frontend.
*/
@Singleton
+@Deprecated(forRemoval = true, since = "#2260") // TODO Remove after Frontend BETA
public class WebAssetVersionCheckTask extends TaskSystem.Task {
private final PlanConfig config;
@@ -110,7 +114,8 @@ private void runTask() {
}
private Optional findOutdatedResource(String resource) {
- Optional resourceFile = files.attemptToFind(resource);
+ Path dir = config.getResourceSettings().getCustomizationDirectory();
+ Optional resourceFile = files.attemptToFind(dir, resource);
Optional webAssetVersion = assetVersions.getAssetVersion(resource);
if (resourceFile.isPresent() && webAssetVersion.isPresent() && webAssetVersion.get() > resourceFile.get().lastModified()) {
return Optional.of(new AssetInfo(
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseFactory.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseFactory.java
index 305b8a0302..9a61b10772 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseFactory.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseFactory.java
@@ -30,10 +30,10 @@
import com.djrapitops.plan.delivery.web.resolver.exception.NotFoundException;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resource.WebResource;
-import com.djrapitops.plan.delivery.webserver.auth.FailReason;
-import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.identification.Identifiers;
import com.djrapitops.plan.identification.ServerUUID;
+import com.djrapitops.plan.settings.config.PlanConfig;
+import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.ErrorPageLang;
import com.djrapitops.plan.settings.theme.Theme;
@@ -41,6 +41,8 @@
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.queries.containers.ContainerFetchQueries;
import com.djrapitops.plan.storage.file.PlanFiles;
+import com.djrapitops.plan.storage.file.PublicHtmlFiles;
+import com.djrapitops.plan.storage.file.Resource;
import com.djrapitops.plan.utilities.dev.Untrusted;
import com.djrapitops.plan.utilities.java.Maps;
import com.djrapitops.plan.utilities.java.UnaryChain;
@@ -53,8 +55,6 @@
import javax.inject.Singleton;
import java.io.IOException;
import java.io.UncheckedIOException;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
@@ -70,6 +70,8 @@ public class ResponseFactory {
private static final String STATIC_BUNDLE_FOLDER = "static";
private final PlanFiles files;
+ private final PlanConfig config;
+ private final PublicHtmlFiles publicHtmlFiles;
private final PageFactory pageFactory;
private final Locale locale;
private final DBSystem dbSystem;
@@ -80,6 +82,7 @@ public class ResponseFactory {
@Inject
public ResponseFactory(
PlanFiles files,
+ PlanConfig config, PublicHtmlFiles publicHtmlFiles,
PageFactory pageFactory,
Locale locale,
DBSystem dbSystem,
@@ -88,6 +91,8 @@ public ResponseFactory(
Lazy addresses
) {
this.files = files;
+ this.config = config;
+ this.publicHtmlFiles = publicHtmlFiles;
this.pageFactory = pageFactory;
this.locale = locale;
this.dbSystem = dbSystem;
@@ -97,11 +102,23 @@ public ResponseFactory(
httpLastModifiedFormatter = formatters.httpLastModifiedLong();
}
+ /**
+ * @throws UncheckedIOException If reading the resource fails
+ */
public WebResource getResource(@Untrusted String resourceName) {
return ResourceService.getInstance().getResource("Plan", resourceName,
() -> files.getResourceFromJar("web/" + resourceName).asWebResource());
}
+ /**
+ * @throws UncheckedIOException If reading the resource fails
+ */
+ private WebResource getPublicOrJarResource(@Untrusted String resourceName) {
+ return publicHtmlFiles.findPublicHtmlResource(resourceName)
+ .orElseGet(() -> files.getResourceFromJar("web/" + resourceName))
+ .asWebResource();
+ }
+
private static Response browserCachedNotChangedResponse() {
return Response.builder()
.setStatus(304)
@@ -168,7 +185,7 @@ private Response buildDBNotOpenResponse(Database.State dbState) throws IOExcepti
}
private Response getCachedOrNew(long modified, String fileName, Function newResponseFunction) {
- WebResource resource = getResource(fileName);
+ WebResource resource = config.isTrue(PluginSettings.FRONTEND_BETA) ? getPublicOrJarResource(fileName) : getResource(fileName);
Optional lastModified = resource.getLastModified();
if (lastModified.isPresent() && modified == lastModified.get()) {
return browserCachedNotChangedResponse();
@@ -217,7 +234,7 @@ public Response javaScriptResponse(long modified, @Untrusted String fileName) {
public Response javaScriptResponse(@Untrusted String fileName) {
try {
- WebResource resource = getResource(fileName);
+ WebResource resource = config.isTrue(PluginSettings.FRONTEND_BETA) ? getPublicOrJarResource(fileName) : getResource(fileName);
String content = UnaryChain.of(resource.asString())
.chain(this::replaceMainAddressPlaceholder)
.chain(theme::replaceThemeColors)
@@ -267,7 +284,7 @@ public Response cssResponse(long modified, @Untrusted String fileName) {
public Response cssResponse(@Untrusted String fileName) {
try {
- WebResource resource = getResource(fileName);
+ WebResource resource = config.isTrue(PluginSettings.FRONTEND_BETA) ? getPublicOrJarResource(fileName) : getResource(fileName);
String content = UnaryChain.of(resource.asString())
.chain(theme::replaceThemeColors)
.chain(contents -> StringUtils.replace(contents, "/static", getBasePath() + "/static"))
@@ -297,7 +314,7 @@ public Response imageResponse(long modified, @Untrusted String fileName) {
public Response imageResponse(@Untrusted String fileName) {
try {
- WebResource resource = getResource(fileName);
+ WebResource resource = config.isTrue(PluginSettings.FRONTEND_BETA) ? getPublicOrJarResource(fileName) : getResource(fileName);
ResponseBuilder responseBuilder = Response.builder()
.setMimeType(MimeType.IMAGE)
.setContent(resource)
@@ -333,7 +350,7 @@ public Response fontResponse(@Untrusted String fileName) {
type = MimeType.FONT_BYTESTREAM;
}
try {
- WebResource resource = getResource(fileName);
+ WebResource resource = config.isTrue(PluginSettings.FRONTEND_BETA) ? getPublicOrJarResource(fileName) : getResource(fileName);
ResponseBuilder responseBuilder = Response.builder()
.setMimeType(type)
.setContent(resource);
@@ -350,6 +367,47 @@ public Response fontResponse(@Untrusted String fileName) {
}
}
+ public Response publicHtmlResourceResponse(long modified, @Untrusted String fileName, String mimeType) {
+ // Slightly different from getCachedOrNew
+ WebResource resource = publicHtmlFiles.findPublicHtmlResource(fileName)
+ .map(Resource::asWebResource)
+ .orElse(null);
+ if (resource == null) return null;
+
+ Optional lastModified = resource.getLastModified();
+ if (lastModified.isPresent() && modified == lastModified.get()) {
+ return browserCachedNotChangedResponse();
+ } else {
+ return publicHtmlResourceResponse(fileName, mimeType);
+ }
+ }
+
+ public Response publicHtmlResourceResponse(@Untrusted String fileName, String mimeType) {
+ try {
+ WebResource resource = publicHtmlFiles.findPublicHtmlResource(fileName)
+ .map(Resource::asWebResource)
+ .orElse(null);
+ if (resource == null) return null;
+
+ byte[] content = resource.asBytes();
+ ResponseBuilder responseBuilder = Response.builder()
+ .setMimeType(mimeType)
+ .setContent(content)
+ .setStatus(200);
+
+ if (fileName.contains(STATIC_BUNDLE_FOLDER)) {
+ resource.getLastModified().ifPresent(lastModified -> responseBuilder
+ // Can't cache css bundles in browser since base path might change
+ .setHeader(HttpHeader.CACHE_CONTROL.asString(), CacheStrategy.CHECK_ETAG)
+ .setHeader(HttpHeader.LAST_MODIFIED.asString(), httpLastModifiedFormatter.apply(lastModified))
+ .setHeader(HttpHeader.ETAG.asString(), lastModified));
+ }
+ return responseBuilder.build();
+ } catch (UncheckedIOException e) {
+ return notFound404("CSS File not found");
+ }
+ }
+
public Response redirectResponse(String location) {
return Response.builder().redirectTo(location).build();
}
@@ -405,49 +463,6 @@ public Response notFound404(String message) {
}
}
- public Response basicAuthFail(WebUserAuthException e) {
- try {
- FailReason failReason = e.getFailReason();
- String reason = failReason.getReason();
- if (failReason == FailReason.ERROR) {
- StringBuilder errorBuilder = new StringBuilder("
");
- for (String line : getStackTrace(e.getCause())) {
- errorBuilder.append(line);
- }
- errorBuilder.append("
");
-
- reason += errorBuilder.toString();
- }
- return Response.builder()
- .setMimeType(MimeType.HTML)
- .setContent(pageFactory.errorPage(Icon.called("lock").build(), "401 Unauthorized", "Authentication Failed.Reason: " + reason + "
").toHtml())
- .setStatus(401)
- .setHeader("WWW-Authenticate", "Basic realm=\"" + failReason.getReason() + "\"")
- .build();
- } catch (IOException jarReadFailed) {
- return forInternalError(e, "Failed to generate PromptAuthorizationResponse");
- }
- }
-
- private List getStackTrace(Throwable throwable) {
- List stackTrace = new ArrayList<>();
- stackTrace.add(throwable.toString());
- for (StackTraceElement element : throwable.getStackTrace()) {
- stackTrace.add(" " + element.toString());
- }
-
- Throwable cause = throwable.getCause();
- if (cause != null) {
- List causeTrace = getStackTrace(cause);
- if (!causeTrace.isEmpty()) {
- causeTrace.set(0, "Caused by: " + causeTrace.get(0));
- stackTrace.addAll(causeTrace);
- }
- }
-
- return stackTrace;
- }
-
public Response forbidden403() {
return forbidden403("Your user is not authorized to view this page.
"
+ "If you believe this is an error contact staff to change your access level.");
@@ -485,23 +500,6 @@ public Response ipWhitelist403(@Untrusted String accessor) {
.build();
}
- public Response basicAuth() {
- try {
- String tips = "
- Ensure you have registered a user with /plan register
"
- + "- Check that the username and password are correct
"
- + "- Username and password are case-sensitive
"
- + "
If you have forgotten your password, ask a staff member to delete your old user and re-register.";
- return Response.builder()
- .setMimeType(MimeType.HTML)
- .setContent(pageFactory.errorPage(Icon.called("lock").build(), "401 Unauthorized", "Authentication Failed." + tips).toHtml())
- .setStatus(401)
- .setHeader("WWW-Authenticate", "Basic realm=\"Plan WebUser (/plan register)\"")
- .build();
- } catch (IOException e) {
- return forInternalError(e, "Failed to generate PromptAuthorizationResponse");
- }
- }
-
public Response badRequest(String errorMessage, String target) {
return Response.builder()
.setMimeType(MimeType.JSON)
@@ -528,7 +526,7 @@ public Response loginPageResponse(@Untrusted Request request) {
try {
return forPage(request, pageFactory.loginPage());
} catch (IOException e) {
- return forInternalError(e, "Failed to generate player page");
+ return forInternalError(e, "Failed to generate login page");
}
}
@@ -536,7 +534,7 @@ public Response registerPageResponse(@Untrusted Request request) {
try {
return forPage(request, pageFactory.registerPage());
} catch (IOException e) {
- return forInternalError(e, "Failed to generate player page");
+ return forInternalError(e, "Failed to generate register page");
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseResolver.java
index d6e67278c2..16c78e64ad 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseResolver.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/ResponseResolver.java
@@ -86,6 +86,7 @@ public class ResponseResolver {
private final ResolverService resolverService;
private final ResponseFactory responseFactory;
private final Lazy webServer;
+ private final PublicHtmlResolver publicHtmlResolver;
@Inject
public ResponseResolver(
@@ -100,6 +101,7 @@ public ResponseResolver(
RootPageResolver rootPageResolver,
RootJSONResolver rootJSONResolver,
StaticResourceResolver staticResourceResolver,
+ PublicHtmlResolver publicHtmlResolver,
LoginPageResolver loginPageResolver,
RegisterPageResolver registerPageResolver,
@@ -123,6 +125,7 @@ public ResponseResolver(
this.rootPageResolver = rootPageResolver;
this.rootJSONResolver = rootJSONResolver;
this.staticResourceResolver = staticResourceResolver;
+ this.publicHtmlResolver = publicHtmlResolver;
this.loginPageResolver = loginPageResolver;
this.registerPageResolver = registerPageResolver;
this.loginResolver = loginResolver;
@@ -157,6 +160,7 @@ public void registerPages() {
resolverService.registerResolverForMatches(plugin, Pattern.compile("^/$"), rootPageResolver);
resolverService.registerResolverForMatches(plugin, Pattern.compile(StaticResourceResolver.PATH_REGEX), staticResourceResolver);
+ resolverService.registerResolverForMatches(plugin, Pattern.compile(".*"), publicHtmlResolver);
resolverService.registerResolver(plugin, "/v1", rootJSONResolver.getResolver());
resolverService.registerResolver(plugin, "/docs/swagger.json", swaggerJsonResolver);
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/PublicHtmlResolver.java b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/PublicHtmlResolver.java
new file mode 100644
index 0000000000..3b657ec325
--- /dev/null
+++ b/Plan/common/src/main/java/com/djrapitops/plan/delivery/webserver/resolver/PublicHtmlResolver.java
@@ -0,0 +1,105 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.webserver.resolver;
+
+import com.djrapitops.plan.delivery.web.resolver.MimeType;
+import com.djrapitops.plan.delivery.web.resolver.NoAuthResolver;
+import com.djrapitops.plan.delivery.web.resolver.Response;
+import com.djrapitops.plan.delivery.web.resolver.request.Request;
+import com.djrapitops.plan.delivery.webserver.ResponseFactory;
+import com.djrapitops.plan.identification.Identifiers;
+import com.djrapitops.plan.utilities.dev.Untrusted;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.util.Optional;
+
+/**
+ * Resolves any files in public_html folder.
+ *
+ * @author AuroraLS3
+ */
+@Singleton
+public class PublicHtmlResolver implements NoAuthResolver {
+
+ private final ResponseFactory responseFactory;
+
+ @Inject
+ public PublicHtmlResolver(ResponseFactory responseFactory) {
+ this.responseFactory = responseFactory;
+ }
+
+ @Override
+ public Optional resolve(Request request) {
+ return Optional.ofNullable(getResponse(request));
+ }
+
+ @SuppressWarnings("OptionalIsPresent") // More readable
+ private Response getResponse(Request request) {
+ @Untrusted String resource = request.getPath().asString().substring(1);
+ @Untrusted Optional etag = Identifiers.getEtag(request);
+
+ Optional mimeType = getMimeType(resource);
+ if (mimeType.isEmpty()) return null;
+
+ return etag.map(tag -> responseFactory.publicHtmlResourceResponse(tag, resource, mimeType.get()))
+ .orElseGet(() -> responseFactory.publicHtmlResourceResponse(resource, mimeType.get()));
+ }
+
+ private Optional getMimeType(@Untrusted String resource) {
+ return Optional.ofNullable(getNullableMimeType(resource));
+ }
+
+ // Checkstyle.OFF: CyclomaticComplexity
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
+ private String getNullableMimeType(@Untrusted String resource) {
+ if (resource.endsWith(".avif")) return "image/avif";
+ if (resource.endsWith(".bin")) return "application/octet-stream";
+ if (resource.endsWith(".bmp")) return "image/bmp";
+ if (resource.endsWith(".css")) return MimeType.CSS;
+ if (resource.endsWith(".csv")) return "text/csv";
+ if (resource.endsWith(".eot")) return MimeType.FONT_BYTESTREAM;
+ if (resource.endsWith(".gif")) return MimeType.IMAGE;
+ if (resource.endsWith(".html")) return MimeType.HTML;
+ if (resource.endsWith(".htm")) return MimeType.HTML;
+ if (resource.endsWith(".ico")) return "image/vnd.microsoft.icon";
+ if (resource.endsWith(".ics")) return "text/calendar";
+ if (resource.endsWith(".js")) return MimeType.JS;
+ if (resource.endsWith(".jpeg")) return MimeType.IMAGE;
+ if (resource.endsWith(".jpg")) return MimeType.IMAGE;
+ if (resource.endsWith(".json")) return MimeType.JSON;
+ if (resource.endsWith(".jsonld")) return "application/ld+json";
+ if (resource.endsWith(".mjs")) return MimeType.JS;
+ if (resource.endsWith(".otf")) return MimeType.FONT_BYTESTREAM;
+ if (resource.endsWith(".pdf")) return "application/pdf";
+ if (resource.endsWith(".php")) return "application/x-httpd-php";
+ if (resource.endsWith(".png")) return MimeType.IMAGE;
+ if (resource.endsWith(".rtf")) return "application/rtf";
+ if (resource.endsWith(".svg")) return "image/svg+xml";
+ if (resource.endsWith(".tif")) return "image/tiff";
+ if (resource.endsWith(".tiff")) return "image/tiff";
+ if (resource.endsWith(".ttf")) return "text/plain";
+ if (resource.endsWith(".txt")) return "text/plain";
+ if (resource.endsWith(".woff")) return MimeType.FONT_BYTESTREAM;
+ if (resource.endsWith(".woff2")) return MimeType.FONT_BYTESTREAM;
+ if (resource.endsWith(".xml")) return "application/xml";
+
+ return null;
+ }
+ // Checkstyle.ON: CyclomaticComplexity
+
+}
\ No newline at end of file
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/config/ResourceSettings.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/config/ResourceSettings.java
index dedf4d6e26..459b697513 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/settings/config/ResourceSettings.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/config/ResourceSettings.java
@@ -18,6 +18,7 @@
import com.djrapitops.plan.settings.config.paths.CustomizedFileSettings;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
+import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import com.djrapitops.plan.storage.file.PlanFiles;
import com.djrapitops.plan.utilities.dev.Untrusted;
import org.apache.commons.lang3.StringUtils;
@@ -72,9 +73,16 @@ public ConfigNode getCustomizationConfigNode() {
}
public Path getCustomizationDirectory() {
- Path exportDirectory = Paths.get(config.get(CustomizedFileSettings.PATH));
- return exportDirectory.isAbsolute()
- ? exportDirectory
- : files.getDataDirectory().resolve(exportDirectory);
+ Path customizationDirectory = Paths.get(config.get(CustomizedFileSettings.PATH));
+ return customizationDirectory.isAbsolute()
+ ? customizationDirectory
+ : files.getDataDirectory().resolve(customizationDirectory);
+ }
+
+ public Path getPublicHtmlDirectory() {
+ Path customizationDirectory = Paths.get(config.get(WebserverSettings.PUBLIC_HTML_PATH));
+ return customizationDirectory.isAbsolute()
+ ? customizationDirectory
+ : files.getDataDirectory().resolve(customizationDirectory);
}
}
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/settings/config/paths/WebserverSettings.java b/Plan/common/src/main/java/com/djrapitops/plan/settings/config/paths/WebserverSettings.java
index 7b3233e776..285f538274 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/settings/config/paths/WebserverSettings.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/settings/config/paths/WebserverSettings.java
@@ -44,6 +44,7 @@ public class WebserverSettings {
public static final Setting DISABLED_AUTHENTICATION = new BooleanSetting("Webserver.Security.Disable_authentication");
public static final Setting LOG_ACCESS_TO_CONSOLE = new BooleanSetting("Webserver.Security.Access_log.Print_to_console");
public static final Setting EXTERNAL_LINK = new StringSetting("Webserver.External_Webserver_address");
+ public static final Setting PUBLIC_HTML_PATH = new StringSetting("Webserver.Public_html_directory");
public static final Setting REDUCED_REFRESH_BARRIER = new TimeSetting("Webserver.Cache.Reduced_refresh_barrier");
public static final Setting INVALIDATE_QUERY_RESULTS = new TimeSetting("Webserver.Cache.Invalidate_query_results_on_disk_after");
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/file/PlanFiles.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/file/PlanFiles.java
index fe82efef1a..6f661cc75e 100644
--- a/Plan/common/src/main/java/com/djrapitops/plan/storage/file/PlanFiles.java
+++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/file/PlanFiles.java
@@ -19,8 +19,6 @@
import com.djrapitops.plan.SubSystem;
import com.djrapitops.plan.delivery.web.AssetVersions;
import com.djrapitops.plan.exceptions.EnableException;
-import com.djrapitops.plan.settings.config.PlanConfig;
-import com.djrapitops.plan.settings.config.paths.CustomizedFileSettings;
import com.djrapitops.plan.utilities.dev.Untrusted;
import dagger.Lazy;
import org.apache.commons.lang3.StringUtils;
@@ -50,19 +48,16 @@ public class PlanFiles implements SubSystem {
private final File configFile;
private final Lazy assetVersions;
- private final Lazy config;
@Inject
public PlanFiles(
@Named("dataFolder") File dataFolder,
JarResource.StreamFunction getResourceStream,
- Lazy assetVersions,
- Lazy config
+ Lazy assetVersions
) {
this.dataFolder = dataFolder;
this.getResourceStream = getResourceStream;
this.assetVersions = assetVersions;
- this.config = config;
this.configFile = getFileFromPluginFolder("config.yml");
}
@@ -150,28 +145,7 @@ public Resource getResourceFromPluginFolder(String resourceName) {
return new FileResource(resourceName, getFileFromPluginFolder(resourceName));
}
- // TODO Customized file logic should be moved to another class so the circular dependency on config can be removed.
- public Optional getCustomizableResource(@Untrusted String resourceName) {
- return Optional.ofNullable(findCustomized(resourceName));
- }
-
- private Resource findCustomized(@Untrusted String resourceName) {
- if (config.get().isTrue(CustomizedFileSettings.WEB_DEV_MODE)) {
- // Bypass cache in web developer mode.
- return getFileResource(resourceName);
- } else {
- return ResourceCache.getOrCache(resourceName, () -> getFileResource(resourceName));
- }
- }
-
- private FileResource getFileResource(@Untrusted String resourceName) {
- return attemptToFind(resourceName)
- .map(found -> new FileResource(resourceName, found))
- .orElse(null);
- }
-
- public Optional attemptToFind(@Untrusted String resourceName) {
- Path dir = config.get().getResourceSettings().getCustomizationDirectory();
+ public Optional attemptToFind(Path dir, @Untrusted String resourceName) {
if (dir.toFile().exists() && dir.toFile().isDirectory()) {
// Path may be absolute due to resolving untrusted path
@Untrusted Path asPath = dir.resolve(resourceName);
diff --git a/Plan/common/src/main/java/com/djrapitops/plan/storage/file/PublicHtmlFiles.java b/Plan/common/src/main/java/com/djrapitops/plan/storage/file/PublicHtmlFiles.java
new file mode 100644
index 0000000000..948e430fe3
--- /dev/null
+++ b/Plan/common/src/main/java/com/djrapitops/plan/storage/file/PublicHtmlFiles.java
@@ -0,0 +1,88 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.storage.file;
+
+import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
+import com.djrapitops.plan.settings.config.PlanConfig;
+import com.djrapitops.plan.settings.config.paths.WebserverSettings;
+import com.djrapitops.plan.utilities.dev.Untrusted;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.util.Optional;
+
+/**
+ * Access to public_html folder and its contents.
+ *
+ * @author AuroraLS3
+ */
+@Singleton
+public class PublicHtmlFiles {
+
+ private final PlanConfig config;
+
+ @Inject
+ public PublicHtmlFiles(PlanConfig config) {
+ this.config = config;
+ }
+
+ public Optional findCustomizedResource(@Untrusted String resourceName) {
+ Path customizationDirectory = config.getResourceSettings().getCustomizationDirectory();
+ return attemptToFind(customizationDirectory, resourceName)
+ .map(found -> new FileResource(resourceName, found));
+ }
+
+ public Optional findPublicHtmlResource(@Untrusted String resourceName) {
+ Path publicHtmlDirectory = config.getResourceSettings().getPublicHtmlDirectory();
+ return attemptToFind(publicHtmlDirectory, resourceName)
+ .map(found -> new FileResource(resourceName, found));
+ }
+
+ private Optional attemptToFind(Path from, @Untrusted String resourceName) {
+ if (!Files.exists(from)) {
+ try {
+ Files.createDirectories(from);
+ } catch (IOException e) {
+ throw new UncheckedIOException("Could not create folder configured in '" + WebserverSettings.PUBLIC_HTML_PATH.getPath() + "'-setting, please create it manually.", e);
+ }
+ }
+ if (from.toFile().exists() && from.toFile().isDirectory()) {
+ @Untrusted Path asPath;
+ try {
+ asPath = from.resolve(resourceName).normalize();
+ } catch (InvalidPathException badCharacter) {
+ throw new BadRequestException("Requested resource name contained a bad character.");
+ }
+ // Path may be absolute due to resolving untrusted path
+ if (!asPath.startsWith(from)) {
+ return Optional.empty();
+ }
+ // Now it should be trustworthy
+ File found = asPath.toFile();
+ if (found.exists()) {
+ return Optional.of(found);
+ }
+ }
+ return Optional.empty();
+ }
+}
diff --git a/Plan/common/src/main/resources/assets/plan/bungeeconfig.yml b/Plan/common/src/main/resources/assets/plan/bungeeconfig.yml
index 997f86b3fa..acaba2af87 100644
--- a/Plan/common/src/main/resources/assets/plan/bungeeconfig.yml
+++ b/Plan/common/src/main/resources/assets/plan/bungeeconfig.yml
@@ -43,22 +43,13 @@ Webserver:
Enabled: false
# %port% is replaced automatically with Webserver.Port
Address: your.domain.here:%port%
- # InternalIP usually does not need to be changed, only change it if you know what you're doing!
+ # Internal IP usually does not need to be changed, only change it if you know what you're doing!
# 0.0.0.0 allocates Internal (local) IP automatically for the WebServer.
Internal_IP: 0.0.0.0
- Cache:
- Reduced_refresh_barrier:
- Time: 15
- Unit: SECONDS
- Invalidate_query_results_on_disk_after:
- Time: 7
- Unit: DAYS
- Invalidate_disk_cache_after:
- Time: 2
- Unit: DAYS
- Invalidate_memory_cache_after:
- Time: 5
- Unit: MINUTES
+ # Use absolute path ("C:\Example\Path", "/var/example/path") or relative ("public_html" -> {server}/plugins/Plan/public_html)
+ # NOTE: All files in this directory can be read by anyone who can access the webserver.
+ # This can be used to host certbot http challenge file, or for customizing Plan React-bundle
+ Public_html_directory: "public_html"
Security:
SSL_certificate:
KeyStore_path: Cert.jks
@@ -88,6 +79,19 @@ Webserver:
Unit: HOURS
Disable_Webserver: false
External_Webserver_address: "https://www.example.address"
+ Cache:
+ Reduced_refresh_barrier:
+ Time: 15
+ Unit: SECONDS
+ Invalidate_query_results_on_disk_after:
+ Time: 7
+ Unit: DAYS
+ Invalidate_disk_cache_after:
+ Time: 2
+ Unit: DAYS
+ Invalidate_memory_cache_after:
+ Time: 5
+ Unit: MINUTES
# -----------------------------------------------------
Data_gathering:
Geolocations: true
diff --git a/Plan/common/src/main/resources/assets/plan/config.yml b/Plan/common/src/main/resources/assets/plan/config.yml
index 12794f214a..6f4e97daeb 100644
--- a/Plan/common/src/main/resources/assets/plan/config.yml
+++ b/Plan/common/src/main/resources/assets/plan/config.yml
@@ -45,22 +45,13 @@ Webserver:
Enabled: false
# %port% is replaced automatically with Webserver.Port
Address: your.domain.here:%port%
- # InternalIP usually does not need to be changed, only change it if you know what you're doing!
+ # Internal IP usually does not need to be changed, only change it if you know what you're doing!
# 0.0.0.0 allocates Internal (local) IP automatically for the WebServer.
Internal_IP: 0.0.0.0
- Cache:
- Reduced_refresh_barrier:
- Time: 15
- Unit: SECONDS
- Invalidate_query_results_on_disk_after:
- Time: 7
- Unit: DAYS
- Invalidate_disk_cache_after:
- Time: 2
- Unit: DAYS
- Invalidate_memory_cache_after:
- Time: 5
- Unit: MINUTES
+ # Use absolute path ("C:\Example\Path", "/var/example/path") or relative ("public_html" -> {server}/plugins/Plan/public_html)
+ # NOTE: All files in this directory can be read by anyone who can access the webserver.
+ # This can be used to host certbot http challenge file, or for customizing Plan React-bundle
+ Public_html_directory: "public_html"
Security:
SSL_certificate:
KeyStore_path: Cert.jks
@@ -90,6 +81,19 @@ Webserver:
Unit: HOURS
Disable_Webserver: false
External_Webserver_address: https://www.example.address
+ Cache:
+ Reduced_refresh_barrier:
+ Time: 15
+ Unit: SECONDS
+ Invalidate_query_results_on_disk_after:
+ Time: 7
+ Unit: DAYS
+ Invalidate_disk_cache_after:
+ Time: 2
+ Unit: DAYS
+ Invalidate_memory_cache_after:
+ Time: 5
+ Unit: MINUTES
# -----------------------------------------------------
Data_gathering:
Geolocations: true
diff --git a/Plan/common/src/test/java/com/djrapitops/plan/storage/file/PlanFilesTest.java b/Plan/common/src/test/java/com/djrapitops/plan/storage/file/PlanFilesTest.java
index 72f49d3506..9fbd9a0541 100644
--- a/Plan/common/src/test/java/com/djrapitops/plan/storage/file/PlanFilesTest.java
+++ b/Plan/common/src/test/java/com/djrapitops/plan/storage/file/PlanFilesTest.java
@@ -16,8 +16,6 @@
*/
package com.djrapitops.plan.storage.file;
-import com.djrapitops.plan.settings.config.PlanConfig;
-import com.djrapitops.plan.settings.config.paths.CustomizedFileSettings;
import extension.FullSystemExtension;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -28,10 +26,8 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* @author AuroraLS3
@@ -49,17 +45,4 @@ void getFileFromPluginFolderDoesNotAllowAbsolutePathTraversal(@TempDir Path temp
File file = files.getFileFromPluginFolder(testFile.toFile().getAbsolutePath());
assertNotEquals(testFile.toFile().getAbsolutePath(), file.getAbsolutePath());
}
-
- @Test
- @DisplayName("getCustomizableResource has no Path Traversal vulnerability")
- void getCustomizableResourceDoesNotAllowAbsolutePathTraversal(@TempDir Path tempDir, PlanConfig config, PlanFiles files) throws IOException {
- config.set(CustomizedFileSettings.PATH, tempDir.resolve("customized").toFile().getAbsolutePath());
-
- Path testFile = tempDir.resolve("file.db");
- Files.createDirectories(tempDir.getParent());
- Files.createFile(testFile);
-
- Optional resource = files.getCustomizableResource(testFile.toFile().getAbsolutePath());
- assertTrue(resource.isEmpty());
- }
}
\ No newline at end of file
diff --git a/Plan/common/src/test/java/com/djrapitops/plan/storage/file/PublicHtmlFilesTest.java b/Plan/common/src/test/java/com/djrapitops/plan/storage/file/PublicHtmlFilesTest.java
new file mode 100644
index 0000000000..0bc3756bed
--- /dev/null
+++ b/Plan/common/src/test/java/com/djrapitops/plan/storage/file/PublicHtmlFilesTest.java
@@ -0,0 +1,100 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.storage.file;
+
+import com.djrapitops.plan.settings.config.PlanConfig;
+import com.djrapitops.plan.settings.config.paths.CustomizedFileSettings;
+import com.djrapitops.plan.settings.config.paths.WebserverSettings;
+import extension.FullSystemExtension;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @author AuroraLS3
+ */
+@ExtendWith(FullSystemExtension.class)
+class PublicHtmlFilesTest {
+
+ @Test
+ @DisplayName("findCustomizedResource has no Path Traversal vulnerability")
+ void getCustomizableResourceDoesNotAllowAbsolutePathTraversal(@TempDir Path tempDir, PlanConfig config, PublicHtmlFiles files) throws IOException {
+ Path directory = tempDir.resolve("customized");
+ config.set(CustomizedFileSettings.PATH, directory.toFile().getAbsolutePath());
+
+ Path testFile = tempDir.resolve("file.db");
+ Files.createDirectories(directory);
+ Files.createDirectories(testFile.getParent());
+ Files.createFile(testFile);
+
+ Optional resource = files.findCustomizedResource(testFile.toFile().getAbsolutePath());
+ assertTrue(resource.isEmpty());
+ }
+
+ @Test
+ @DisplayName("findPublicHtmlResource has no Path Traversal vulnerability")
+ void findPublicHtmlResourceDoesNotAllowAbsolutePathTraversal(@TempDir Path tempDir, PlanConfig config, PublicHtmlFiles files) throws IOException {
+ Path directory = tempDir.resolve("public_html");
+ config.set(WebserverSettings.PUBLIC_HTML_PATH, directory.toFile().getAbsolutePath());
+
+ Path testFile = tempDir.resolve("file.db");
+ Files.createDirectories(directory);
+ Files.createDirectories(testFile.getParent());
+ Files.createFile(testFile);
+
+ Optional resource = files.findPublicHtmlResource(testFile.toFile().getAbsolutePath());
+ assertTrue(resource.isEmpty());
+ }
+
+ @Test
+ @DisplayName("findCustomizedResource finds File")
+ void findCustomizedResourceFindsFile(@TempDir Path tempDir, PlanConfig config, PublicHtmlFiles files) throws IOException {
+ Path directory = tempDir.resolve("customized");
+ config.set(CustomizedFileSettings.PATH, directory.toFile().getAbsolutePath());
+
+ Path testFile = directory.resolve("file.db");
+ Files.createDirectories(directory);
+ Files.createDirectories(testFile.getParent());
+ Files.createFile(testFile);
+
+ Optional resource = files.findCustomizedResource("file.db");
+ assertTrue(resource.isPresent());
+ }
+
+ @Test
+ @DisplayName("findPublicHtmlResource finds File")
+ void findPublicHtmlResourceFindsFile(@TempDir Path tempDir, PlanConfig config, PublicHtmlFiles files) throws IOException {
+ Path directory = tempDir.resolve("public_html");
+ config.set(WebserverSettings.PUBLIC_HTML_PATH, directory.toFile().getAbsolutePath());
+
+ Path testFile = directory.resolve("file.db");
+ Files.createDirectories(directory);
+ Files.createDirectories(testFile.getParent());
+ Files.createFile(testFile);
+
+ Optional resource = files.findPublicHtmlResource("file.db");
+ assertTrue(resource.isPresent());
+ }
+}
\ No newline at end of file
diff --git a/Plan/common/src/test/java/extension/FullSystemExtension.java b/Plan/common/src/test/java/extension/FullSystemExtension.java
index 228f9c1568..161d26bab5 100644
--- a/Plan/common/src/test/java/extension/FullSystemExtension.java
+++ b/Plan/common/src/test/java/extension/FullSystemExtension.java
@@ -18,12 +18,14 @@
import com.djrapitops.plan.PlanSystem;
import com.djrapitops.plan.commands.PlanCommand;
+import com.djrapitops.plan.delivery.DeliveryUtilities;
import com.djrapitops.plan.delivery.export.Exporter;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.file.PlanFiles;
+import com.djrapitops.plan.storage.file.PublicHtmlFiles;
import org.junit.jupiter.api.extension.*;
import utilities.RandomData;
import utilities.dagger.PlanPluginComponent;
@@ -86,6 +88,8 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon
PlanPluginComponent.class.equals(type) ||
PlanCommand.class.equals(type) ||
Database.class.equals(type) ||
+ DeliveryUtilities.class.equals(type) ||
+ PublicHtmlFiles.class.equals(type) ||
Exporter.class.equals(type);
}
@@ -121,6 +125,12 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte
if (Database.class.equals(type)) {
return planSystem.getDatabaseSystem().getDatabase();
}
+ if (DeliveryUtilities.class.equals(type)) {
+ return planSystem.getDeliveryUtilities();
+ }
+ if (PublicHtmlFiles.class.equals(type)) {
+ return planSystem.getDeliveryUtilities().getPublicHtmlFiles();
+ }
if (Exporter.class.equals(type)) {
return planSystem.getExportSystem().getExporter();
}
diff --git a/Plan/gradle/wrapper/gradle-wrapper.jar b/Plan/gradle/wrapper/gradle-wrapper.jar
index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..943f0cbfa754578e88a3dae77fce6e3dea56edbf 100644
GIT binary patch
delta 36900
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
Il)tb9
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