diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 7f0708b4..c9a5538b 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -11,6 +11,7 @@ java { dependencies { compileOnly(libs.paperApi) compileOnlyApi(libs.checkerQual) + compileOnlyApi(libs.adventureApi) } indra { diff --git a/api/src/main/java/xyz/jpenilla/squaremap/api/HtmlComponentSerializer.java b/api/src/main/java/xyz/jpenilla/squaremap/api/HtmlComponentSerializer.java new file mode 100644 index 00000000..85d4114b --- /dev/null +++ b/api/src/main/java/xyz/jpenilla/squaremap/api/HtmlComponentSerializer.java @@ -0,0 +1,35 @@ +package xyz.jpenilla.squaremap.api; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.flattener.ComponentFlattener; +import net.kyori.adventure.text.serializer.ComponentEncoder; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.jetbrains.annotations.ApiStatus; + +/** + * Safely encodes {@link Component Components} as HTML text. + * + *

Mostly useful for marker tooltips.

+ * + * @see Squaremap#htmlComponentSerializer() + */ +@DefaultQualifier(NonNull.class) +public interface HtmlComponentSerializer extends ComponentEncoder { + + /** + * Create a new {@link HtmlComponentSerializer} using the provided {@link ComponentFlattener}. + * + * @param flattener component flattener + * @return serializer + */ + static HtmlComponentSerializer withFlattener(final ComponentFlattener flattener) { + return ProviderHolder.HTML_SERIALIZER.create(flattener); + } + + @ApiStatus.Internal + interface Provider { + HtmlComponentSerializer create(ComponentFlattener flattener); + } + +} diff --git a/api/src/main/java/xyz/jpenilla/squaremap/api/HtmlStripper.java b/api/src/main/java/xyz/jpenilla/squaremap/api/HtmlStripper.java new file mode 100644 index 00000000..2b4e40cd --- /dev/null +++ b/api/src/main/java/xyz/jpenilla/squaremap/api/HtmlStripper.java @@ -0,0 +1,36 @@ +package xyz.jpenilla.squaremap.api; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.jetbrains.annotations.ApiStatus; + +/** + * Strips HTML tags from text. Allows sanitizing untrusted strings before use in + * marker tooltips. + */ +@DefaultQualifier(NonNull.class) +public interface HtmlStripper { + + /** + * Get an {@link HtmlStripper}. + * + * @return HTML stripper + */ + static HtmlStripper htmlStripper() { + return ProviderHolder.HTML_STRIPPER.instance(); + } + + /** + * Strips HTML tags from the provided string. + * + * @param string untrusted string + * @return sanitized string + */ + String stripHtml(String string); + + @ApiStatus.Internal + interface Provider { + HtmlStripper instance(); + } + +} diff --git a/api/src/main/java/xyz/jpenilla/squaremap/api/ProviderHolder.java b/api/src/main/java/xyz/jpenilla/squaremap/api/ProviderHolder.java new file mode 100644 index 00000000..b823a03b --- /dev/null +++ b/api/src/main/java/xyz/jpenilla/squaremap/api/ProviderHolder.java @@ -0,0 +1,13 @@ +package xyz.jpenilla.squaremap.api; + +import net.kyori.adventure.util.Services; + +final class ProviderHolder { + static final HtmlComponentSerializer.Provider HTML_SERIALIZER = service(HtmlComponentSerializer.Provider.class); + static final HtmlStripper.Provider HTML_STRIPPER = service(HtmlStripper.Provider.class); + + private static T service(final Class clazz) { + return Services.service(clazz) + .orElseThrow(() -> new IllegalStateException("Could not find " + clazz.getName() + " implementation")); + } +} diff --git a/api/src/main/java/xyz/jpenilla/squaremap/api/Squaremap.java b/api/src/main/java/xyz/jpenilla/squaremap/api/Squaremap.java index a13d98b5..76e36b7e 100644 --- a/api/src/main/java/xyz/jpenilla/squaremap/api/Squaremap.java +++ b/api/src/main/java/xyz/jpenilla/squaremap/api/Squaremap.java @@ -4,6 +4,7 @@ import java.nio.file.Path; import java.util.Collection; import java.util.Optional; +import net.kyori.adventure.text.flattener.ComponentFlattener; import org.checkerframework.checker.nullness.qual.NonNull; /** @@ -63,4 +64,11 @@ public interface Squaremap { */ @NonNull Path webDir(); + /** + * Get an {@link HtmlComponentSerializer} using the platform {@link ComponentFlattener}. + * + * @return serializer + */ + @NonNull HtmlComponentSerializer htmlComponentSerializer(); + } diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/SquaremapApiProvider.java b/common/src/main/java/xyz/jpenilla/squaremap/common/SquaremapApiProvider.java index 18a7f8db..a936640a 100644 --- a/common/src/main/java/xyz/jpenilla/squaremap/common/SquaremapApiProvider.java +++ b/common/src/main/java/xyz/jpenilla/squaremap/common/SquaremapApiProvider.java @@ -1,6 +1,7 @@ package xyz.jpenilla.squaremap.common; import com.google.inject.Inject; +import com.google.inject.Provider; import com.google.inject.Singleton; import java.awt.image.BufferedImage; import java.nio.file.Path; @@ -8,8 +9,10 @@ import java.util.Collections; import java.util.Optional; import java.util.function.Function; +import net.kyori.adventure.text.flattener.ComponentFlattener; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultQualifier; +import xyz.jpenilla.squaremap.api.HtmlComponentSerializer; import xyz.jpenilla.squaremap.api.MapWorld; import xyz.jpenilla.squaremap.api.PlayerManager; import xyz.jpenilla.squaremap.api.Registry; @@ -24,17 +27,20 @@ public final class SquaremapApiProvider implements Squaremap { private final PlayerManager playerManager; private final WorldManager worldManager; private final IconRegistry iconRegistry; + private final Provider flattener; @Inject private SquaremapApiProvider( final DirectoryProvider directoryProvider, final AbstractPlayerManager playerManager, - final WorldManager worldManager + final WorldManager worldManager, + final Provider flattener ) { this.directoryProvider = directoryProvider; this.playerManager = playerManager; this.worldManager = worldManager; this.iconRegistry = new IconRegistry(directoryProvider); + this.flattener = flattener; } @Override @@ -61,4 +67,9 @@ public PlayerManager playerManager() { public Path webDir() { return this.directoryProvider.webDirectory(); } + + @Override + public HtmlComponentSerializer htmlComponentSerializer() { + return HtmlComponentSerializer.withFlattener(this.flattener.get()); + } } diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/task/UpdatePlayers.java b/common/src/main/java/xyz/jpenilla/squaremap/common/task/UpdatePlayers.java index 1d635656..acc7b1a6 100644 --- a/common/src/main/java/xyz/jpenilla/squaremap/common/task/UpdatePlayers.java +++ b/common/src/main/java/xyz/jpenilla/squaremap/common/task/UpdatePlayers.java @@ -16,13 +16,13 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; +import xyz.jpenilla.squaremap.api.HtmlComponentSerializer; import xyz.jpenilla.squaremap.common.AbstractPlayerManager; import xyz.jpenilla.squaremap.common.ServerAccess; import xyz.jpenilla.squaremap.common.config.ConfigManager; import xyz.jpenilla.squaremap.common.config.WorldConfig; import xyz.jpenilla.squaremap.common.data.DirectoryProvider; import xyz.jpenilla.squaremap.common.util.FileUtil; -import xyz.jpenilla.squaremap.common.util.HtmlComponentSerializer; import xyz.jpenilla.squaremap.common.util.Util; @DefaultQualifier(NonNull.class) diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlComponentSerializer.java b/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlComponentSerializer.java deleted file mode 100644 index 7cd2292c..00000000 --- a/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlComponentSerializer.java +++ /dev/null @@ -1,15 +0,0 @@ -package xyz.jpenilla.squaremap.common.util; - -import net.kyori.adventure.text.ComponentLike; -import net.kyori.adventure.text.flattener.ComponentFlattener; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.framework.qual.DefaultQualifier; - -@DefaultQualifier(NonNull.class) -public interface HtmlComponentSerializer { - static HtmlComponentSerializer withFlattener(ComponentFlattener flattener) { - return new HtmlComponentSerializerImpl(flattener); - } - - String serialize(ComponentLike componentLike); -} diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlComponentSerializerImpl.java b/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlComponentSerializerImpl.java index 8fef9da8..4e74c941 100644 --- a/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlComponentSerializerImpl.java +++ b/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlComponentSerializerImpl.java @@ -3,7 +3,7 @@ import java.util.ArrayDeque; import java.util.Deque; import java.util.concurrent.ThreadLocalRandom; -import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.flattener.ComponentFlattener; import net.kyori.adventure.text.flattener.FlattenerListener; import net.kyori.adventure.text.format.Style; @@ -15,6 +15,7 @@ import org.checkerframework.framework.qual.DefaultQualifier; import org.owasp.html.PolicyFactory; import org.owasp.html.Sanitizers; +import xyz.jpenilla.squaremap.api.HtmlComponentSerializer; @DefaultQualifier(NonNull.class) final class HtmlComponentSerializerImpl implements HtmlComponentSerializer { @@ -27,7 +28,7 @@ final class HtmlComponentSerializerImpl implements HtmlComponentSerializer { } @Override - public String serialize(final ComponentLike componentLike) { + public String serialize(final Component componentLike) { final HtmlFlattener state = new HtmlFlattener(); this.flattener.flatten(componentLike.asComponent(), state); return SANITIZER.sanitize(state.toString()); @@ -118,4 +119,11 @@ private static String asHtml(final TextFormat format) { throw new IllegalArgumentException("Cannot handle format: " + format + " (" + format.getClass().getTypeName() + ")"); } } + + public static final class Provider implements HtmlComponentSerializer.Provider { + @Override + public HtmlComponentSerializer create(final ComponentFlattener flattener) { + return new HtmlComponentSerializerImpl(flattener); + } + } } diff --git a/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlStripperImpl.java b/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlStripperImpl.java new file mode 100644 index 00000000..9a76ab5d --- /dev/null +++ b/common/src/main/java/xyz/jpenilla/squaremap/common/util/HtmlStripperImpl.java @@ -0,0 +1,31 @@ +package xyz.jpenilla.squaremap.common.util; + +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.owasp.html.HtmlPolicyBuilder; +import org.owasp.html.PolicyFactory; +import xyz.jpenilla.squaremap.api.HtmlStripper; + +@DefaultQualifier(NonNull.class) +public final class HtmlStripperImpl implements HtmlStripper { + private static final PolicyFactory SANITIZER = new HtmlPolicyBuilder().toFactory(); + + private HtmlStripperImpl() { + } + + @Override + public String stripHtml(final String string) { + Objects.requireNonNull(string, "Parameter 'string' must not be null"); + return SANITIZER.sanitize(string); + } + + public static final class Provider implements HtmlStripper.Provider { + private static final HtmlStripper INSTANCE = new HtmlStripperImpl(); + + @Override + public HtmlStripper instance() { + return INSTANCE; + } + } +} diff --git a/common/src/main/resources/META-INF/services/xyz.jpenilla.squaremap.api.HtmlComponentSerializer$Provider b/common/src/main/resources/META-INF/services/xyz.jpenilla.squaremap.api.HtmlComponentSerializer$Provider new file mode 100644 index 00000000..3c5a71e7 --- /dev/null +++ b/common/src/main/resources/META-INF/services/xyz.jpenilla.squaremap.api.HtmlComponentSerializer$Provider @@ -0,0 +1 @@ +xyz.jpenilla.squaremap.common.util.HtmlComponentSerializerImpl$Provider diff --git a/common/src/main/resources/META-INF/services/xyz.jpenilla.squaremap.api.HtmlStripper$Provider b/common/src/main/resources/META-INF/services/xyz.jpenilla.squaremap.api.HtmlStripper$Provider new file mode 100644 index 00000000..0b536061 --- /dev/null +++ b/common/src/main/resources/META-INF/services/xyz.jpenilla.squaremap.api.HtmlStripper$Provider @@ -0,0 +1 @@ +xyz.jpenilla.squaremap.common.util.HtmlStripperImpl$Provider