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 b83f0ffb8..62b2a1ea5 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 4d906838c..28f1b23ee 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 @@ public final class URIQuery { ); } 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 9e947a1a4..5a0507d65 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 688198b9d..440acf5b0 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 @@ package com.djrapitops.plan.delivery; 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 class DeliveryUtilities { 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 1b52e66b6..7992fdbe2 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.Database; 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 class PageFactory { ) { this.versionChecker = versionChecker; this.files = files; + this.publicHtmlFiles = publicHtmlFiles; this.config = config; this.theme = theme; this.dbSystem = dbSystem; @@ -106,7 +109,8 @@ public class PageFactory { } 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 class PageFactory { 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 7b56d95d6..0db799497 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 @@ import java.util.regex.Pattern; @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 class ResolverSvc implements ResolverService { 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 9efa6f387..5e5afe5ad 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.PlanConfig; 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 @@ import java.util.function.Supplier; 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 class ResourceSvc implements ResourceService { } 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 e05cf9a14..0de54d1d8 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.Inject; 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 @@ import java.util.concurrent.TimeUnit; /** * 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 @@ public class WebAssetVersionCheckTask extends TaskSystem.Task { } 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 305b8a030..9a61b1077 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.ResponseBuilder; 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.DBSystem; 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.Inject; 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 class 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 class 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 @@ public class ResponseFactory { } 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 class ResponseFactory { 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 class ResponseFactory { 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 class ResponseFactory { 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 class ResponseFactory { 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 class ResponseFactory { } } + 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 class ResponseFactory { } } - 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 class ResponseFactory { .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 class ResponseFactory { 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 class ResponseFactory { 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 d6e67278c..16c78e64a 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 class ResponseResolver { RootPageResolver rootPageResolver, RootJSONResolver rootJSONResolver, StaticResourceResolver staticResourceResolver, + PublicHtmlResolver publicHtmlResolver, LoginPageResolver loginPageResolver, RegisterPageResolver registerPageResolver, @@ -123,6 +125,7 @@ public class 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 class ResponseResolver { 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 000000000..3b657ec32 --- /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 dedf4d6e2..459b69751 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 @@ package com.djrapitops.plan.settings.config; 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 class ResourceSettings { } 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 7b3233e77..285f53827 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 fe82efef1..6f661cc75 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 @@ package com.djrapitops.plan.storage.file; 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 class PlanFiles implements SubSystem { 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 000000000..948e430fe --- /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 997f86b3f..acaba2af8 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 12794f214..6f4e97dae 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 72f49d350..9fbd9a054 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.File; 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 @@ class PlanFilesTest { 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 000000000..0bc3756be --- /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 228f9c156..161d26bab 100644 --- a/Plan/common/src/test/java/extension/FullSystemExtension.java +++ b/Plan/common/src/test/java/extension/FullSystemExtension.java @@ -18,12 +18,14 @@ package extension; 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 class FullSystemExtension implements ParameterResolver, BeforeAllCallback 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 class FullSystemExtension implements ParameterResolver, BeforeAllCallback 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 7454180f2..943f0cbfa 100644 Binary files a/Plan/gradle/wrapper/gradle-wrapper.jar and b/Plan/gradle/wrapper/gradle-wrapper.jar differ diff --git a/Plan/gradle/wrapper/gradle-wrapper.properties b/Plan/gradle/wrapper/gradle-wrapper.properties index aa991fcea..f398c33c4 100644 --- a/Plan/gradle/wrapper/gradle-wrapper.properties +++ b/Plan/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/Plan/gradlew b/Plan/gradlew index 744e882ed..65dcd68d6 100755 --- a/Plan/gradlew +++ b/Plan/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MSYS* | MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,105 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/Plan/gradlew.bat b/Plan/gradlew.bat index 107acd32c..93e3f59f1 100644 --- a/Plan/gradlew.bat +++ b/Plan/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/Plan/react/buildBundle.bat b/Plan/react/buildBundle.bat new file mode 100644 index 000000000..93b2a7458 --- /dev/null +++ b/Plan/react/buildBundle.bat @@ -0,0 +1,2 @@ +cd .. +./gradlew :common:yarnBundle \ No newline at end of file diff --git a/Plan/react/buildBundle.sh b/Plan/react/buildBundle.sh new file mode 100644 index 000000000..d5ee4ce01 --- /dev/null +++ b/Plan/react/buildBundle.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +cd .. +./gradlew :common:yarnBundle \ No newline at end of file diff --git a/Plan/react/devServer.bat b/Plan/react/devServer.bat new file mode 100644 index 000000000..d90805ebd --- /dev/null +++ b/Plan/react/devServer.bat @@ -0,0 +1,2 @@ +cd .. +./gradlew :common:yarnStart \ No newline at end of file diff --git a/Plan/react/devServer.sh b/Plan/react/devServer.sh new file mode 100644 index 000000000..e5e0f465e --- /dev/null +++ b/Plan/react/devServer.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +cd .. +./gradlew :common:yarnStart \ No newline at end of file diff --git a/Plan/sponge/src/main/java/com/djrapitops/plan/storage/file/SpongePlanFiles.java b/Plan/sponge/src/main/java/com/djrapitops/plan/storage/file/SpongePlanFiles.java index af06611e3..827762b68 100644 --- a/Plan/sponge/src/main/java/com/djrapitops/plan/storage/file/SpongePlanFiles.java +++ b/Plan/sponge/src/main/java/com/djrapitops/plan/storage/file/SpongePlanFiles.java @@ -19,7 +19,6 @@ package com.djrapitops.plan.storage.file; import com.djrapitops.plan.PlanPlugin; import com.djrapitops.plan.PlanSponge; import com.djrapitops.plan.delivery.web.AssetVersions; -import com.djrapitops.plan.settings.config.PlanConfig; import dagger.Lazy; import org.spongepowered.api.Sponge; import org.spongepowered.api.resource.ResourcePath; @@ -46,10 +45,9 @@ public class SpongePlanFiles extends PlanFiles { @Named("dataFolder") File dataFolder, JarResource.StreamFunction getResourceStream, PlanPlugin plugin, - Lazy assetVersions, - Lazy config + Lazy assetVersions ) { - super(dataFolder, getResourceStream, assetVersions, config); + super(dataFolder, getResourceStream, assetVersions); this.plugin = plugin; }