Merge branch 'master' into 2233/whitelist-bounce-gathering

# Conflicts:
#	Plan/common/src/main/java/com/djrapitops/plan/storage/database/transactions/init/CreateTablesTransaction.java
This commit is contained in:
Aurora Lahtela 2024-03-09 20:19:28 +02:00
commit 3ebe7a66a1
524 changed files with 8878 additions and 13160 deletions

View File

@ -23,30 +23,16 @@ jobs:
steps:
- name: 📥 Checkout git repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: ☕ Setup JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: 'adopt'
java-version: '17'
- name: 🚦 Setup Selenium Webdriver
uses: nanasess/setup-chromedriver@v2
- name: 🚦 Setup Selenium Webdriver settings
run: |
export DISPLAY=:99
chromedriver --url-base=/wd/hub &
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
- name: 📶 Verify MariaDB connection
env:
PORT: ${{ job.services.mariadb.ports[3306] }}
run: |
while ! mysqladmin ping -h"127.0.0.1" -P"$PORT" --silent; do
sleep 1
done
- name: 💼 Load Gradle Cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
@ -68,15 +54,29 @@ jobs:
echo "versionString=$(cat build/versions/jar.txt)" >> $GITHUB_ENV
echo "artifactPath=$(pwd)/builds" >> $GITHUB_ENV
- name: 📤 Upload Plan.jar
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: Plan-${{ env.versionString }}-${{ env.git_hash }}.jar
path: ${{ env.artifactPath }}/Plan-${{ env.snapshotVersion }}.jar
- name: 📤 Upload PlanFabric.jar
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: PlanFabric-${{ env.versionString }}-${{ env.git_hash }}.jar
path: ${{ env.artifactPath }}/PlanFabric-${{ env.snapshotVersion }}.jar
- name: 🚦 Setup Selenium Webdriver
uses: nanasess/setup-chromedriver@v2
- name: 🚦 Setup Selenium Webdriver settings
run: |
export DISPLAY=:99
chromedriver --url-base=/wd/hub &
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
- name: 📶 Verify MariaDB connection
env:
PORT: ${{ job.services.mariadb.ports[3306] }}
run: |
while ! mysqladmin ping -h"127.0.0.1" -P"$PORT" --silent; do
sleep 1
done
- name: 🩺 Test
env:
MYSQL_DB: test

View File

