diff --git a/README.md b/README.md index 23ee2143..bc6fb379 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ | `chatformatter.translatable` | `` | | `chatformatter.keybind` | `` | | `chatformatter.nbt` | `` | +| `chatformatter.unsafe` | **Required** to use unsafe tags (``, ``, etc.) in addition to the specific tag permission. | | `chatformatter.reload` | `/chatformatter reload` | | `chatformatter.receiveupdates` | receive update announcements for this plugin | diff --git a/chatformatter-core/src/main/java/com/eternalcode/formatter/ChatHandlerImpl.java b/chatformatter-core/src/main/java/com/eternalcode/formatter/ChatHandlerImpl.java index 0b82fb04..6d9e1e77 100644 --- a/chatformatter-core/src/main/java/com/eternalcode/formatter/ChatHandlerImpl.java +++ b/chatformatter-core/src/main/java/com/eternalcode/formatter/ChatHandlerImpl.java @@ -28,6 +28,7 @@ class ChatHandlerImpl implements ChatHandler { private static final String PERMISSION_ALL = "chatformatter.*"; + private static final String PERMISSION_UNSAFE = "chatformatter.unsafe"; private static final Map TAG_RESOLVERS_BY_PERMISSION = new ImmutableMap.Builder() .put("chatformatter.decorations.*", StandardTags.decorations()) .put("chatformatter.decorations.bold", StandardTags.decorations(TextDecoration.BOLD)) @@ -142,14 +143,22 @@ private TagResolver.Single messagePlaceholder(Player sender, String rawMessage) } private TagResolver providePermittedTags(Player player) { - List tagResolvers = new ArrayList<>(); + boolean isUnsafeAllowed = player.hasPermission(PERMISSION_UNSAFE); - if (player.hasPermission(PERMISSION_ALL)) { + if (isUnsafeAllowed && player.hasPermission(PERMISSION_ALL)) { return TagResolver.standard(); } + List tagResolvers = new ArrayList<>(); + for (Map.Entry entry : TAG_RESOLVERS_BY_PERMISSION.entrySet()) { - if (player.hasPermission(entry.getKey())) { + String permission = entry.getKey(); + + if (this.isUnsafePermission(permission) && !isUnsafeAllowed) { + continue; + } + + if (player.hasPermission(permission) || player.hasPermission(PERMISSION_ALL)) { tagResolvers.add(entry.getValue()); } } @@ -157,4 +166,13 @@ private TagResolver providePermittedTags(Player player) { return TagResolver.resolver(tagResolvers); } + private boolean isUnsafePermission(String permission) { + return permission.contains("click") + || permission.contains("hover") + || permission.contains("insertion") + || permission.contains("score") + || permission.contains("selector") + || permission.contains("nbt"); + } + } diff --git a/chatformatter-core/src/test/java/com/eternalcode/formatter/ChatHandlerSafeTest.java b/chatformatter-core/src/test/java/com/eternalcode/formatter/ChatHandlerSafeTest.java new file mode 100644 index 00000000..fa97445a --- /dev/null +++ b/chatformatter-core/src/test/java/com/eternalcode/formatter/ChatHandlerSafeTest.java @@ -0,0 +1,108 @@ +package com.eternalcode.formatter; + +import com.eternalcode.formatter.config.PluginConfig; +import com.eternalcode.formatter.placeholder.PlaceholderRegistry; +import com.eternalcode.formatter.rank.ChatRankProvider; +import com.eternalcode.formatter.template.TemplateService; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ChatHandlerSafeTest { + + private PluginConfig config; + private ChatHandlerImpl chatHandler; + private Player player; + + @BeforeEach + void setup() { + config = new PluginConfig(); + ChatRankProvider rankProvider = mock(ChatRankProvider.class); + PlaceholderRegistry placeholderRegistry = mock(PlaceholderRegistry.class); + TemplateService templateService = mock(TemplateService.class); + + when(rankProvider.getRank(any())).thenReturn("default"); + when(placeholderRegistry.format(anyString(), any())).thenAnswer(inv -> { + String s = inv.getArgument(0); + return s.replace("{message}", "") + .replace("{displayname}", "Player") + .replace("{member}", "") + .replace("$hoverName(Player)", ""); + }); + when(templateService.applyTemplates(anyString())).thenAnswer(inv -> inv.getArgument(0)); + + MiniMessage miniMessage = MiniMessage.miniMessage(); + + chatHandler = new ChatHandlerImpl(miniMessage, config, rankProvider, placeholderRegistry, templateService); + player = mock(Player.class); + } + + @Test + void testUnsafeTagsWithoutPermission() { + when(player.hasPermission("chatformatter.click")).thenReturn(true); + when(player.hasPermission("chatformatter.unsafe")).thenReturn(false); + when(player.getDisplayName()).thenReturn("Player"); + when(player.getName()).thenReturn("Player"); + + String rawMessage = "Click me"; + String jsonMessage = GsonComponentSerializer.gson().serialize(Component.text(rawMessage)); + + ChatMessage chatMessage = new ChatMessage(player, Optional.empty(), jsonMessage); + + ChatRenderedMessage result = chatHandler.process(chatMessage); + String json = result.jsonMessage(); + + assertFalse(json.contains("\"clickEvent\""), + "Should not contain click event without unsafe permission. JSON: " + json); + } + + @Test + void testUnsafeTagsWithPermission() { + when(player.hasPermission("chatformatter.click")).thenReturn(true); + when(player.hasPermission("chatformatter.unsafe")).thenReturn(true); + when(player.getDisplayName()).thenReturn("Player"); + when(player.getName()).thenReturn("Player"); + + String rawMessage = "Click me"; + String jsonMessage = GsonComponentSerializer.gson().serialize(Component.text(rawMessage)); + + ChatMessage chatMessage = new ChatMessage(player, Optional.empty(), jsonMessage); + + ChatRenderedMessage result = chatHandler.process(chatMessage); + String json = result.jsonMessage(); + + assertTrue(json.contains("\"clickEvent\""), "Should contain click event with unsafe permission. JSON: " + json); + } + + @Test + void testWildcardPermission() { + when(player.hasPermission("chatformatter.*")).thenReturn(true); + when(player.hasPermission("chatformatter.unsafe")).thenReturn(true); + when(player.getDisplayName()).thenReturn("Player"); + when(player.getName()).thenReturn("Player"); + + String rawMessage = "Click me"; + String jsonMessage = GsonComponentSerializer.gson().serialize(Component.text(rawMessage)); + + ChatMessage chatMessage = new ChatMessage(player, Optional.empty(), jsonMessage); + + ChatRenderedMessage result = chatHandler.process(chatMessage); + String json = result.jsonMessage(); + + assertTrue(json.contains("\"clickEvent\""), + "Should contain click event with wildcard permission. JSON: " + json); + } +} diff --git a/chatformatter-paper-plugin/build.gradle.kts b/chatformatter-paper-plugin/build.gradle.kts index 95141264..f693aa8b 100644 --- a/chatformatter-paper-plugin/build.gradle.kts +++ b/chatformatter-paper-plugin/build.gradle.kts @@ -40,7 +40,8 @@ bukkit { "chatformatter.selector", "chatformatter.keybind", "chatformatter.newline", - "chatformatter.rainbow" + "chatformatter.rainbow", + "chatformatter.unsafe" ) default = Default.OP } @@ -69,6 +70,10 @@ bukkit { register("chatformatter.keybind") { default = Default.OP } register("chatformatter.newline") { default = Default.OP } register("chatformatter.rainbow") { default = Default.OP } + register("chatformatter.unsafe") { + description = "Allows using unsafe MiniMessage tags like and " + default = Default.OP + } register("chatformatter.color.*") { children = listOf(