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
This commit is contained in:
Aurora Lahtela 2023-02-05 12:08:29 +02:00 committed by GitHub
parent 413e087c4d
commit 09279cbb66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 690 additions and 287 deletions

View File

@ -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";

View File

@ -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");
}
}

View File

@ -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")

View File

@ -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> formatters;
private final Lazy<Graphs> graphs;
private final Lazy<PublicHtmlFiles> publicHtmlFiles;
@Inject
public DeliveryUtilities(
Lazy<Formatters> formatters,
Lazy<Graphs> graphs
) {
Lazy<Graphs> graphs,
Lazy<PublicHtmlFiles> 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();
}
}

View File

@ -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> versionChecker;
private final Lazy<PlanFiles> files;
private final Lazy<PublicHtmlFiles> publicHtmlFiles;
private final Lazy<PlanConfig> config;
private final Lazy<Theme> theme;
private final Lazy<DBSystem> dbSystem;
@ -73,7 +75,7 @@ public class PageFactory {
public PageFactory(
Lazy<VersionChecker> versionChecker,
Lazy<PlanFiles> files,
Lazy<PlanConfig> config,
Lazy<PublicHtmlFiles> publicHtmlFiles, Lazy<PlanConfig> config,
Lazy<Theme> theme,
Lazy<DBSystem> dbSystem,
Lazy<ServerInfo> 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();

View File

@ -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<Container> basicResolvers;
private final List<Container> 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;
}

View File

@ -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<Snippet> 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<WebResource> source) throws IOException {
Optional<Resource> customizedResource = files.getCustomizableResource(fileName);
Optional<Resource> customizedResource = publicHtmlFiles.findCustomizedResource(fileName);
if (customizedResource.isPresent()) {
return readCustomized(customizedResource.get());
} else {

View File

@ -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<AssetInfo> findOutdatedResource(String resource) {
Optional<File> resourceFile = files.attemptToFind(resource);
Path dir = config.getResourceSettings().getCustomizationDirectory();
Optional<File> resourceFile = files.attemptToFind(dir, resource);
Optional<Long> webAssetVersion = assetVersions.getAssetVersion(resource);
if (resourceFile.isPresent() && webAssetVersion.isPresent() && webAssetVersion.get() > resourceFile.get().lastModified()) {
return Optional.of(new AssetInfo(

View File

@ -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> 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<String, Response> newResponseFunction) {
WebResource resource = getResource(fileName);
WebResource resource = config.isTrue(PluginSettings.FRONTEND_BETA) ? getPublicOrJarResource(fileName) : getResource(fileName);
Optional<Long> 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<Long> 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("</p><pre>");
for (String line : getStackTrace(e.getCause())) {
errorBuilder.append(line);
}
errorBuilder.append("</pre>");
reason += errorBuilder.toString();
}
return Response.builder()
.setMimeType(MimeType.HTML)
.setContent(pageFactory.errorPage(Icon.called("lock").build(), "401 Unauthorized", "Authentication Failed.</p><p><b>Reason: " + reason + "</b></p><p>").toHtml())
.setStatus(401)
.setHeader("WWW-Authenticate", "Basic realm=\"" + failReason.getReason() + "\"")
.build();
} catch (IOException jarReadFailed) {
return forInternalError(e, "Failed to generate PromptAuthorizationResponse");
}
}
private List<String> getStackTrace(Throwable throwable) {
List<String> stackTrace = new ArrayList<>();
stackTrace.add(throwable.toString());
for (StackTraceElement element : throwable.getStackTrace()) {
stackTrace.add(" " + element.toString());
}
Throwable cause = throwable.getCause();
if (cause != null) {
List<String> 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.<br>"
+ "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 = "<br>- Ensure you have registered a user with <b>/plan register</b><br>"
+ "- Check that the username and password are correct<br>"
+ "- Username and password are case-sensitive<br>"
+ "<br>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");
}
}

View File

@ -86,6 +86,7 @@ public class ResponseResolver {
private final ResolverService resolverService;
private final ResponseFactory responseFactory;
private final Lazy<WebServer> 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);

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Response> 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<Long> etag = Identifiers.getEtag(request);
Optional<String> 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<String> 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
}

View File

@ -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);
}
}

View File

@ -44,6 +44,7 @@ public class WebserverSettings {
public static final Setting<Boolean> DISABLED_AUTHENTICATION = new BooleanSetting("Webserver.Security.Disable_authentication");
public static final Setting<Boolean> LOG_ACCESS_TO_CONSOLE = new BooleanSetting("Webserver.Security.Access_log.Print_to_console");
public static final Setting<String> EXTERNAL_LINK = new StringSetting("Webserver.External_Webserver_address");
public static final Setting<String> PUBLIC_HTML_PATH = new StringSetting("Webserver.Public_html_directory");
public static final Setting<Long> REDUCED_REFRESH_BARRIER = new TimeSetting("Webserver.Cache.Reduced_refresh_barrier");
public static final Setting<Long> INVALIDATE_QUERY_RESULTS = new TimeSetting("Webserver.Cache.Invalidate_query_results_on_disk_after");

View File

@ -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> assetVersions;
private final Lazy<PlanConfig> config;
@Inject
public PlanFiles(
@Named("dataFolder") File dataFolder,
JarResource.StreamFunction getResourceStream,
Lazy<AssetVersions> assetVersions,
Lazy<PlanConfig> config
Lazy<AssetVersions> 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<Resource> 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<File> attemptToFind(@Untrusted String resourceName) {
Path dir = config.get().getResourceSettings().getCustomizationDirectory();
public Optional<File> 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);

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Resource> findCustomizedResource(@Untrusted String resourceName) {
Path customizationDirectory = config.getResourceSettings().getCustomizationDirectory();
return attemptToFind(customizationDirectory, resourceName)
.map(found -> new FileResource(resourceName, found));
}
public Optional<Resource> findPublicHtmlResource(@Untrusted String resourceName) {
Path publicHtmlDirectory = config.getResourceSettings().getPublicHtmlDirectory();
return attemptToFind(publicHtmlDirectory, resourceName)
.map(found -> new FileResource(resourceName, found));
}
private Optional<File> 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();
}
}

View File

@ -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

View File

@ -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

View File

@ -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> resource = files.getCustomizableResource(testFile.toFile().getAbsolutePath());
assertTrue(resource.isEmpty());
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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> 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> 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> 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> resource = files.findPublicHtmlResource("file.db");
assertTrue(resource.isPresent());
}
}

View File

@ -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();
}

Binary file not shown.

View File

@ -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

285
Plan/gradlew vendored
View File

@ -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" "$@"

15
Plan/gradlew.bat vendored
View File

@ -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

View File

@ -0,0 +1,2 @@
cd ..
./gradlew :common:yarnBundle

View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
cd ..
./gradlew :common:yarnBundle

2
Plan/react/devServer.bat Normal file
View File

@ -0,0 +1,2 @@
cd ..
./gradlew :common:yarnStart

4
Plan/react/devServer.sh Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
cd ..
./gradlew :common:yarnStart

View File

@ -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> assetVersions,
Lazy<PlanConfig> config
Lazy<AssetVersions> assetVersions
) {
super(dataFolder, getResourceStream, assetVersions, config);
super(dataFolder, getResourceStream, assetVersions);
this.plugin = plugin;
}