@ -64,6 +64,9 @@ public interface CapabilityService {
return Capability.getByName(capabilityName).isPresent();
}
/**
* Singleton holder for listeners.
*/
class ListHolder {
static final AtomicReference<List<Consumer<Boolean>>> enableListeners = new AtomicReference<>(
new CopyOnWriteArrayList<>()

View File

@ -89,7 +89,7 @@ public interface ComponentService {
* Converts the given input into a {@link Component}.
* Input example {@code §ctext}.
*
* @param legacy the input legacy
* @param legacy the input legacy
* @param character the character to use as the color prefix, usually {@code §}.
* @return a {@link Component}
* @see #fromLegacy(String)
@ -115,7 +115,7 @@ public interface ComponentService {
* Input example: {@code &#rrggbbtext}.
*
* @param adventureLegacy the input adventure legacy
* @param character the character to use as the color prefix, usually {@code &}.
* @param character the character to use as the color prefix, usually {@code &}.
* @return a {@link Component}
* @see #fromAdventureLegacy(String)
* @see Component#SECTION
@ -139,7 +139,7 @@ public interface ComponentService {
* Input example: {@code §x§r§r§g§g§b§btext}.
*
* @param bungeeLegacy the input bungee legacy
* @param character the character to use as the color prefix, usually {@code §}.
* @param character the character to use as the color prefix, usually {@code §}.
* @return a {@link Component}
* @see Component#SECTION
* @see Component#AMPERSAND
@ -164,6 +164,9 @@ public interface ComponentService {
*/
Component fromJson(String json);
/**
* Singleton holder for ComponentService.
*/
class Holder {
static final AtomicReference<ComponentService> service = new AtomicReference<>();

View File

@ -1,6 +1,5 @@
/**
* PageExtension API for extending the webserver and the website.
*
* <a href="https://github.com/plan-player-analytics/Plan/wiki/APIv5-PageExtension-API">Documentation</a>
*/
package com.djrapitops.plan.delivery.web;

View File

@ -46,6 +46,11 @@ public final class CompositeResolver implements Resolver {
this.resolvers = new ArrayList<>();
}
/**
* Create a new builder for a .
*
* @return a builder.
*/
public static CompositeResolver.Builder builder() {
return new Builder();
}
@ -100,6 +105,9 @@ public final class CompositeResolver implements Resolver {
return getResolver(forThis.getPath()).map(resolver -> resolver.requiresAuth(forThis)).orElse(true);
}
/**
* Builder class for {@link CompositeResolver}.
*/
public static class Builder {
private final CompositeResolver composite;
@ -132,6 +140,11 @@ public final class CompositeResolver implements Resolver {
return this;
}
/**
* Build the final result after adding all resolvers.
*
* @return The {@link CompositeResolver}
*/
public CompositeResolver build() {
return composite;
}

View File

@ -22,11 +22,20 @@ import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* Utility class for constructing a {@link Resolver} with Functional Interfaces.
*/
public class FunctionalResolverWrapper implements Resolver {
private final Function<Request, Optional<Response>> resolver;
private final Predicate<Request> accessCheck;
/**
* Default constructor.
*
* @param resolver Function that solves the {@link Request} into an Optional {@link Response}.
* @param accessCheck Predicate that checks if {@link Request} is allowed or if 403 Forbidden should be given.
*/
public FunctionalResolverWrapper(Function<Request, Optional<Response>> resolver, Predicate<Request> accessCheck) {
this.resolver = resolver;
this.accessCheck = accessCheck;

View File

@ -19,7 +19,9 @@ package com.djrapitops.plan.delivery.web.resolver;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
/**
* Interface for resolving requests of Plan webserver.
@ -40,6 +42,25 @@ public interface Resolver {
*/
boolean canAccess(Request request);
/**
* Override this to tell Plan what web permissions this endpoint uses.
* <p>
* This allows:
* <ul>
* <li>Plan to store these permissions in the permission list</li>
* <li>Users can grant/deny the permission for a group</li>
* <li>Plan can show what endpoints specific permission gives access to</li>
* </ul>
* <p>
* Requires PAGE_EXTENSION_USER_PERMISSIONS capability
*
* @return Set of permissions eg. [plugin.custom.permission, plugin.custom.permission.child.node]
* @see com.djrapitops.plan.capability.CapabilityService for Capability checks
*/
default Set<String> usedWebPermissions() {
return new HashSet<>();
}
/**
* Implement request resolution.
*
@ -51,10 +72,21 @@ public interface Resolver {
*/
Optional<Response> resolve(Request request);
/**
* Creates a new {@link ResponseBuilder} for a {@link Response}.
*
* @return a new builder.
*/
default ResponseBuilder newResponseBuilder() {
return Response.builder();
}
/**
* Used to check if the resolver requires authentication to be used.
*
* @param request Incoming request that you can use to figure out if authentication is required.
* @return true if you want 401 to be given when user has not logged in.
*/
default boolean requiresAuth(Request request) {
return true;
}

View File

@ -33,7 +33,7 @@ public class ResponseBuilder {
/**
* Set MIME Type of the Response.
*
* @param mimeType MIME type of the Response https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
* @param mimeType MIME type of the Response <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types">Documentation</a>
* @return this builder.
* @see MimeType for common MIME types.
*/
@ -46,7 +46,7 @@ public class ResponseBuilder {
* <p>
* Default status code is 200 (OK) if not set.
*
* @param code 1xx, 2xx, 3xx, 4xx, 5xx, https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
* @param code 1xx, 2xx, 3xx, 4xx, 5xx, <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status">Documentation</a>
* @return this builder.
*/
public ResponseBuilder setStatus(int code) {
@ -57,7 +57,7 @@ public class ResponseBuilder {
/**
* Set HTTP Response header.
*
* @param header Key of the header. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
* @param header Key of the header. <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers">Documentation</a>
* @param value Value for the header.
* @return this builder.
*/
@ -75,7 +75,7 @@ public class ResponseBuilder {
* Utility method for building redirects.
*
* @param url URL to redirect the client to with 302 Found.
* @return https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location
* @return <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location">Documentation</a>
*/
public ResponseBuilder redirectTo(String url) {
return setStatus(302).setHeader("Location", url).setContent(new byte[0]);

View File

@ -26,6 +26,11 @@ package com.djrapitops.plan.delivery.web.resolver.exception;
*/
public class BadRequestException extends IllegalArgumentException {
/**
* Default constructor.
*
* @param message Error message - avoid including any input incoming in the request to prevent XSS.
*/
public BadRequestException(String message) {
super(message);
}

View File

@ -1,6 +1,5 @@
/**
* Classes for implementing functionality with {@link com.djrapitops.plan.delivery.web.ResolverService}.
*
* <a href="https://github.com/plan-player-analytics/Plan/wiki/APIv5-PageExtension-API#resolverservice">Documentation</a>
*/
package com.djrapitops.plan.delivery.web.resolver;

View File

@ -43,7 +43,7 @@ public final class Request {
* @param path Requested path /example/target
* @param query Request parameters ?param=value etc
* @param user Web user doing the request (if authenticated)
* @param headers Request headers https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
* @param headers Request headers <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers">Documentation</a>
* @param requestBody Raw body as bytes, if present
*/
public Request(String method, URIPath path, URIQuery query, WebUser user, Map<String, String> headers, byte[] requestBody) {
@ -57,6 +57,11 @@ public final class Request {
/**
* Special constructor that figures out URIPath and URIQuery from "/path/and?query=params" and has no request body.
*
* @param method HTTP requst method
* @param target The requested path and query, e.g. "/path/and?query=params"
* @param user User that made the request
* @param headers HTTP request headers
*/
public Request(String method, String target, WebUser user, Map<String, String> headers) {
this.method = method;
@ -121,7 +126,7 @@ public final class Request {
/**
* Get a header in the request.
*
* @param key https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
* @param key <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers">Documentation</a>
* @return Value if it is present in the request.
*/
public Optional<String> getHeader(String key) {

View File

@ -1,6 +1,5 @@
/**
* Classes for implementing functionality with {@link com.djrapitops.plan.delivery.web.ResourceService}.
*
* <a href="https://github.com/plan-player-analytics/Plan/wiki/APIv5-PageExtension-API#resourceservice">Documentation</a>
*/
package com.djrapitops.plan.delivery.web.resource;

View File

@ -113,6 +113,7 @@ public interface DataExtension {
* <p>
* Requires Capability DATA_EXTENSION_BUILDER_API
*
* @param text Text that is displayed next to the value.
* @return new builder.
*/
default ValueBuilder valueBuilder(String text) {

View File

@ -81,6 +81,9 @@ public interface ExtensionService {
*/
void unregister(DataExtension extension);
/**
* Singleton holder for {@link ExtensionService}.
*/
class Holder {
static final AtomicReference<ExtensionService> service = new AtomicReference<>();

View File

@ -42,6 +42,12 @@ public enum FormatType {
*/
NONE;
/**
* Get a format type by the enum name without exception.
*
* @param name FormatType#name()
* @return Optional if the format type is found by that name, empty if not found.
*/
public static Optional<FormatType> getByName(String name) {
if (name == null) {
return Optional.empty();
@ -51,4 +57,5 @@ public enum FormatType {
} catch (IllegalArgumentException e) {
return Optional.empty();
}
}}
}
}

View File

@ -30,6 +30,11 @@ package com.djrapitops.plan.extension;
*/
public interface Group {
/**
* Get the name of the group.
*
* @return Name of the group given by a {@link com.djrapitops.plan.extension.annotation.GroupProvider}, e.g. "Miner"
*/
String getGroupName();
}

View File

@ -89,7 +89,7 @@ public @interface BooleanProvider {
/**
* Name of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Name of the icon, if name is not valid no icon is shown.
*/
@ -98,7 +98,7 @@ public @interface BooleanProvider {
/**
* Family of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Family that matches an icon, if there is no icon for this family no icon is shown.
*/

View File

@ -76,7 +76,7 @@ public @interface ComponentProvider {
/**
* Name of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Name of the icon, if name is not valid no icon is shown.
*/
@ -85,7 +85,7 @@ public @interface ComponentProvider {
/**
* Family of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Family that matches an icon, if there is no icon for this family no icon is shown.
*/

View File

@ -67,7 +67,7 @@ public @interface DoubleProvider {
/**
* Name of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Name of the icon, if name is not valid no icon is shown.
*/
@ -76,7 +76,7 @@ public @interface DoubleProvider {
/**
* Family of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Family that matches an icon, if there is no icon for this family no icon is shown.
*/

View File

@ -62,7 +62,7 @@ public @interface GroupProvider {
/**
* Name of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Name of the icon, if name is not valid no icon is shown.
*/
@ -71,7 +71,7 @@ public @interface GroupProvider {
/**
* Family of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Family that matches an icon, if there is no icon for this family no icon is shown.
*/

View File

@ -39,10 +39,18 @@ public @interface InvalidateMethod {
*/
String value();
/**
* Multiple {@link InvalidateMethod} annotations are supported per class.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Multiple {
/**
* All the annotations.
*
* @return All InvalidateMethod annotations in the class.
*/
InvalidateMethod[] value();
}

View File

@ -79,7 +79,7 @@ public @interface NumberProvider {
/**
* Name of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Name of the icon, if name is not valid no icon is shown.
*/
@ -88,7 +88,7 @@ public @interface NumberProvider {
/**
* Family of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Family that matches an icon, if there is no icon for this family no icon is shown.
*/

View File

@ -70,7 +70,7 @@ public @interface PercentageProvider {
/**
* Name of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Name of the icon, if name is not valid no icon is shown.
*/
@ -79,7 +79,7 @@ public @interface PercentageProvider {
/**
* Family of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Family that matches an icon, if there is no icon for this family no icon is shown.
*/

View File

@ -44,7 +44,7 @@ public @interface PluginInfo {
/**
* Name of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Name of the icon, if name is not valid no icon is shown.
*/
@ -53,7 +53,7 @@ public @interface PluginInfo {
/**
* Family of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Family that matches an icon, if there is no icon for this family no icon is shown.
*/

View File

@ -79,7 +79,7 @@ public @interface StringProvider {
/**
* Name of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Name of the icon, if name is not valid no icon is shown.
*/
@ -88,7 +88,7 @@ public @interface StringProvider {
/**
* Family of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Family that matches an icon, if there is no icon for this family no icon is shown.
*/

View File

@ -41,7 +41,7 @@ public @interface TabInfo {
/**
* Name of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Name of the icon, if name is not valid no icon is shown.
*/
@ -50,7 +50,7 @@ public @interface TabInfo {
/**
* Family of Font Awesome icon.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @return Family that matches an icon, if there is no icon for this family no icon is shown.
*/

View File

@ -18,7 +18,10 @@ package com.djrapitops.plan.extension.builder;
import com.djrapitops.plan.component.Component;
import com.djrapitops.plan.extension.FormatType;
import com.djrapitops.plan.extension.annotation.*;
import com.djrapitops.plan.extension.annotation.BooleanProvider;
import com.djrapitops.plan.extension.annotation.Conditional;
import com.djrapitops.plan.extension.annotation.StringProvider;
import com.djrapitops.plan.extension.annotation.Tab;
import com.djrapitops.plan.extension.extractor.ExtensionMethod;
import com.djrapitops.plan.extension.icon.Color;
import com.djrapitops.plan.extension.icon.Family;
@ -64,7 +67,7 @@ public interface ValueBuilder {
/**
* Icon displayed next to the value.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons
*
* @param iconName Name of the icon
* @param iconFamily Family of the icon
@ -78,7 +81,7 @@ public interface ValueBuilder {
/**
* Icon displayed next to the value.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons
*
* @param icon Icon built using the methods in {@link Icon}.
* @return This builder.

View File

@ -47,6 +47,12 @@ public enum Color {
BLACK,
NONE;
/**
* Get a color by the enum name without exception.
*
* @param name Color#name()
* @return Optional if the color is found by that name, empty if not found.
*/
public static Optional<Color> getByName(String name) {
if (name == null) {
return Optional.empty();
@ -56,4 +62,5 @@ public enum Color {
} catch (IllegalArgumentException e) {
return Optional.empty();
}
}}
}
}

View File

@ -37,6 +37,12 @@ public enum Family {
*/
BRAND;
/**
* Get a family by the enum name without exception.
*
* @param name Family#name()
* @return Optional if the family is found by that name, empty if not found.
*/
public static Optional<Family> getByName(String name) {
if (name == null) {
return Optional.empty();

View File

@ -21,7 +21,7 @@ import java.util.Objects;
/**
* Object that represents an icon on the website.
* <p>
* See https://fontawesome.com/icons (select 'free')) for icons and their {@link Family}.
* See <a href="https://fontawesome.com/icons">FontAwesome</a> (select 'free')) for icons and their {@link Family}.
*
* @author AuroraLS3
*/
@ -92,6 +92,9 @@ public class Icon {
return "Icon{" + type.name() + ", '" + name + '\'' + ", " + color.name() + '}';
}
/**
* Builder for an {@link Icon}.
*/
public static class Builder {
private final Icon icon;

View File

@ -1,6 +1,5 @@
/**
* DataExtension API and related classes.
*
* <a href="https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API">Documentation</a>
*/
package com.djrapitops.plan.extension;

View File

@ -62,14 +62,44 @@ public interface CommonQueries {
*/
long fetchLastSeen(UUID playerUUID, UUID serverUUID);
/**
* Get the UUIDs of all servers Plan has registered.
*
* @return Set of Server UUIDs
*/
Set<UUID> fetchServerUUIDs();
/**
* Fetch UUID of a player by name.
*
* @param playerName Name of the player
* @return UUID if it is found by Plan or empty if not found.
*/
Optional<UUID> fetchUUIDOf(String playerName);
/**
* Fetch name of a player by UUID.
*
* @param playerUUID UUID of the player
* @return Name if it is known by Plan or empty if not.
*/
Optional<String> fetchNameOf(UUID playerUUID);
/**
* Check that schema has table you are using in your queries.
*
* @param table Name of the table, e.g. plan_users.
* @return true if table exists.
*/
boolean doesDBHaveTable(String table);
/**
* Check that schema table has a column you are using in your queries.
*
* @param table Name of the table, e.g. plan_users.
* @param column Name of the column, e.g. id
* @return true if table and column exist.
*/
boolean doesDBHaveTableColumn(String table, String column);
/**

View File

@ -126,7 +126,7 @@ public interface QueryService {
CommonQueries getCommonQueries();
/**
* See https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
* See <a href="https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html">Functional Interfaces</a>
*/
@FunctionalInterface
interface ThrowingConsumer<T> {
@ -134,7 +134,7 @@ public interface QueryService {
}
/**
* See https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
* See <a href="https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html">Functional Interfaces</a>
*/
@FunctionalInterface
interface ThrowingFunction<T, R> {
@ -142,7 +142,7 @@ public interface QueryService {
}
/**
* See https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
* See <a href="https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html">Functional Interfaces</a>
*/
@FunctionalInterface
interface VoidFunction {

View File

@ -1,6 +1,5 @@
/**
* Query API related classes.
*
* <a href="https://github.com/plan-player-analytics/Plan/wiki/APIv5-Query-API">Documentation</a>
*/
package com.djrapitops.plan.query;

View File

@ -51,6 +51,9 @@ public interface ListenerService {
*/
void registerListenerForPlan(Object listener);
/**
* Singleton holder for listeners.
*/
class Holder {
static final AtomicReference<ListenerService> service = new AtomicReference<>();

View File

@ -16,7 +16,7 @@ plugins {
id 'java-library'
id "jacoco"
id "checkstyle"
id "org.sonarqube" version "4.3.1.3277"
id "org.sonarqube" version "4.4.1.3373"
id 'fabric-loom' version '1.3-SNAPSHOT' apply false
}
@ -42,6 +42,8 @@ allprojects {
ext.minorVersion = '6'
ext.buildVersion = buildVersion
ext.fullVersion = project.ext.majorVersion + '.' + project.ext.minorVersion + ' build ' + project.ext.buildVersion
ext.fullVersionFilename = project.ext.majorVersion + '.' + project.ext.minorVersion + '-build-' + project.ext.buildVersion
ext.fullVersionSemantic = project.ext.majorVersion + '.' + project.ext.minorVersion + '+build.' + project.ext.buildVersion
// Fix for UTF-8 files showing with wrong encoding when compiled on Windows machines.
compileJava { options.encoding = "UTF-8" }
@ -49,7 +51,7 @@ allprojects {
javadoc { options.encoding = 'UTF-8' }
}
logger.lifecycle("Building artifact for version $fullVersion")
logger.lifecycle("Building artifact for version $fullVersion / $fullVersionFilename / $fullVersionSemantic")
subprojects {
// Build plugins
@ -67,7 +69,7 @@ subprojects {
}
ext {
daggerVersion = "2.47"
daggerVersion = "2.51"
palVersion = "5.1.0"
@ -81,31 +83,32 @@ subprojects {
redisBungeeVersion = "0.3.8-SNAPSHOT"
redisBungeeProxioDevVersion = "0.7.3"
commonsTextVersion = "1.10.0"
commonsCompressVersion = "1.24.0"
commonsCodecVersion = "1.16.0"
commonsTextVersion = "1.11.0"
commonsCompressVersion = "1.26.0"
commonsCodecVersion = "1.16.1"
caffeineVersion = "3.1.8"
jettyVersion = "11.0.16"
jettyVersion = "11.0.20"
caffeineVersion = "2.9.2"
mysqlVersion = "8.1.0"
mariadbVersion = "3.2.0"
mysqlVersion = "8.3.0"
mariadbVersion = "3.3.3"
sqliteVersion = "3.42.0.1"
adventureVersion = "4.14.0"
hikariVersion = "5.0.1"
slf4jVersion = "2.0.9"
geoIpVersion = "4.1.0"
hikariVersion = "5.1.0"
slf4jVersion = "2.0.11"
geoIpVersion = "4.2.0"
gsonVersion = "2.10.1"
dependencyDownloadVersion = "1.3.1"
ipAddressMatcherVersion = "5.4.0"
ipAddressMatcherVersion = "5.5.0"
jasyptVersion = "1.9.3"
bstatsVersion = "3.0.2"
placeholderapiVersion = "2.11.3"
placeholderapiVersion = "2.11.5"
nkPlaceholderapiVersion = "1.4-SNAPSHOT"
junitVersion = "5.10.0"
mockitoVersion = "5.5.0"
testContainersVersion = "1.19.0"
swaggerVersion = "2.2.16"
junitVersion = "5.10.2"
mockitoVersion = "5.11.0"
testContainersVersion = "1.19.7"
swaggerVersion = "2.2.20"
}
repositories {
@ -127,6 +130,8 @@ subprojects {
testImplementation "com.google.dagger:dagger:$daggerVersion"
testAnnotationProcessor "com.google.dagger:dagger-compiler:$daggerVersion"
compileOnly "io.swagger.core.v3:swagger-core-jakarta:$swaggerVersion"
// Test Tooling Dependencies
testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion" // JUnit 5
testImplementation "org.mockito:mockito-core:$mockitoVersion" // Mockito Core

View File

@ -16,13 +16,16 @@
*/
package com.djrapitops.plan.gathering;
import com.djrapitops.plan.gathering.domain.PluginMetadata;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@ -129,4 +132,12 @@ public class BukkitSensor implements ServerSensor<World> {
.map(Player::getName)
.collect(Collectors.toList());
}
@Override
public List<PluginMetadata> getInstalledPlugins() {
return Arrays.stream(Bukkit.getPluginManager().getPlugins())
.map(Plugin::getDescription)
.map(description -> new PluginMetadata(description.getName(), description.getVersion()))
.collect(Collectors.toList());
}
}

View File

@ -25,7 +25,7 @@ import java.util.stream.Collectors;
/**
* UserImportRefiner attempts to find any crucial information that is missing.
*
* <p>
* - Finds UUIDs if only name is present.
* - Finds Names if only UUID is present.
* - Removes any importers that do not have any identifiers.

View File

@ -91,7 +91,9 @@ public class PlayerOnlineListener implements Listener {
String address = event.getHostname();
if (!address.isEmpty()) {
address = address.substring(0, address.lastIndexOf(':'));
if (address.contains(":")) {
address = address.substring(0, address.lastIndexOf(':'));
}
if (address.contains("\u0000")) {
address = address.substring(0, address.indexOf('\u0000'));
}

View File

@ -53,7 +53,7 @@ import java.util.logging.Logger;
* Task that handles player ping calculation on Bukkit based servers.
* <p>
* Modified PingManager from LagMonitor plugin.
* https://github.com/games647/LagMonitor/blob/master/src/main/java/com/github/games647/lagmonitor/task/PingManager.java
* <a href="https://github.com/games647/LagMonitor/blob/master/src/main/java/com/github/games647/lagmonitor/task/PingManager.java">original</a>
*
* @author games647
*/
@ -89,9 +89,9 @@ public class BukkitPingCounter extends TaskSystem.Task implements Listener {
startRecording = new ConcurrentHashMap<>();
playerHistory = new HashMap<>();
Optional<PingMethod> pingMethod = loadPingMethod();
if (pingMethod.isPresent()) {
this.pingMethod = pingMethod.get();
Optional<PingMethod> loaded = loadPingMethod();
if (loaded.isPresent()) {
this.pingMethod = loaded.get();
pingMethodAvailable = true;
} else {
pingMethodAvailable = false;

View File

@ -26,6 +26,7 @@ import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
import com.djrapitops.plan.gathering.ShutdownDataPreservation;
import com.djrapitops.plan.gathering.ShutdownHook;
import com.djrapitops.plan.gathering.timed.BukkitPingCounter;
import com.djrapitops.plan.gathering.timed.InstalledPluginGatheringTask;
import com.djrapitops.plan.gathering.timed.ServerTPSCounter;
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
import com.djrapitops.plan.settings.upkeep.ConfigStoreTask;
@ -108,4 +109,8 @@ public interface BukkitTaskModule {
@Binds
@IntoSet
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
@Binds
@IntoSet
TaskSystem.Task bindInstalledPluginGatheringTask(InstalledPluginGatheringTask installedPluginGatheringTask);
}

View File

@ -32,7 +32,7 @@ import java.lang.reflect.Field;
* An utility class that simplifies reflection in Bukkit plugins.
* <p>
* Modified Reflection utility from LagMonitor plugin.
* https://github.com/games647/LagMonitor/blob/master/src/main/java/com/github/games647/lagmonitor/traffic/Reflection.java
* <a href="https://github.com/games647/LagMonitor/blob/master/src/main/java/com/github/games647/lagmonitor/traffic/Reflection.java">original code</a>
*
* @author Kristian
*/

View File

@ -17,9 +17,11 @@
package com.djrapitops.plan.gathering;
import com.djrapitops.plan.PlanBungee;
import com.djrapitops.plan.gathering.domain.PluginMetadata;
import com.djrapitops.plan.identification.properties.RedisCheck;
import com.djrapitops.plan.identification.properties.RedisPlayersOnlineSupplier;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Plugin;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -35,11 +37,13 @@ public class BungeeSensor implements ServerSensor<Object> {
private final IntSupplier onlinePlayerCountSupplier;
private final IntSupplier onlinePlayerCountBungee;
private final Supplier<Collection<ProxiedPlayer>> getPlayers;
private final Supplier<Collection<Plugin>> getPlugins;
@Inject
public BungeeSensor(PlanBungee plugin) {
getPlayers = plugin.getProxy()::getPlayers;
onlinePlayerCountBungee = plugin.getProxy()::getOnlineCount;
getPlugins = plugin.getProxy().getPluginManager()::getPlugins;
onlinePlayerCountSupplier = RedisCheck.isClassAvailable() ? new RedisPlayersOnlineSupplier() : onlinePlayerCountBungee;
}
@ -63,4 +67,12 @@ public class BungeeSensor implements ServerSensor<Object> {
public boolean usingRedisBungee() {
return RedisCheck.isClassAvailable();
}
@Override
public List<PluginMetadata> getInstalledPlugins() {
return getPlugins.get().stream()
.map(Plugin::getDescription)
.map(description -> new PluginMetadata(description.getName(), description.getVersion()))
.collect(Collectors.toList());
}
}

View File

@ -40,5 +40,5 @@ public interface BungeeSuperClassBindingModule {
ListenerSystem bindListenerSystem(BungeeListenerSystem listenerSystem);
@Binds
ServerSensor<Object> bindServerSensor(BungeeSensor sensor);
ServerSensor<?> bindServerSensor(BungeeSensor sensor);
}

View File

@ -23,6 +23,7 @@ import com.djrapitops.plan.delivery.webserver.cache.JSONFileStorage;
import com.djrapitops.plan.delivery.webserver.configuration.AddressAllowList;
import com.djrapitops.plan.extension.ExtensionServerDataUpdater;
import com.djrapitops.plan.gathering.timed.BungeePingCounter;
import com.djrapitops.plan.gathering.timed.InstalledPluginGatheringTask;
import com.djrapitops.plan.gathering.timed.ProxyTPSCounter;
import com.djrapitops.plan.gathering.timed.SystemUsageBuffer;
import com.djrapitops.plan.settings.upkeep.NetworkConfigStoreTask;
@ -87,4 +88,8 @@ public interface BungeeTaskModule {
@Binds
@IntoSet
TaskSystem.Task bindAddressAllowListUpdateTask(AddressAllowList addressAllowList);
@Binds
@IntoSet
TaskSystem.Task bindInstalledPluginGatheringTask(InstalledPluginGatheringTask installedPluginGatheringTask);
}

View File

@ -3,8 +3,8 @@ import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id "dev.vankka.dependencydownload.plugin" version "$dependencyDownloadVersion"
id "com.github.node-gradle.node" version "7.0.0"
id "io.swagger.core.v3.swagger-gradle-plugin" version "2.2.16"
id "com.github.node-gradle.node" version "7.0.2"
id "io.swagger.core.v3.swagger-gradle-plugin" version "2.2.20"
}
configurations {
@ -64,6 +64,7 @@ dependencies {
mysqlDriver "com.mysql:mysql-connector-j:$mysqlVersion"
mariadbDriver "org.mariadb.jdbc:mariadb-java-client:$mariadbVersion"
sqliteDriver "org.xerial:sqlite-jdbc:$sqliteVersion"
sqliteDriver "org.slf4j:slf4j-nop:1.7.36"
ipAddressMatcher "com.github.seancfoley:ipaddress:$ipAddressMatcherVersion"
shadow "org.apache.commons:commons-text:$commonsTextVersion"
@ -82,6 +83,8 @@ dependencies {
// json-simple has junit (a test dependency) compile scoped
exclude group: "junit", module: "junit"
}
implementation "org.jasypt:jasypt:$jasyptVersion:lite"
// Swagger annotations
implementation "jakarta.ws.rs:jakarta.ws.rs-api:3.1.0"
@ -92,7 +95,7 @@ dependencies {
testArtifacts project(":extensions:adventure")
testImplementation project(":extensions:adventure")
testImplementation "com.google.code.gson:gson:$gsonVersion"
testImplementation "org.seleniumhq.selenium:selenium-java:4.12.1"
testImplementation "org.seleniumhq.selenium:selenium-java:4.18.1"
testImplementation "org.testcontainers:testcontainers:$testContainersVersion"
testImplementation "org.testcontainers:junit-jupiter:$testContainersVersion"
testImplementation "org.testcontainers:nginx:$testContainersVersion"
@ -111,13 +114,14 @@ task updateVersion(type: Copy) {
node {
download = true
version = "16.14.2"
version = "20.9.0"
nodeProjectDir = file("$rootDir/react/dashboard")
}
task yarnBundle(type: YarnTask) {
inputs.files(fileTree("$rootDir/react/dashboard/src"))
inputs.file("$rootDir/react/dashboard/package.json")
inputs.file("$rootDir/react/dashboard/vite.config.js")
outputs.dir("$rootDir/react/dashboard/build")
@ -229,8 +233,12 @@ artifacts {
}
processResources {
dependsOn copyYarnBuildResults
dependsOn determineAssetModifications
// Skips Yarn build on Jitpack since Jitpack doesn't offer gclib version compatible with Node 20
// Jitpack build is used mainly for java dependencies.
if (!project.hasProperty("isJitpack")) {
dependsOn copyYarnBuildResults
dependsOn determineAssetModifications
}
dependsOn generateResourceForMySQLDriver
dependsOn generateResourceForSQLiteDriver
dependsOn generateResourceForIpAddressMatcher

View File

@ -0,0 +1,119 @@
/*
* 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;
import com.djrapitops.plan.component.ComponentSvc;
import com.djrapitops.plan.delivery.web.ResolverSvc;
import com.djrapitops.plan.delivery.web.ResourceSvc;
import com.djrapitops.plan.extension.ExtensionSvc;
import com.djrapitops.plan.query.QuerySvc;
import com.djrapitops.plan.settings.ListenerSvc;
import com.djrapitops.plan.settings.SchedulerSvc;
import com.djrapitops.plan.settings.SettingsSvc;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Breaks up {@link PlanSystem} to be a smaller class.
*
* @author AuroraLS3
*/
@Singleton
public class ApiServices {
private final ComponentSvc componentService;
private final ResolverSvc resolverService;
private final ResourceSvc resourceService;
private final ExtensionSvc extensionService;
private final QuerySvc queryService;
private final ListenerSvc listenerService;
private final SettingsSvc settingsService;
private final SchedulerSvc schedulerService;
@Inject
public ApiServices(
ComponentSvc componentService,
ResolverSvc resolverService,
ResourceSvc resourceService,
ExtensionSvc extensionService,
QuerySvc queryService,
ListenerSvc listenerService,
SettingsSvc settingsService,
SchedulerSvc schedulerService
) {
this.componentService = componentService;
this.resolverService = resolverService;
this.resourceService = resourceService;
this.extensionService = extensionService;
this.queryService = queryService;
this.listenerService = listenerService;
this.settingsService = settingsService;
this.schedulerService = schedulerService;
}
public void register() {
extensionService.register();
componentService.register();
resolverService.register();
resourceService.register();
listenerService.register();
settingsService.register();
schedulerService.register();
queryService.register();
}
public void registerExtensions() {
extensionService.registerExtensions();
}
public void disableExtensionDataUpdates() {
extensionService.disableUpdates();
}
public ComponentSvc getComponentService() {
return componentService;
}
public ResolverSvc getResolverService() {
return resolverService;
}
public ResourceSvc getResourceService() {
return resourceService;
}
public ExtensionSvc getExtensionService() {
return extensionService;
}
public QuerySvc getQueryService() {
return queryService;
}
public ListenerSvc getListenerService() {
return listenerService;
}
public SettingsSvc getSettingsService() {
return settingsService;
}
public SchedulerSvc getSchedulerService() {
return schedulerService;
}
}

View File

@ -17,25 +17,17 @@
package com.djrapitops.plan;
import com.djrapitops.plan.api.PlanAPI;
import com.djrapitops.plan.component.ComponentSvc;
import com.djrapitops.plan.delivery.DeliveryUtilities;
import com.djrapitops.plan.delivery.export.ExportSystem;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.web.ResolverSvc;
import com.djrapitops.plan.delivery.web.ResourceSvc;
import com.djrapitops.plan.delivery.webserver.NonProxyWebserverDisableChecker;
import com.djrapitops.plan.delivery.webserver.WebServerSystem;
import com.djrapitops.plan.extension.ExtensionSvc;
import com.djrapitops.plan.gathering.cache.CacheSystem;
import com.djrapitops.plan.gathering.importing.ImportSystem;
import com.djrapitops.plan.gathering.listeners.ListenerSystem;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.processing.Processing;
import com.djrapitops.plan.query.QuerySvc;
import com.djrapitops.plan.settings.ConfigSystem;
import com.djrapitops.plan.settings.ListenerSvc;
import com.djrapitops.plan.settings.SchedulerSvc;
import com.djrapitops.plan.settings.SettingsSvc;
import com.djrapitops.plan.settings.locale.LocaleSystem;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.file.PlanFiles;
@ -77,14 +69,7 @@ public class PlanSystem implements SubSystem {
private final ImportSystem importSystem;
private final ExportSystem exportSystem;
private final DeliveryUtilities deliveryUtilities;
private final ComponentSvc componentService;
private final ResolverSvc resolverService;
private final ResourceSvc resourceService;
private final ExtensionSvc extensionService;
private final QuerySvc queryService;
private final ListenerSvc listenerService;
private final SettingsSvc settingsService;
private final SchedulerSvc schedulerService;
private final ApiServices apiServices;
private final PluginLogger logger;
private final ErrorLogger errorLogger;
@ -104,16 +89,9 @@ public class PlanSystem implements SubSystem {
ImportSystem importSystem,
ExportSystem exportSystem,
DeliveryUtilities deliveryUtilities,
ComponentSvc componentService,
ResolverSvc resolverService,
ResourceSvc resourceService,
ExtensionSvc extensionService,
QuerySvc queryService,
ListenerSvc listenerService,
SettingsSvc settingsService,
SchedulerSvc schedulerService,
PluginLogger logger,
ErrorLogger errorLogger,
ApiServices apiServices, // API v5
@SuppressWarnings("deprecation") PlanAPI.PlanAPIHolder apiHolder // Deprecated PlanAPI, backwards compatibility
) {
this.files = files;
@ -130,16 +108,9 @@ public class PlanSystem implements SubSystem {
this.importSystem = importSystem;
this.exportSystem = exportSystem;
this.deliveryUtilities = deliveryUtilities;
this.componentService = componentService;
this.resolverService = resolverService;
this.resourceService = resourceService;
this.extensionService = extensionService;
this.queryService = queryService;
this.listenerService = listenerService;
this.settingsService = settingsService;
this.schedulerService = schedulerService;
this.logger = logger;
this.errorLogger = errorLogger;
this.apiServices = apiServices;
logger.info("§2");
logger.info("§2 ██▌");
@ -162,14 +133,7 @@ public class PlanSystem implements SubSystem {
* Enables the rest of the systems that are not enabled in {@link #enableForCommands()}.
*/
public void enableOtherThanCommands() {
extensionService.register();
componentService.register();
resolverService.register();
resourceService.register();
listenerService.register();
settingsService.register();
schedulerService.register();
queryService.register();
apiServices.register();
enableSystems(
processing,
@ -189,11 +153,11 @@ public class PlanSystem implements SubSystem {
// Disables Webserver if Proxy is detected in the database
if (serverInfo.getServer().isNotProxy()) {
processing.submitNonCritical(new NonProxyWebserverDisableChecker(
configSystem.getConfig(), webServerSystem.getAddresses(), webServerSystem, logger, errorLogger
configSystem.getConfig(), localeSystem.getLocale(), webServerSystem.getAddresses(), webServerSystem, logger, errorLogger
));
}
extensionService.registerExtensions();
apiServices.registerExtensions();
enabled = true;
String javaVersion = System.getProperty("java.specification.version");
@ -223,7 +187,7 @@ public class PlanSystem implements SubSystem {
enabled = false;
Formatters.clearSingleton();
extensionService.disableUpdates();
apiServices.disableExtensionDataUpdates();
disableSystems(
taskSystem,
@ -316,12 +280,8 @@ public class PlanSystem implements SubSystem {
return enabled;
}
public ExtensionSvc getExtensionService() {
return extensionService;
}
public ComponentSvc getComponentService() {
return componentService;
public ApiServices getApiServices() {
return apiServices;
}
public static long getServerEnableTime() {

View File

@ -43,7 +43,7 @@ import java.util.stream.Collectors;
* PlanAPI extension for all implementations.
*
* @author AuroraLS3
* @deprecated Plan API v4 has been deprecated, use the APIv5 instead (https://github.com/plan-player-analytics/Plan/wiki/APIv5).
* @deprecated Plan API v4 has been deprecated, use the APIv5 instead (<a href="https://github.com/plan-player-analytics/Plan/wiki/APIv5">wiki</a>).
*/
@Singleton
@Deprecated(forRemoval = true, since = "5.0")

View File

@ -35,7 +35,7 @@ import java.util.UUID;
* Interface for PlanAPI methods.
*
* @author AuroraLS3
* @deprecated Plan API v4 has been deprecated, use the APIv5 instead (https://github.com/plan-player-analytics/Plan/wiki/APIv5).
* @deprecated Plan API v4 has been deprecated, use the APIv5 instead (<a href="https://github.com/plan-player-analytics/Plan/wiki/APIv5">wiki</a>).
*/
@Deprecated(since = "5.0")
public interface PlanAPI {
@ -65,7 +65,7 @@ public interface PlanAPI {
}
/**
* @deprecated PluginData API has been deprecated - see https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API for new API.
* @deprecated PluginData API has been deprecated - see <a href="https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API">wiki</a> for new API.
*/
@Deprecated
void addPluginDataSource(PluginData pluginData);

View File

@ -29,7 +29,7 @@ import java.util.Optional;
* The Keys might change in the future, but the Optional API should help dealing with those cases.
*
* @author AuroraLS3
* @deprecated Plan API v4 has been deprecated, use the APIv5 instead (https://github.com/plan-player-analytics/Plan/wiki/APIv5).
* @deprecated Plan API v4 has been deprecated, use the APIv5 instead (<a href="https://github.com/plan-player-analytics/Plan/wiki/APIv5">wiki</a>).
*/
@Deprecated(since = "5.0")
public class PlayerContainer {

View File

@ -28,7 +28,7 @@ import java.util.Optional;
* The Keys might change in the future, but the Optional API should help dealing with those cases.
*
* @author AuroraLS3
* @deprecated Plan API v4 has been deprecated, use the APIv5 instead (https://github.com/plan-player-analytics/Plan/wiki/APIv5).
* @deprecated Plan API v4 has been deprecated, use the APIv5 instead (<a href="https://github.com/plan-player-analytics/Plan/wiki/APIv5">wiki</a>).
*/
@Deprecated(forRemoval = true, since = "5.0")
public class ServerContainer {

View File

@ -29,6 +29,7 @@ import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.exceptions.ExportException;
import com.djrapitops.plan.gathering.domain.GeoInfo;
import com.djrapitops.plan.gathering.domain.event.JoinAddress;
import com.djrapitops.plan.gathering.importing.ImportSystem;
import com.djrapitops.plan.gathering.importing.importers.Importer;
import com.djrapitops.plan.identification.Identifiers;
@ -42,6 +43,7 @@ import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.CommandLang;
import com.djrapitops.plan.settings.locale.lang.GenericLang;
import com.djrapitops.plan.settings.locale.lang.HelpLang;
import com.djrapitops.plan.settings.locale.lang.HtmlLang;
import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.queries.containers.ContainerFetchQueries;
@ -258,12 +260,17 @@ public class DataUtilityCommands {
Optional<GeoInfo> mostRecentGeoInfo = new GeoInfoMutator(geoInfo).mostRecent();
String geolocation = mostRecentGeoInfo.isPresent() ? mostRecentGeoInfo.get().getGeolocation() : "-";
SessionsMutator sessionsMutator = SessionsMutator.forContainer(player);
String latestJoinAddress = sessionsMutator.latestSession()
.flatMap(session -> session.getExtraData(JoinAddress.class))
.map(JoinAddress::getAddress)
.orElse("-");
String table = locale.getString(CommandLang.HEADER_INSPECT, playerName) + '\n' +
locale.getString(CommandLang.INGAME_ACTIVITY_INDEX, activityIndex.getFormattedValue(formatters.decimals()), activityIndex.getGroup()) + '\n' +
locale.getString(CommandLang.INGAME_REGISTERED, timestamp.apply(() -> registered)) + '\n' +
locale.getString(CommandLang.INGAME_LAST_SEEN, timestamp.apply(() -> lastSeen)) + '\n' +
locale.getString(CommandLang.INGAME_GEOLOCATION, geolocation) + '\n' +
locale.getString(HtmlLang.TITLE_LATEST_JOIN_ADDRESSES, latestJoinAddress) + '\n' +
locale.getString(CommandLang.INGAME_TIMES_KICKED, player.getValue(PlayerKeys.KICK_COUNT).orElse(0)) + '\n' +
'\n' +
locale.getString(CommandLang.INGAME_PLAYTIME, length.apply(sessionsMutator.toPlaytime())) + '\n' +

View File

@ -27,7 +27,7 @@ import java.util.UUID;
* @author AuroraLS3
* @see TableContainer
* @see InspectContainer
* @deprecated PluginData API has been deprecated - see https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API for new API.
* @deprecated PluginData API has been deprecated - see <a href="https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API">wiki</a> for new API.
*/
@Deprecated(since = "5.0")
public final class AnalysisContainer extends InspectContainer {

View File

@ -27,7 +27,7 @@ import java.util.TreeMap;
*
* @author AuroraLS3
* @see TableContainer
* @deprecated PluginData API has been deprecated - see https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API for new API.
* @deprecated PluginData API has been deprecated - see <a href="https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API">wiki</a> for new API.
*/
@Deprecated(since = "5.0")
public class InspectContainer {

View File

@ -26,7 +26,7 @@ import java.util.List;
* Container used for creating Html tables.
*
* @author AuroraLS3
* @deprecated PluginData API has been deprecated - see https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API for new API.
* @deprecated PluginData API has been deprecated - see <a href="https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API">wiki</a> for new API.
*/
@Deprecated(since = "5.0")
public class TableContainer {

View File

@ -23,7 +23,7 @@ import java.util.UUID;
* Interface for PluginData objects that affect Ban state of players.
*
* @author AuroraLS3
* @deprecated PluginData API has been deprecated - see https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API for new API.
* @deprecated PluginData API has been deprecated - see <a href="https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API">wiki</a> for new API.
*/
@Deprecated(since = "5.0")
public interface BanData {

View File

@ -20,7 +20,7 @@ package com.djrapitops.plan.data.plugin;
* Enum class for PluginData to estimate the required width of the contained items.
*
* @author AuroraLS3
* @deprecated PluginData API has been deprecated - see https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API for new API.
* @deprecated PluginData API has been deprecated - see <a href="https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API">wiki</a> for new API.
*/
@Deprecated
public enum ContainerSize {

View File

@ -33,7 +33,7 @@ import java.util.UUID;
* to register objects extending this class.
*
* @author AuroraLS3
* @deprecated PluginData API has been deprecated - see https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API for new API.
* @deprecated PluginData API has been deprecated - see <a href="https://github.com/plan-player-analytics/Plan/wiki/APIv5---DataExtension-API">wiki</a> for new API.
*/
@Deprecated(since = "5.0")
public abstract class PluginData {

View File

@ -0,0 +1,77 @@
/*
* 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.domain;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
/**
* Represents plugin version history.
* <p>
* If version is null the plugin was uninstalled at that time.
*
* @author AuroraLS3
*/
public class PluginHistoryMetadata {
private final String name;
@Nullable
private final String version;
private final long modified;
public PluginHistoryMetadata(String name, @Nullable String version, long modified) {
this.name = name;
this.version = version;
this.modified = modified;
}
public String getName() {
return name;
}
@Nullable
public String getVersion() {
return version;
}
public long getModified() {
return modified;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PluginHistoryMetadata that = (PluginHistoryMetadata) o;
return getModified() == that.getModified() && Objects.equals(getName(), that.getName()) && Objects.equals(getVersion(), that.getVersion());
}
@Override
public int hashCode() {
return Objects.hash(getName(), getVersion(), getModified());
}
@Override
public String toString() {
return "PluginHistoryMetadata{" +
"name='" + name + '\'' +
", version='" + version + '\'' +
", modified=" + modified +
'}';
}
}

View File

@ -18,6 +18,7 @@ package com.djrapitops.plan.delivery.domain;
import com.djrapitops.plan.delivery.domain.mutators.ActivityIndex;
import com.djrapitops.plan.gathering.domain.BaseUser;
import com.djrapitops.plan.gathering.domain.Ping;
import java.util.Objects;
import java.util.Optional;
@ -38,6 +39,7 @@ public class TablePlayer implements Comparable<TablePlayer> {
private Long registered;
private Long lastSeen;
private String geolocation;
private Ping ping;
private boolean banned = false;
@ -87,6 +89,10 @@ public class TablePlayer implements Comparable<TablePlayer> {
return Optional.ofNullable(geolocation);
}
public Ping getPing() {
return ping;
}
public boolean isBanned() {
return banned;
}
@ -111,12 +117,13 @@ public class TablePlayer implements Comparable<TablePlayer> {
lastSeen.equals(that.lastSeen) &&
name.equals(that.name) &&
activityIndex.equals(that.activityIndex) &&
geolocation.equals(that.geolocation);
geolocation.equals(that.geolocation) &&
ping.equals(that.ping);
}
@Override
public int hashCode() {
return Objects.hash(name, activityIndex, activePlaytime, sessionCount, registered, lastSeen, geolocation);
return Objects.hash(name, activityIndex, activePlaytime, sessionCount, registered, lastSeen, geolocation, ping);
}
@Override
@ -130,6 +137,7 @@ public class TablePlayer implements Comparable<TablePlayer> {
", registered=" + registered +
", lastSeen=" + lastSeen +
", geolocation='" + geolocation + '\'' +
", ping='" + ping + '\'' +
", banned=" + banned +
'}';
}
@ -190,6 +198,11 @@ public class TablePlayer implements Comparable<TablePlayer> {
return this;
}
public Builder ping(Ping ping) {
player.ping = ping;
return this;
}
public TablePlayer build() {
return player;
}

View File

@ -35,6 +35,7 @@ public enum WebPermission implements Supplier<String>, Lang {
PAGE_NETWORK_OVERVIEW_GRAPHS_ONLINE("See Players Online graph"),
PAGE_NETWORK_OVERVIEW_GRAPHS_DAY_BY_DAY("See Day by Day graph"),
PAGE_NETWORK_OVERVIEW_GRAPHS_HOUR_BY_HOUR("See Hour by Hour graph"),
PAGE_NETWORK_OVERVIEW_GRAPHS_CALENDAR("See Network calendar"),
PAGE_NETWORK_SERVER_LIST("See list of servers"),
PAGE_NETWORK_PLAYERBASE("See Playerbase Overview -tab"),
PAGE_NETWORK_PLAYERBASE_OVERVIEW("See Playerbase Overview numbers"),
@ -54,6 +55,7 @@ public enum WebPermission implements Supplier<String>, Lang {
PAGE_NETWORK_GEOLOCATIONS_PING_PER_COUNTRY("See Ping Per Country table"),
PAGE_NETWORK_PLAYERS("See Player list -tab"),
PAGE_NETWORK_PERFORMANCE("See network Performance tab"),
PAGE_NETWORK_PLUGIN_HISTORY("See Plugin History across the network"),
PAGE_NETWORK_PLUGINS("See Plugins tab of Proxy"),
PAGE_SERVER("See all of server page"),
@ -89,6 +91,7 @@ public enum WebPermission implements Supplier<String>, Lang {
PAGE_SERVER_PERFORMANCE("See Performance tab"),
PAGE_SERVER_PERFORMANCE_GRAPHS("See Performance graphs"),
PAGE_SERVER_PERFORMANCE_OVERVIEW("See Performance numbers"),
PAGE_SERVER_PLUGIN_HISTORY("See Plugin History"),
PAGE_SERVER_PLUGINS("See Plugins -tabs of servers"),
PAGE_PLAYER("See all of player page"),

View File

@ -139,7 +139,6 @@ public interface DataContainer {
/**
* Get formatted Value identified by the Key.
* <p>
*
* @param key Key that identifies the Value
* @param formatter Formatter for the Optional returned by {@link DataContainer#getValue(Key)}

View File

@ -0,0 +1,60 @@
/*
* 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.domain.datatransfer;
import com.djrapitops.plan.delivery.domain.PluginHistoryMetadata;
import java.util.List;
import java.util.Objects;
/**
* History of plugin versions, sorted most recent first.
*
* @author AuroraLS3
*/
public class PluginHistoryDto {
private final List<PluginHistoryMetadata> history;
public PluginHistoryDto(List<PluginHistoryMetadata> history) {
this.history = history;
}
public List<PluginHistoryMetadata> getHistory() {
return history;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PluginHistoryDto that = (PluginHistoryDto) o;
return Objects.equals(getHistory(), that.getHistory());
}
@Override
public int hashCode() {
return Objects.hash(getHistory());
}
@Override
public String toString() {
return "PluginHistoryDto{" +
"history=" + history +
'}';
}
}

View File

@ -17,6 +17,7 @@
package com.djrapitops.plan.delivery.domain.datatransfer;
import com.djrapitops.plan.delivery.domain.datatransfer.extension.ExtensionValueDataDto;
import com.djrapitops.plan.gathering.domain.Ping;
import java.util.Map;
import java.util.UUID;
@ -36,6 +37,9 @@ public class TablePlayerDto {
private Long lastSeen;
private Long registered;
private String country;
private Double pingAverage;
private Integer pingMax;
private Integer pingMin;
private Map<String, ExtensionValueDataDto> extensionValues;
@ -119,6 +123,30 @@ public class TablePlayerDto {
this.playerUUID = playerUUID;
}
public Double getPingAverage() {
return pingAverage;
}
public void setPingAverage(Double pingAverage) {
this.pingAverage = pingAverage;
}
public Integer getPingMax() {
return pingMax;
}
public void setPingMax(Integer pingMax) {
this.pingMax = pingMax;
}
public Integer getPingMin() {
return pingMin;
}
public void setPingMin(Integer pingMin) {
this.pingMin = pingMin;
}
public static final class TablePlayerDtoBuilder {
private final TablePlayerDto tablePlayerDto;
@ -169,6 +197,15 @@ public class TablePlayerDto {
return this;
}
public TablePlayerDtoBuilder withPing(Ping ping) {
if (ping != null) {
tablePlayerDto.setPingAverage(ping.getAverage());
tablePlayerDto.setPingMax(ping.getMax());
tablePlayerDto.setPingMin(ping.getMin());
}
return this;
}
public TablePlayerDto build() {return tablePlayerDto;}
}
}

View File

@ -20,6 +20,7 @@ import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.html.Html;
import com.djrapitops.plan.extension.table.Table;
import com.djrapitops.plan.extension.table.TableColumnFormat;
import org.apache.commons.text.StringEscapeUtils;
import java.util.ArrayList;
import java.util.Arrays;
@ -76,7 +77,7 @@ public class TableDto {
case DATE_SECOND:
return Formatters.getInstance().secondLong().apply(Long.parseLong(value.toString()));
case PLAYER_NAME:
return Html.LINK.create("../player/" + Html.encodeToURL(value.toString()));
return Html.LINK.create("../player/" + Html.encodeToURL(value.toString()), StringEscapeUtils.escapeHtml4(value.toString()));
default:
return value.toString();
}

View File

@ -28,15 +28,15 @@ import java.util.Objects;
*/
public class ServerSpecificLineGraph {
private final List<Double[]> points;
private final List<Number[]> points;
private final ServerDto server;
public ServerSpecificLineGraph(List<Double[]> points, ServerDto server) {
public ServerSpecificLineGraph(List<Number[]> points, ServerDto server) {
this.points = points;
this.server = server;
}
public List<Double[]> getPoints() {
public List<Number[]> getPoints() {
return points;
}

View File

@ -38,15 +38,16 @@ import java.util.concurrent.TimeUnit;
* <p>
* Activity for a single week is calculated using {@code A(t) = (1 / (pi/2 * (t/T) + 1))}.
* A(t) is based on function f(x) = 1 / (x + 1), which has property f(0) = 1, decreasing from there, but not in a straight line.
* You can see the function plotted here https://www.wolframalpha.com/input/?i=1+%2F+(x%2B1)+from+-1+to+2
* You can see the function plotted <a href="https://www.wolframalpha.com/input/?i=1+%2F+(x%2B1)+from+-1+to+2">here</a>
* <p>
* To fine tune the curve pi/2 is used since it felt like a good curve.
* <p>
* Activity index A is calculated by using the formula:
* {@code A = 5 - 5 * [A(t1) + A(t2) + A(t3)] / 3}
* <p>
* <a href="https://www.wolframalpha.com/input/?i=plot+y+%3D+5+-+5+*+(1+%2F+(pi%2F2+*+x%2B1))+and+y+%3D1+and+y+%3D+2+and+y+%3D+3+and+y+%3D+3.75+from+-0.5+to+3">
* Plot for A and limits
* https://www.wolframalpha.com/input/?i=plot+y+%3D+5+-+5+*+(1+%2F+(pi%2F2+*+x%2B1))+and+y+%3D1+and+y+%3D+2+and+y+%3D+3+and+y+%3D+3.75+from+-0.5+to+3
* </a>
* <p>
* New Limits for A would thus be
* {@code < 1: Inactive}
@ -90,6 +91,16 @@ public class ActivityIndex {
return getGroups(null);
}
public static String[] getDefaultGroupLangKeys() {
return new String[]{
HtmlLang.INDEX_VERY_ACTIVE.getKey(),
HtmlLang.INDEX_ACTIVE.getKey(),
HtmlLang.INDEX_REGULAR.getKey(),
HtmlLang.INDEX_IRREGULAR.getKey(),
HtmlLang.INDEX_INACTIVE.getKey()
};
}
public static String[] getGroups(Locale locale) {
if (locale == null) {
return new String[]{

View File

@ -166,6 +166,12 @@ public class Exporter extends FileExporter {
}
public void exportReact() throws ExportException {
if (config.isFalse(ExportSettings.PLAYER_PAGES)
&& config.isFalse(ExportSettings.SERVER_PAGE)
&& config.isFalse(ExportSettings.PLAYERS_PAGE)) {
return;
}
Path toDirectory = config.getPageExportPath();
try {

View File

@ -126,6 +126,7 @@ public class NetworkPageExporter extends FileExporter {
"graph?type=activity",
"graph?type=geolocation",
"graph?type=uniqueAndNew",
"graph?type=serverCalendar",
"network/pingTable",
"sessions",
"extensionData?server=" + serverUUID,

View File

@ -16,6 +16,7 @@
*/
package com.djrapitops.plan.delivery.export;
import com.djrapitops.plan.delivery.rendering.BundleAddressCorrection;
import com.djrapitops.plan.delivery.web.AssetVersions;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
@ -52,23 +53,25 @@ public class ReactExporter extends FileExporter {
private final PlanConfig config;
private final RootJSONResolver jsonHandler;
private final AssetVersions assetVersions;
private final BundleAddressCorrection bundleAddressCorrection;
@Inject
public ReactExporter(
PlanFiles files,
PlanConfig config,
RootJSONResolver jsonHandler,
AssetVersions assetVersions
AssetVersions assetVersions,
BundleAddressCorrection bundleAddressCorrection
) {
this.files = files;
this.config = config;
this.jsonHandler = jsonHandler;
this.assetVersions = assetVersions;
this.bundleAddressCorrection = bundleAddressCorrection;
}
public void exportReactFiles(Path toDirectory) throws IOException {
exportIndexHtml(toDirectory);
exportAsset(toDirectory, "asset-manifest.json");
exportAsset(toDirectory, "favicon.ico");
exportAsset(toDirectory, "logo192.png");
exportAsset(toDirectory, "logo512.png");
@ -104,17 +107,18 @@ public class ReactExporter extends FileExporter {
Path to = toDirectory.resolve(path);
Resource resource = files.getResourceFromJar("web/" + path);
// Make static asset loading work with subdirectory addresses
if (path.endsWith(".css") || "asset-manifest.json".equals(path)) {
if (path.endsWith(".css")) {
String contents = resource.asString();
String withReplacedStatic = StringUtils.replace(contents, "/static", getBasePath() + "/static");
export(to, withReplacedStatic);
String withReplaced = bundleAddressCorrection.correctAddressForExport(contents, path);
export(to, withReplaced);
} else if (path.endsWith(".js")) {
String withReplacedConstants = StringUtils.replaceEach(
resource.asString(),
new String[]{"PLAN_BASE_ADDRESS", "PLAN_EXPORTED_VERSION", ".p=\"/\""},
new String[]{config.get(WebserverSettings.EXTERNAL_LINK), "true", ".p=\"" + getBasePath() + "/\""}
new String[]{"PLAN_BASE_ADDRESS", "PLAN_EXPORTED_VERSION"},
new String[]{config.get(WebserverSettings.EXTERNAL_LINK), "true"}
);
export(to, withReplacedConstants);
String withReplaced = bundleAddressCorrection.correctAddressForExport(withReplacedConstants, path);
export(to, withReplaced);
} else {
export(to, resource);
}
@ -141,25 +145,11 @@ public class ReactExporter extends FileExporter {
private void exportIndexHtml(Path toDirectory) throws IOException {
String contents = files.getResourceFromJar("web/index.html")
.asString();
String basePath = getBasePath();
contents = StringUtils.replaceEach(contents,
new String[]{"/static", "/pageExtensionApi.js"},
new String[]{basePath + "/static", basePath + "/pageExtensionApi.js"});
contents = bundleAddressCorrection.correctAddressForExport(contents, "index.html");
export(toDirectory.resolve("index.html"), contents);
}
private String getBasePath() {
String basePath = config.get(WebserverSettings.EXTERNAL_LINK)
.replace("http://", "")
.replace("https://", "");
if (StringUtils.contains(basePath, '/')) {
return basePath.substring(StringUtils.indexOf(basePath, '/'));
} else {
return "";
}
}
private void exportAsset(Path toDirectory, String asset) throws IOException {
export(toDirectory.resolve(asset), files.getResourceFromJar("web/" + asset));
}

View File

@ -24,7 +24,7 @@ import java.util.TimeZone;
/**
* Formats timestamps to the Last-Modified header time format.
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified">Documentation for the header</a>
*
* @author AuroraLS3
*/

View File

@ -0,0 +1,155 @@
/*
* 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.rendering;
import com.djrapitops.plan.delivery.webserver.Addresses;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* In charge of correcting the root address in the javascript bundle.
* <p>
* The javascript bundle assumes everything is hosted at /,
* but hosting settings affect the address and it could be hosted at a subdirectory like /plan/
*
* @author AuroraLS3
*/
@Singleton
public class BundleAddressCorrection {
private static final String STATIC = "static";
private static final Pattern JAVASCRIPT_ADDRESS_PATTERN = Pattern.compile("\"(\\./|/?static)(.+?)\\.(json|js|css)\"");
private final PlanConfig config;
private final Addresses addresses;
@Inject
public BundleAddressCorrection(PlanConfig config, Addresses addresses) {
this.config = config;
this.addresses = addresses;
}
private String getExportBasePath() {
return addresses.getBasePath(config.get(WebserverSettings.EXTERNAL_LINK));
}
private String getWebserverBasePath() {
String address = addresses.getMainAddress()
.orElseGet(addresses::getFallbackLocalhostAddress);
return addresses.getBasePath(address);
}
public String correctAddressForWebserver(String content, String fileName) {
String basePath = getWebserverBasePath();
return correctAddress(content, fileName, basePath);
}
public String correctAddressForExport(String content, String fileName) {
String basePath = getExportBasePath();
return correctAddress(content, fileName, basePath);
}
// basePath is either empty if the address doesn't have a subdirectory, or a subdirectory.
@Nullable
private String correctAddress(String content, String fileName, String basePath) {
if (fileName.endsWith(".css")) {
return correctAddressInCss(content, basePath);
} else if (fileName.endsWith(".js")) {
return correctAddressInJavascript(content, basePath);
} else if ("index.html".equals(fileName)) {
return correctAddressInHtml(content, basePath);
}
return content;
}
private String correctAddressInHtml(String content, String basePath) {
String endingSlash = basePath.endsWith("/") ? "" : "/";
return StringUtils.replaceEach(content,
new String[]{"src=\"/", "href=\"/"},
new String[]{"src=\"" + basePath + endingSlash, "href=\"" + basePath + endingSlash});
}
private String correctAddressInCss(String content, String basePath) {
String endingSlash = basePath.endsWith("/") ? "" : "/";
return StringUtils.replace(content, "/static", basePath + endingSlash + STATIC);
}
private String correctAddressInJavascript(String content, String basePath) {
int lastIndex = 0;
StringBuilder output = new StringBuilder();
Matcher matcher = JAVASCRIPT_ADDRESS_PATTERN.matcher(content);
while (matcher.find()) {
String addressStart = matcher.group(1);
String file = matcher.group(2);
String extension = matcher.group(3);
int startIndex = matcher.start();
int endIndex = matcher.end();
// If basePath is empty the website is hosted at root of the tree /
boolean atUrlRoot = basePath.isEmpty();
// This handles /static and static representation
boolean startsWithSlash = addressStart.startsWith("/");
String startingSlash = startsWithSlash ? "/" : "";
// This handles basePath containing a slash after subdirectory, such as /plan/ instead of /plan
String endingSlash = basePath.endsWith("/") ? "" : "/";
// Without subdirectory we can use the address as is, and it doesn't need changes,
// otherwise we can add the directory to the start.
String staticReplacement = atUrlRoot
? startingSlash + STATIC
: basePath + endingSlash + STATIC;
String relativeReplacement = atUrlRoot
? "./"
: basePath + endingSlash + "static/";
// Replaces basePath starting slash if the replaced thing does not start with slash
if (!startsWithSlash && staticReplacement.startsWith("/")) {
staticReplacement = staticReplacement.substring(1);
}
// Replacement examples when basepath is empty, "/plan" or "/plan/"
// "./Filename-hash.js" -> "./Filename-hash.js" or "/plan/static/Filename-hash.js"
// "/static/Filename-hash.js" -> "/static/Filename-hash.js" or "/plan/static/Filename-hash.js"
// "static/Filename-hash.js" -> "static/Filename-hash.js" or "plan/static/Filename-hash.js"
String replacementAddress = StringUtils.equalsAny(addressStart, "/static", STATIC)
? staticReplacement
: relativeReplacement;
String replacement = '"' + replacementAddress + file + '.' + extension + '"';
output.append(content, lastIndex, startIndex) // Append non-match
.append(replacement); // Append replaced address
lastIndex = endIndex;
}
// Append rest of the content that didn't match
if (lastIndex < content.length()) {
output.append(content, lastIndex, content.length());
}
return output.toString();
}
}

View File

@ -36,6 +36,7 @@ public class Contributors {
new Contributor("Antonok", CODE),
new Contributor("Argetan", CODE),
new Contributor("Aurelien", CODE, LANG),
new Contributor("Binero", CODE),
new Contributor("BrainStone", CODE),
new Contributor("Catalina", LANG),
new Contributor("Elguerrero", LANG),
@ -107,7 +108,10 @@ public class Contributors {
new Contributor("lis2a", LANG),
new Contributor("ToxiWoxi", CODE),
new Contributor("xlanyleeet", LANG),
new Contributor("Jumala9163", LANG)
new Contributor("Jumala9163", LANG),
new Contributor("Dreeam-qwq", CODE),
new Contributor("jhqwqmc", LANG),
new Contributor("liuzhen932", LANG)
};
private Contributors() {

View File

@ -320,9 +320,9 @@ public class JSONFactory {
tableEntries.add(Maps.builder(String.class, Object.class)
.put("country", geolocation)
.put("avg_ping", formatters.decimals().apply(ping.getAverage()) + " ms")
.put("min_ping", ping.getMin() + " ms")
.put("max_ping", ping.getMax() + " ms")
.put("avg_ping", ping.getAverage())
.put("min_ping", ping.getMin())
.put("max_ping", ping.getMax())
.build());
}
return tableEntries;

View File

@ -154,9 +154,9 @@ public class PlayerJSONCreator {
private Map<String, Object> createPingGraphJson(PlayerContainer player) {
PingGraph pingGraph = graphs.line().pingGraph(player.getUnsafe(PlayerKeys.PING));
return Maps.builder(String.class, Object.class)
.put("min_ping_series", pingGraph.getMinGraph().getPoints())
.put("avg_ping_series", pingGraph.getAvgGraph().getPoints())
.put("max_ping_series", pingGraph.getMaxGraph().getPoints())
.put("min_ping_series", pingGraph.getMinGraph().getPointArrays())
.put("avg_ping_series", pingGraph.getAvgGraph().getPointArrays())
.put("max_ping_series", pingGraph.getMaxGraph().getPointArrays())
.put("colors", Maps.builder(String.class, String.class)
.put("min", theme.getValue(ThemeVal.GRAPH_MIN_PING))
.put("avg", theme.getValue(ThemeVal.GRAPH_AVG_PING))

View File

@ -42,8 +42,6 @@ import java.util.stream.Collectors;
/**
* Utility for creating jQuery Datatables JSON for a Players Table.
* <p>
* See https://www.datatables.net/manual/data/orthogonal-data#HTML-5 for sort kinds
*
* @author AuroraLS3
*/
@ -137,12 +135,14 @@ public class PlayersTableJSONCreator {
.map(player -> TablePlayerDto.builder()
.withUuid(player.getPlayerUUID())
.withName(player.getName().orElseGet(() -> player.getPlayerUUID().toString()))
.withActivityIndex(player.getCurrentActivityIndex().map(ActivityIndex::getValue).orElse(0.0))
.withSessionCount((long) player.getSessionCount().orElse(0))
.withPlaytimeActive(player.getActivePlaytime().orElse(null))
.withLastSeen(player.getLastSeen().orElse(null))
.withRegistered(player.getRegistered().orElse(null))
.withCountry(player.getGeolocation().orElse(null))
.withExtensionValues(mapToExtensionValues(extensionData.get(player.getPlayerUUID())))
.withPing(player.getPing())
.build()
).collect(Collectors.toList());
}
@ -200,7 +200,9 @@ public class PlayersTableJSONCreator {
Html link = openPlayerPageInNewTab ? Html.LINK_EXTERNAL : Html.LINK;
putDataEntry(dataJson, link.create(url, StringUtils.replace(StringEscapeUtils.escapeHtml4(name), "\\", "\\\\") /* Backslashes escaped to prevent json errors */), "name");
/* Backslashes escaped to prevent json errors */
String escapedName = StringUtils.replace(StringEscapeUtils.escapeHtml4(name), "\\", "\\\\");
putDataEntry(dataJson, link.create(url, escapedName, escapedName), "name");
putDataEntry(dataJson, activityIndex.getValue(), activityString, "index");
putDataEntry(dataJson, activePlaytime, numberFormatters.get(FormatType.TIME_MILLISECONDS).apply(activePlaytime), "activePlaytime");
putDataEntry(dataJson, loginTimes, "sessions");

View File

@ -16,7 +16,6 @@
*/
package com.djrapitops.plan.delivery.rendering.json;
import com.djrapitops.plan.delivery.domain.DateHolder;
import com.djrapitops.plan.delivery.domain.DateObj;
import com.djrapitops.plan.delivery.domain.mutators.TPSMutator;
import com.djrapitops.plan.delivery.formatting.Formatter;
@ -55,17 +54,14 @@ import java.util.concurrent.TimeUnit;
@Singleton
public class ServerOverviewJSONCreator implements ServerTabJSONCreator<Map<String, Object>> {
private final Formatter<Long> day;
private final PlanConfig config;
private final DBSystem dbSystem;
private final ServerInfo serverInfo;
private final ServerSensor<?> serverSensor;
private final Formatter<Long> timeAmount;
private final Formatter<Double> decimals;
private final Formatter<Double> percentage;
private final ServerUptimeCalculator serverUptimeCalculator;
private final Formatter<DateHolder> year;
@Inject
public ServerOverviewJSONCreator(
@ -82,9 +78,6 @@ public class ServerOverviewJSONCreator implements ServerTabJSONCreator<Map<Strin
this.serverSensor = serverSensor;
this.serverUptimeCalculator = serverUptimeCalculator;
year = formatters.year();
day = formatters.dayLong();
timeAmount = formatters.timeAmount();
decimals = formatters.decimals();
percentage = formatters.percentage();
}
@ -118,7 +111,7 @@ public class ServerOverviewJSONCreator implements ServerTabJSONCreator<Map<Strin
double averageTPS = tpsMutator.averageTPS();
sevenDays.put("average_tps", averageTPS != -1 ? decimals.apply(averageTPS) : GenericLang.UNAVAILABLE.getKey());
sevenDays.put("low_tps_spikes", tpsMutator.lowTpsSpikeCount(config.get(DisplaySettings.GRAPH_TPS_THRESHOLD_MED)));
sevenDays.put("downtime", timeAmount.apply(tpsMutator.serverDownTime()));
sevenDays.put("downtime", tpsMutator.serverDownTime());
return sevenDays;
}
@ -137,18 +130,19 @@ public class ServerOverviewJSONCreator implements ServerTabJSONCreator<Map<Strin
numbers.put("online_players", getOnlinePlayers(serverUUID, db));
Optional<DateObj<Integer>> lastPeak = db.query(TPSQueries.fetchPeakPlayerCount(serverUUID, twoDaysAgo));
Optional<DateObj<Integer>> allTimePeak = db.query(TPSQueries.fetchAllTimePeakPlayerCount(serverUUID));
numbers.put("last_peak_date", lastPeak.map(year).orElse("-"));
numbers.put("last_peak_date", lastPeak.map(DateObj::getDate).map(Object.class::cast).orElse("-"));
numbers.put("last_peak_players", lastPeak.map(dateObj -> dateObj.getValue().toString()).orElse("-"));
numbers.put("best_peak_date", allTimePeak.map(year).orElse("-"));
numbers.put("best_peak_date", allTimePeak.map(DateObj::getDate).map(Object.class::cast).orElse("-"));
numbers.put("best_peak_players", allTimePeak.map(dateObj -> dateObj.getValue().toString()).orElse("-"));
Long totalPlaytime = db.query(SessionQueries.playtime(0L, now, serverUUID));
numbers.put("playtime", timeAmount.apply(totalPlaytime));
numbers.put("player_playtime", userCount != 0 ? timeAmount.apply(totalPlaytime / userCount) : "-");
numbers.put("playtime", totalPlaytime);
numbers.put("player_playtime", userCount != 0 ? totalPlaytime / userCount : "-");
numbers.put("sessions", db.query(SessionQueries.sessionCount(0L, now, serverUUID)));
numbers.put("player_kills", db.query(KillQueries.playerKillCount(0L, now, serverUUID)));
numbers.put("mob_kills", db.query(KillQueries.mobKillCount(0L, now, serverUUID)));
numbers.put("deaths", db.query(KillQueries.deathCount(0L, now, serverUUID)));
numbers.put("current_uptime", serverUptimeCalculator.getServerUptimeMillis(serverUUID).map(timeAmount)
numbers.put("current_uptime", serverUptimeCalculator.getServerUptimeMillis(serverUUID)
.map(Object.class::cast)
.orElse(GenericLang.UNAVAILABLE.getKey()));
return numbers;
@ -171,9 +165,9 @@ public class ServerOverviewJSONCreator implements ServerTabJSONCreator<Map<Strin
Map<String, Object> weeks = new HashMap<>();
weeks.put("start", day.apply(twoWeeksAgo));
weeks.put("midpoint", day.apply(oneWeekAgo));
weeks.put("end", day.apply(now));
weeks.put("start", twoWeeksAgo);
weeks.put("midpoint", oneWeekAgo);
weeks.put("end", now);
Integer uniqueBefore = db.query(PlayerCountQueries.uniquePlayerCount(twoWeeksAgo, oneWeekAgo, serverUUID));
Integer uniqueAfter = db.query(PlayerCountQueries.uniquePlayerCount(oneWeekAgo, now, serverUUID));
@ -199,9 +193,9 @@ public class ServerOverviewJSONCreator implements ServerTabJSONCreator<Map<Strin
Long playtimeAfter = db.query(SessionQueries.playtime(oneWeekAgo, now, serverUUID));
long avgPlaytimeBefore = uniqueBefore != 0 ? playtimeBefore / uniqueBefore : 0L;
long avgPlaytimeAfter = uniqueAfter != 0 ? playtimeAfter / uniqueAfter : 0L;
Trend avgPlaytimeTrend = new Trend(avgPlaytimeBefore, avgPlaytimeAfter, false, timeAmount);
weeks.put("average_playtime_before", timeAmount.apply(avgPlaytimeBefore));
weeks.put("average_playtime_after", timeAmount.apply(avgPlaytimeAfter));
Trend avgPlaytimeTrend = new Trend(avgPlaytimeBefore, avgPlaytimeAfter, false);
weeks.put("average_playtime_before", avgPlaytimeBefore);
weeks.put("average_playtime_after", avgPlaytimeAfter);
weeks.put("average_playtime_trend", avgPlaytimeTrend);
Long sessionsBefore = db.query(SessionQueries.sessionCount(twoWeeksAgo, oneWeekAgo, serverUUID));

View File

@ -310,6 +310,33 @@ public class GraphJSONCreator {
",\"firstDay\":" + 1 + '}';
}
public String networkCalendarJSON() {
Database db = dbSystem.getDatabase();
long now = System.currentTimeMillis();
long twoYearsAgo = now - TimeUnit.DAYS.toMillis(730L);
int timeZoneOffset = config.getTimeZone().getOffset(now);
NavigableMap<Long, Integer> uniquePerDay = db.query(
PlayerCountQueries.uniquePlayerCounts(twoYearsAgo, now, timeZoneOffset)
);
NavigableMap<Long, Integer> newPerDay = db.query(
PlayerCountQueries.newPlayerCounts(twoYearsAgo, now, timeZoneOffset)
);
NavigableMap<Long, Long> playtimePerDay = db.query(
SessionQueries.playtimePerDay(twoYearsAgo, now, timeZoneOffset)
);
NavigableMap<Long, Integer> sessionsPerDay = db.query(
SessionQueries.sessionCountPerDay(twoYearsAgo, now, timeZoneOffset)
);
return "{\"data\":" +
graphs.calendar().serverCalendar(
uniquePerDay,
newPerDay,
playtimePerDay,
sessionsPerDay
).toCalendarSeries() +
",\"firstDay\":" + 1 + '}';
}
public Map<String, Object> serverWorldPieJSONAsMap(ServerUUID serverUUID) {
Database db = dbSystem.getDatabase();
WorldTimes worldTimes = db.query(WorldTimesQueries.fetchServerTotalWorldTimes(serverUUID));
@ -462,7 +489,7 @@ public class GraphJSONCreator {
Integer unknown = joinAddresses.get(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP);
if (unknown != null) {
joinAddresses.remove(JoinAddressTable.DEFAULT_VALUE_FOR_LOOKUP);
joinAddresses.put(locale.getString(GenericLang.UNKNOWN).toLowerCase(), unknown);
joinAddresses.put(GenericLang.UNKNOWN.getKey(), unknown);
}
}
@ -510,7 +537,7 @@ public class GraphJSONCreator {
List<ServerSpecificLineGraph> proxyGraphs = new ArrayList<>();
for (Server proxy : db.query(ServerQueries.fetchProxyServers())) {
ServerUUID proxyUUID = proxy.getUuid();
List<Double[]> points = Lists.map(
List<Number[]> points = Lists.map(
db.query(TPSQueries.fetchPlayersOnlineOfServer(halfYearAgo, now, proxyUUID)),
point -> Point.fromDateObj(point).toArray()
);

View File

@ -21,6 +21,7 @@ import com.djrapitops.plan.delivery.rendering.json.graphs.HighChart;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* This is a LineGraph for any set of Points, thus it is Abstract.
@ -80,6 +81,10 @@ public class LineGraph implements HighChart {
return points;
}
public List<Number[]> getPointArrays() {
return getPoints().stream().map(Point::toArray).collect(Collectors.toList());
}
private void addMissingPoints(StringBuilder arrayBuilder, Long lastX, long date) {
long iterate = lastX + gapStrategy.diffToFirstGapPointMs;
while (iterate < date) {

View File

@ -75,7 +75,7 @@ public class Point {
"y=" + y + '}';
}
public Double[] toArray() {
return new Double[]{x, y};
public Number[] toArray() {
return new Number[]{x, y};
}
}

View File

@ -57,7 +57,7 @@ public class PieGraphFactory {
public Pie activityPie(Map<String, Integer> activityData) {
String[] colors = theme.getPieColors(ThemeVal.GRAPH_ACTIVITY_PIE);
return new ActivityPie(activityData, colors, ActivityIndex.getGroups(locale));
return new ActivityPie(activityData, colors, ActivityIndex.getDefaultGroupLangKeys());
}
public Pie serverPreferencePie(Map<ServerUUID, String> serverNames, Map<ServerUUID, WorldTimes> serverWorldTimes) {

View File

@ -78,4 +78,9 @@ public class SpecialGraphFactory {
}
}
public Map<String, String> getGeocodes() {
if (geoCodes == null) prepareGeocodes();
return geoCodes;
}
}

View File

@ -53,6 +53,6 @@ public class StackGraphFactory {
public StackGraph activityStackGraph(DateMap<Map<String, Integer>> activityData) {
String[] colors = theme.getPieColors(ThemeVal.GRAPH_ACTIVITY_PIE);
return new ActivityStackGraph(activityData, colors, dayFormatter, ActivityIndex.getGroups(locale));
return new ActivityStackGraph(activityData, colors, dayFormatter, ActivityIndex.getDefaultGroupLangKeys());
}
}

View File

@ -16,9 +16,7 @@
*/
package com.djrapitops.plan.delivery.rendering.json.network;
import com.djrapitops.plan.delivery.domain.DateHolder;
import com.djrapitops.plan.delivery.domain.DateObj;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.json.Trend;
import com.djrapitops.plan.gathering.ServerSensor;
@ -50,14 +48,11 @@ import java.util.concurrent.TimeUnit;
@Singleton
public class NetworkOverviewJSONCreator implements NetworkTabJSONCreator<Map<String, Object>> {
private final Formatter<Long> day;
private final PlanConfig config;
private final DBSystem dbSystem;
private final ServerInfo serverInfo;
private final ServerSensor<?> serverSensor;
private final Formatter<Long> timeAmount;
private final ServerUptimeCalculator serverUptimeCalculator;
private final Formatter<DateHolder> year;
@Inject
public NetworkOverviewJSONCreator(
@ -73,10 +68,6 @@ public class NetworkOverviewJSONCreator implements NetworkTabJSONCreator<Map<Str
this.serverInfo = serverInfo;
this.serverSensor = serverSensor;
this.serverUptimeCalculator = serverUptimeCalculator;
year = formatters.year();
day = formatters.dayLong();
timeAmount = formatters.timeAmount();
}
public Map<String, Object> createJSONAsMap() {
@ -122,17 +113,18 @@ public class NetworkOverviewJSONCreator implements NetworkTabJSONCreator<Map<Str
ServerUUID serverUUID = serverInfo.getServerUUID();
Optional<DateObj<Integer>> lastPeak = db.query(TPSQueries.fetchPeakPlayerCount(serverUUID, twoDaysAgo));
Optional<DateObj<Integer>> allTimePeak = db.query(TPSQueries.fetchAllTimePeakPlayerCount(serverUUID));
numbers.put("last_peak_date", lastPeak.map(year).orElse("-"));
numbers.put("last_peak_date", lastPeak.map(DateObj::getDate).map(Object.class::cast).orElse("-"));
numbers.put("last_peak_players", lastPeak.map(dateObj -> dateObj.getValue().toString()).orElse("-"));
numbers.put("best_peak_date", allTimePeak.map(year).orElse("-"));
numbers.put("best_peak_date", allTimePeak.map(DateObj::getDate).map(Object.class::cast).orElse("-"));
numbers.put("best_peak_players", allTimePeak.map(dateObj -> dateObj.getValue().toString()).orElse("-"));
Long totalPlaytime = db.query(SessionQueries.playtime(0L, now));
numbers.put("playtime", timeAmount.apply(totalPlaytime));
numbers.put("player_playtime", userCount != 0 ? timeAmount.apply(totalPlaytime / userCount) : "-");
numbers.put("playtime", totalPlaytime);
numbers.put("player_playtime", userCount != 0 ? totalPlaytime / userCount : "-");
Long sessionCount = db.query(SessionQueries.sessionCount(0L, now));
numbers.put("sessions", sessionCount);
numbers.put("session_length_avg", sessionCount != 0 ? timeAmount.apply(totalPlaytime / sessionCount) : "-");
numbers.put("current_uptime", serverUptimeCalculator.getServerUptimeMillis(serverUUID).map(timeAmount)
numbers.put("session_length_avg", sessionCount != 0 ? totalPlaytime / sessionCount : "-");
numbers.put("current_uptime", serverUptimeCalculator.getServerUptimeMillis(serverUUID)
.map(Object.class::cast)
.orElse(GenericLang.UNAVAILABLE.getKey()));
return numbers;
@ -147,9 +139,9 @@ public class NetworkOverviewJSONCreator implements NetworkTabJSONCreator<Map<Str
Map<String, Object> weeks = new HashMap<>();
weeks.put("start", day.apply(twoWeeksAgo));
weeks.put("midpoint", day.apply(oneWeekAgo));
weeks.put("end", day.apply(now));
weeks.put("start", twoWeeksAgo);
weeks.put("midpoint", oneWeekAgo);
weeks.put("end", now);
Integer uniqueBefore = db.query(PlayerCountQueries.uniquePlayerCount(twoWeeksAgo, oneWeekAgo));
Integer uniqueAfter = db.query(PlayerCountQueries.uniquePlayerCount(oneWeekAgo, now));
@ -175,9 +167,9 @@ public class NetworkOverviewJSONCreator implements NetworkTabJSONCreator<Map<Str
Long playtimeAfter = db.query(SessionQueries.playtime(oneWeekAgo, now));
long avgPlaytimeBefore = uniqueBefore != 0 ? playtimeBefore / uniqueBefore : 0L;
long avgPlaytimeAfter = uniqueAfter != 0 ? playtimeAfter / uniqueAfter : 0L;
Trend avgPlaytimeTrend = new Trend(avgPlaytimeBefore, avgPlaytimeAfter, false, timeAmount);
weeks.put("average_playtime_before", timeAmount.apply(avgPlaytimeBefore));
weeks.put("average_playtime_after", timeAmount.apply(avgPlaytimeAfter));
Trend avgPlaytimeTrend = new Trend(avgPlaytimeBefore, avgPlaytimeAfter, false);
weeks.put("average_playtime_before", avgPlaytimeBefore);
weeks.put("average_playtime_after", avgPlaytimeAfter);
weeks.put("average_playtime_trend", avgPlaytimeTrend);
Long sessionsBefore = db.query(SessionQueries.sessionCount(twoWeeksAgo, oneWeekAgo));
@ -189,9 +181,9 @@ public class NetworkOverviewJSONCreator implements NetworkTabJSONCreator<Map<Str
long avgSessionLengthBefore = sessionsBefore != 0 ? playtimeBefore / sessionsBefore : 0;
long avgSessionLengthAfter = sessionsAfter != 0 ? playtimeAfter / sessionsAfter : 0;
Trend avgSessionLengthTrend = new Trend(avgSessionLengthBefore, avgSessionLengthAfter, false, timeAmount);
weeks.put("session_length_average_before", timeAmount.apply(avgSessionLengthBefore));
weeks.put("session_length_average_after", timeAmount.apply(avgSessionLengthAfter));
Trend avgSessionLengthTrend = new Trend(avgSessionLengthBefore, avgSessionLengthAfter, false);
weeks.put("session_length_average_before", avgSessionLengthBefore);
weeks.put("session_length_average_after", avgSessionLengthAfter);
weeks.put("session_length_average_trend", avgSessionLengthTrend);
return weeks;

View File

@ -16,11 +16,11 @@
*/
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.delivery.rendering.BundleAddressCorrection;
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
import com.djrapitops.plan.delivery.web.ResourceService;
import com.djrapitops.plan.delivery.web.resolver.exception.NotFoundException;
import com.djrapitops.plan.delivery.web.resource.WebResource;
import com.djrapitops.plan.delivery.webserver.Addresses;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.settings.theme.Theme;
@ -50,7 +50,7 @@ public class PageFactory {
private final Lazy<PublicHtmlFiles> publicHtmlFiles;
private final Lazy<Theme> theme;
private final Lazy<DBSystem> dbSystem;
private final Lazy<Addresses> addresses;
private final Lazy<BundleAddressCorrection> bundleAddressCorrection;
private static final String ERROR_HTML_FILE = "error.html";
@Inject
@ -61,14 +61,14 @@ public class PageFactory {
Lazy<Theme> theme,
Lazy<DBSystem> dbSystem,
Lazy<ServerInfo> serverInfo,
Lazy<Addresses> addresses
Lazy<BundleAddressCorrection> bundleAddressCorrection
) {
this.versionChecker = versionChecker;
this.files = files;
this.publicHtmlFiles = publicHtmlFiles;
this.theme = theme;
this.dbSystem = dbSystem;
this.addresses = addresses;
this.bundleAddressCorrection = bundleAddressCorrection;
}
public Page playersPage() throws IOException {
@ -81,18 +81,12 @@ public class PageFactory {
WebResource resource = ResourceService.getInstance().getResource(
"Plan", fileName, () -> getPublicHtmlOrJarResource(fileName)
);
return new ReactPage(getBasePath(), resource);
return new ReactPage(bundleAddressCorrection.get(), resource);
} catch (UncheckedIOException readFail) {
throw readFail.getCause();
}
}
private String getBasePath() {
String address = addresses.get().getMainAddress()
.orElseGet(addresses.get()::getFallbackLocalhostAddress);
return addresses.get().getBasePath(address);
}
/**
* Create a server page.
*

View File

@ -16,8 +16,8 @@
*/
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.delivery.rendering.BundleAddressCorrection;
import com.djrapitops.plan.delivery.web.resource.WebResource;
import org.apache.commons.lang3.StringUtils;
/**
* Represents React index.html.
@ -26,20 +26,17 @@ import org.apache.commons.lang3.StringUtils;
*/
public class ReactPage implements Page {
private final String basePath;
private final BundleAddressCorrection bundleAddressCorrection;
private final WebResource reactHtml;
public ReactPage(String basePath, WebResource reactHtml) {
this.basePath = basePath;
public ReactPage(BundleAddressCorrection bundleAddressCorrection, WebResource reactHtml) {
this.bundleAddressCorrection = bundleAddressCorrection;
this.reactHtml = reactHtml;
}
@Override
public String toHtml() {
return StringUtils.replaceEach(
reactHtml.asString(),
new String[]{"/static", "/pageExtensionApi.js"},
new String[]{basePath + "/static", basePath + "/pageExtensionApi.js"});
return bundleAddressCorrection.correctAddressForWebserver(reactHtml.asString(), "index.html");
}
@Override

View File

@ -20,8 +20,8 @@ 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.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.transactions.GrantWebPermissionToGroupsWithPermissionTransaction;
import com.djrapitops.plan.storage.database.transactions.StoreMissingWebPermissionsTransaction;
import com.djrapitops.plan.storage.database.transactions.webuser.GrantWebPermissionToGroupsWithPermissionTransaction;
import com.djrapitops.plan.storage.database.transactions.webuser.StoreMissingWebPermissionsTransaction;
import com.djrapitops.plan.utilities.dev.Untrusted;
import net.playeranalytics.plugin.server.PluginLogger;
@ -64,6 +64,8 @@ public class ResolverSvc implements ResolverService {
public void registerResolver(String pluginName, String start, Resolver resolver) {
basicResolvers.add(new Container(pluginName, checking -> checking.startsWith(start), resolver, start));
Collections.sort(basicResolvers);
Set<String> usedWebPermissions = resolver.usedWebPermissions();
dbSystem.getDatabase().executeTransaction(new StoreMissingWebPermissionsTransaction(usedWebPermissions));
if (config.isTrue(PluginSettings.DEV_MODE)) {
logger.info("Registered basic resolver '" + start + "' for plugin " + pluginName);
}

View File

@ -72,7 +72,7 @@ public class Addresses {
}
public Optional<String> getMainAddress() {
Optional<String> proxyServerAddress = getProxyServerAddress();
Optional<String> proxyServerAddress = getAnyValidServerAddress();
return proxyServerAddress.isPresent() ? proxyServerAddress : getAccessAddress();
}
@ -112,8 +112,21 @@ public class Addresses {
.findAny();
}
public Optional<String> getAnyValidServerAddress() {
return dbSystem.getDatabase().query(ServerQueries.fetchPlanServerInformationCollection())
.stream()
.map(Server::getWebAddress)
.filter(this::isValidAddress)
.findAny();
}
private boolean isValidAddress(String address) {
return address != null && !address.isEmpty() && !"0.0.0.0".equals(address);
return address != null
&& !address.isEmpty()
&& !"0.0.0.0".equals(address)
&& !"https://www.example.address".equals(address)
&& !"http://www.example.address".equals(address)
&& !"http://localhost:0".equals(address);
}
public Optional<String> getServerPropertyIP() {

View File

@ -20,6 +20,8 @@ import com.djrapitops.plan.delivery.webserver.http.WebServer;
import com.djrapitops.plan.settings.config.PlanConfig;
import com.djrapitops.plan.settings.config.paths.PluginSettings;
import com.djrapitops.plan.settings.config.paths.WebserverSettings;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.PluginLang;
import com.djrapitops.plan.utilities.logging.ErrorContext;
import com.djrapitops.plan.utilities.logging.ErrorLogger;
import net.playeranalytics.plugin.server.PluginLogger;
@ -34,6 +36,7 @@ import java.io.IOException;
public class NonProxyWebserverDisableChecker implements Runnable {
private final PlanConfig config;
private final Locale locale;
private final Addresses addresses;
private final WebServerSystem webServerSystem;
private final PluginLogger logger;
@ -41,12 +44,14 @@ public class NonProxyWebserverDisableChecker implements Runnable {
public NonProxyWebserverDisableChecker(
PlanConfig config,
Locale locale,
Addresses addresses,
WebServerSystem webServerSystem,
PluginLogger logger,
ErrorLogger errorLogger
) {
this.config = config;
this.locale = locale;
this.addresses = addresses;
this.webServerSystem = webServerSystem;
this.logger = logger;
@ -58,7 +63,7 @@ public class NonProxyWebserverDisableChecker implements Runnable {
if (config.isFalse(PluginSettings.PROXY_COPY_CONFIG)) return;
addresses.getProxyServerAddress().ifPresent(address -> {
logger.info("Proxy server detected in the database - Proxy Webserver address is '" + address + "'.");
logger.info(locale.getString(PluginLang.ENABLE_NOTIFY_PROXY_ADDRESS, address));
WebServer webServer = webServerSystem.getWebServer();
if (webServer.isEnabled()) {
@ -68,12 +73,12 @@ public class NonProxyWebserverDisableChecker implements Runnable {
}
private void disableWebserver(WebServer webServer) {
logger.warn("Disabling Webserver on this server - You can override this behavior by setting '" + PluginSettings.PROXY_COPY_CONFIG.getPath() + "' to false.");
logger.warn(locale.getString(PluginLang.ENABLE_NOTIFY_PROXY_DISABLED_WEBSERVER, PluginSettings.PROXY_COPY_CONFIG.getPath()));
webServer.disable();
try {
config.set(WebserverSettings.DISABLED, true);
config.save();
logger.warn("Note: Set '" + WebserverSettings.DISABLED.getPath() + "' to true");
logger.warn(locale.getString(PluginLang.ENABLE_NOTIFY_SETTING_CHANGE, WebserverSettings.DISABLED.getPath(), "true"));
} catch (IOException e) {
errorLogger.warn(e, ErrorContext.builder()
.whatToDo("Set '" + WebserverSettings.DISABLED.getPath() + "' to true manually.")

View File

@ -0,0 +1,84 @@
/*
* 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;
import com.djrapitops.plan.utilities.dev.Untrusted;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* Simple guard against DDoS attacks to single endpoint.
* <p>
* This only protects against a DDoS that doesn't follow redirects.
*
* @author AuroraLS3
*/
public class RateLimitGuard {
private static final int ATTEMPT_LIMIT = 30;
private final Cache<String, Integer> requests = Caffeine.newBuilder()
.expireAfterWrite(120, TimeUnit.SECONDS)
.build();
private final Cache<String, String> lastRequestPath = Caffeine.newBuilder()
.expireAfterWrite(120, TimeUnit.SECONDS)
.build();
public boolean shouldPreventRequest(@Untrusted String accessor) {
Integer attempts = requests.getIfPresent(accessor);
if (attempts == null) return false;
// Too many attempts, forbid further attempts.
return attempts >= ATTEMPT_LIMIT;
}
public void increaseAttemptCount(@Untrusted String requestPath, @Untrusted String accessor) {
String previous = lastRequestPath.getIfPresent(accessor);
if (!Objects.equals(previous, requestPath)) {
resetAttemptCount(accessor);
}
Integer attempts = requests.getIfPresent(accessor);
if (attempts == null) {
attempts = 0;
}
lastRequestPath.put(accessor, requestPath);
requests.put(accessor, attempts + 1);
}
public void resetAttemptCount(@Untrusted String accessor) {
// previous request changed
requests.cleanUp();
requests.invalidate(accessor);
}
public static class Disabled extends RateLimitGuard {
@Override
public boolean shouldPreventRequest(String accessor) {
return false;
}
@Override
public void increaseAttemptCount(String requestPath, String accessor) { /* Disabled */ }
@Override
public void resetAttemptCount(String accessor) { /* Disabled */ }
}
}

View File

@ -20,6 +20,7 @@ import com.djrapitops.plan.delivery.domain.container.PlayerContainer;
import com.djrapitops.plan.delivery.domain.keys.PlayerKeys;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.BundleAddressCorrection;
import com.djrapitops.plan.delivery.rendering.html.icon.Family;
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
import com.djrapitops.plan.delivery.rendering.pages.Page;
@ -75,6 +76,7 @@ public class ResponseFactory {
private final DBSystem dbSystem;
private final Theme theme;
private final Lazy<Addresses> addresses;
private final Lazy<BundleAddressCorrection> bundleAddressCorrection;
private final Formatter<Long> httpLastModifiedFormatter;
@Inject
@ -86,7 +88,8 @@ public class ResponseFactory {
DBSystem dbSystem,
Formatters formatters,
Theme theme,
Lazy<Addresses> addresses
Lazy<Addresses> addresses,
Lazy<BundleAddressCorrection> bundleAddressCorrection
) {
this.files = files;
this.publicHtmlFiles = publicHtmlFiles;
@ -97,6 +100,7 @@ public class ResponseFactory {
this.addresses = addresses;
httpLastModifiedFormatter = formatters.httpLastModifiedLong();
this.bundleAddressCorrection = bundleAddressCorrection;
}
/**
@ -232,9 +236,7 @@ public class ResponseFactory {
String content = UnaryChain.of(resource.asString())
.chain(this::replaceMainAddressPlaceholder)
.chain(theme::replaceThemeColors)
.chain(contents -> StringUtils.replace(contents,
".p=\"/\"",
".p=\"" + getBasePath() + "/\""))
.chain(contents -> bundleAddressCorrection.get().correctAddressForWebserver(contents, fileName))
.apply();
ResponseBuilder responseBuilder = Response.builder()
.setMimeType(MimeType.JS)
@ -244,7 +246,7 @@ public class ResponseFactory {
if (fileName.contains(STATIC_BUNDLE_FOLDER)) {
resource.getLastModified().ifPresent(lastModified -> responseBuilder
// Can't cache main bundle in browser since base path might change
.setHeader(HttpHeader.CACHE_CONTROL.asString(), fileName.contains("main") ? CacheStrategy.CHECK_ETAG : CacheStrategy.CACHE_IN_BROWSER)
.setHeader(HttpHeader.CACHE_CONTROL.asString(), fileName.contains("index") ? CacheStrategy.CHECK_ETAG : CacheStrategy.CACHE_IN_BROWSER)
.setHeader(HttpHeader.LAST_MODIFIED.asString(), httpLastModifiedFormatter.apply(lastModified))
.setHeader(HttpHeader.ETAG.asString(), lastModified));
}
@ -254,12 +256,6 @@ public class ResponseFactory {
}
}
private String getBasePath() {
String address = addresses.get().getMainAddress()
.orElseGet(addresses.get()::getFallbackLocalhostAddress);
return addresses.get().getBasePath(address);
}
private String replaceMainAddressPlaceholder(String resource) {
String address = addresses.get().getAccessAddress()
.orElseGet(addresses.get()::getFallbackLocalhostAddress);
@ -275,7 +271,7 @@ public class ResponseFactory {
WebResource resource = getPublicOrJarResource(fileName);
String content = UnaryChain.of(resource.asString())
.chain(theme::replaceThemeColors)
.chain(contents -> StringUtils.replace(contents, "/static", getBasePath() + "/static"))
.chain(contents -> bundleAddressCorrection.get().correctAddressForWebserver(contents, fileName))
.apply();
ResponseBuilder responseBuilder = Response.builder()
@ -479,6 +475,14 @@ public class ResponseFactory {
.build();
}
public Response failedRateLimit403() {
return Response.builder()
.setMimeType(MimeType.HTML)
.setContent("<h1>403 Forbidden</h1><p>You are being rate-limited.</p>")
.setStatus(403)
.build();
}
public Response ipWhitelist403(@Untrusted String accessor) {
return Response.builder()
.setMimeType(MimeType.HTML)

View File

@ -147,8 +147,9 @@ public class ResponseResolver {
String plugin = "Plan";
resolverService.registerResolver(plugin, "/robots.txt", fileResolver(responseFactory::robotsResponse));
resolverService.registerResolver(plugin, "/manifest.json", fileResolver(() -> responseFactory.jsonFileResponse("manifest.json")));
resolverService.registerResolver(plugin, "/asset-manifest.json", fileResolver(() -> responseFactory.jsonFileResponse("asset-manifest.json")));
resolverService.registerResolver(plugin, "/favicon.ico", fileResolver(responseFactory::faviconResponse));
resolverService.registerResolver(plugin, "/logo192.png", fileResolver(() -> responseFactory.imageResponse("logo192.png")));
resolverService.registerResolver(plugin, "/logo512.png", fileResolver(() -> responseFactory.imageResponse("logo512.png")));
resolverService.registerResolver(plugin, "/pageExtensionApi.js", fileResolver(() -> responseFactory.javaScriptResponse("pageExtensionApi.js")));
resolverService.registerResolver(plugin, "/query", queryPageResolver);

View File

@ -61,6 +61,6 @@ public enum DataID {
public String of(ServerUUID serverUUID) {
if (serverUUID == null) return name();
return name() + '-' + serverUUID;
return name() + "_" + serverUUID;
}
}

View File

@ -75,8 +75,14 @@ public class AccessLogger {
}
}
try {
long timestamp = internalRequest.getTimestamp();
String accessAddress = internalRequest.getAccessAddress(webserverConfiguration);
String method = internalRequest.getMethod();
method = method != null ? method : "?";
String url = StoreRequestTransaction.getTruncatedURI(request, internalRequest);
int responseCode = response.getCode();
dbSystem.getDatabase().executeTransaction(
new StoreRequestTransaction(webserverConfiguration, internalRequest, request, response)
new StoreRequestTransaction(timestamp, accessAddress, method, url, responseCode)
);
} catch (CompletionException | DBOpException e) {
errorLogger.warn(e, ErrorContext.builder()

View File

@ -81,4 +81,6 @@ public interface InternalRequest {
}
return authenticationExtractor.extractAuthentication(this);
}
String getRequestedPath();
}

View File

@ -129,6 +129,11 @@ public class JettyInternalRequest implements InternalRequest {
return baseRequest.getRequestURI();
}
@Override
public String getRequestedPath() {
return baseRequest.getHttpURI().getDecodedPath();
}
@Override
public String toString() {
return "JettyInternalRequest{" +

View File

@ -19,6 +19,7 @@ package com.djrapitops.plan.delivery.webserver.http;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.webserver.PassBruteForceGuard;
import com.djrapitops.plan.delivery.webserver.RateLimitGuard;
import com.djrapitops.plan.delivery.webserver.ResponseFactory;
import com.djrapitops.plan.delivery.webserver.ResponseResolver;
import com.djrapitops.plan.delivery.webserver.auth.FailReason;
@ -40,6 +41,7 @@ public class RequestHandler {
private final ResponseResolver responseResolver;
private final PassBruteForceGuard bruteForceGuard;
private final RateLimitGuard rateLimitGuard;
private final AccessLogger accessLogger;
@Inject
@ -50,15 +52,23 @@ public class RequestHandler {
this.accessLogger = accessLogger;
bruteForceGuard = new PassBruteForceGuard();
rateLimitGuard = new RateLimitGuard();
}
public Response getResponse(InternalRequest internalRequest) {
@Untrusted String accessAddress = internalRequest.getAccessAddress(webserverConfiguration);
@Untrusted String requestedPath = internalRequest.getRequestedPath();
rateLimitGuard.increaseAttemptCount(requestedPath, accessAddress);
boolean blocked = false;
Response response;
@Untrusted Request request = null;
if (bruteForceGuard.shouldPreventRequest(accessAddress)) {
response = responseFactory.failedLoginAttempts403();
blocked = true;
} else if (rateLimitGuard.shouldPreventRequest(accessAddress)) {
response = responseFactory.failedRateLimit403();
blocked = true;
} else if (!webserverConfiguration.getAllowedIpList().isAllowed(accessAddress)) {
webserverConfiguration.getWebserverLogMessages()
.warnAboutWhitelistBlock(accessAddress, internalRequest.getRequestedURIString());
@ -77,7 +87,9 @@ public class RequestHandler {
response.getHeaders().putIfAbsent("Access-Control-Allow-Credentials", "true");
response.getHeaders().putIfAbsent("X-Robots-Tag", "noindex, nofollow");
accessLogger.log(internalRequest, request, response);
if (!blocked) {
accessLogger.log(internalRequest, request, response);
}
return response;
}

View File

@ -37,7 +37,7 @@ import java.util.Optional;
@Singleton
public class StaticResourceResolver implements NoAuthResolver {
private static final String PART_REGEX = "(vendor|css|js|img|static)";
private static final String PART_REGEX = "(static)";
public static final String PATH_REGEX = "^.*/" + PART_REGEX + "/.*";
private final ResponseFactory responseFactory;

View File

@ -214,7 +214,7 @@ public class GraphsJSONResolver extends JSONResolver {
case GRAPH_OPTIMIZED_PERFORMANCE:
return List.of(WebPermission.PAGE_SERVER_PERFORMANCE_GRAPHS, WebPermission.PAGE_NETWORK_PERFORMANCE);
case GRAPH_ONLINE:
return List.of(WebPermission.PAGE_SERVER_OVERVIEW_PLAYERS_ONLINE_GRAPH);
return List.of(WebPermission.PAGE_SERVER_OVERVIEW_PLAYERS_ONLINE_GRAPH, WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_ONLINE);
case GRAPH_UNIQUE_NEW:
return List.of(WebPermission.PAGE_SERVER_ONLINE_ACTIVITY_GRAPHS_DAY_BY_DAY);
case GRAPH_HOURLY_UNIQUE_NEW:
@ -250,6 +250,8 @@ public class GraphsJSONResolver extends JSONResolver {
return List.of(WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_DAY_BY_DAY);
case GRAPH_HOURLY_UNIQUE_NEW:
return List.of(WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_HOUR_BY_HOUR);
case GRAPH_CALENDAR:
return List.of(WebPermission.PAGE_NETWORK_OVERVIEW_GRAPHS_CALENDAR);
case GRAPH_SERVER_PIE:
return List.of(WebPermission.PAGE_NETWORK_SESSIONS_SERVER_PIE);
case GRAPH_WORLD_MAP:
@ -313,6 +315,8 @@ public class GraphsJSONResolver extends JSONResolver {
return graphJSON.uniqueAndNewGraphJSON();
case GRAPH_HOURLY_UNIQUE_NEW:
return graphJSON.hourlyUniqueAndNewGraphJSON();
case GRAPH_CALENDAR:
return graphJSON.networkCalendarJSON();
case GRAPH_SERVER_PIE:
return graphJSON.serverPreferencePieJSONAsMap();
case GRAPH_HOSTNAME_PIE:

Some files were not shown because too many files have changed in this diff Show More