From 7a73301a3718867391c552840294be7ff798be73 Mon Sep 17 00:00:00 2001 From: Pokechu22 Date: Sun, 9 Jun 2019 07:56:58 -0700 Subject: [PATCH] Add permissions for individual colors (#1441) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add tests for existing format behavior * Replace formatting implementation * Add permissions for individual color codes Resolves #415 * Use format code names * Fix escaping * Mockito: test scope only * Explicitly check the .magic permission Once I switch to checking if a perm's set in the loop, the explicit check is needed for an * perm. * Add support for removing individual colors * Use `obfuscated` as the name for §k `magic` is still accepted as the group name, so this is not a breaking change. --- .../src/com/earth2me/essentials/IUser.java | 2 + .../src/com/earth2me/essentials/User.java | 23 ++++ .../essentials/perm/IPermissionsHandler.java | 3 + .../essentials/perm/PermissionsHandler.java | 5 + .../perm/impl/SuperpermsHandler.java | 5 + .../earth2me/essentials/utils/FormatUtil.java | 111 +++++++++++++----- .../essentials/utils/FormatUtilTest.java | 108 +++++++++++++++++ pom.xml | 6 + 8 files changed, 236 insertions(+), 27 deletions(-) create mode 100644 Essentials/test/com/earth2me/essentials/utils/FormatUtilTest.java diff --git a/Essentials/src/com/earth2me/essentials/IUser.java b/Essentials/src/com/earth2me/essentials/IUser.java index f9b0c2954..d1a5c94e9 100644 --- a/Essentials/src/com/earth2me/essentials/IUser.java +++ b/Essentials/src/com/earth2me/essentials/IUser.java @@ -22,6 +22,8 @@ public interface IUser { boolean isAuthorized(IEssentialsCommand cmd, String permissionPrefix); + boolean isPermissionSet(String node); + void healCooldown() throws Exception; void giveMoney(BigDecimal value) throws MaxMoneyException; diff --git a/Essentials/src/com/earth2me/essentials/User.java b/Essentials/src/com/earth2me/essentials/User.java index e9ac59ed5..856bc55f9 100644 --- a/Essentials/src/com/earth2me/essentials/User.java +++ b/Essentials/src/com/earth2me/essentials/User.java @@ -97,6 +97,11 @@ public class User extends UserData implements Comparable, IMessageRecipien return result; } + @Override + public boolean isPermissionSet(final String node) { + return isPermSetCheck(node); + } + private boolean isAuthorizedCheck(final String node) { if (base instanceof OfflinePlayer) { @@ -116,6 +121,24 @@ public class User extends UserData implements Comparable, IMessageRecipien } } + private boolean isPermSetCheck(final String node) { + if (base instanceof OfflinePlayer) { + return false; + } + + try { + return ess.getPermissionsHandler().isPermissionSet(base, node); + } catch (Exception ex) { + if (ess.getSettings().isDebug()) { + ess.getLogger().log(Level.SEVERE, "Permission System Error: " + ess.getPermissionsHandler().getName() + " returned: " + ex.getMessage(), ex); + } else { + ess.getLogger().log(Level.SEVERE, "Permission System Error: " + ess.getPermissionsHandler().getName() + " returned: " + ex.getMessage()); + } + + return false; + } + } + @Override public void healCooldown() throws Exception { final Calendar now = new GregorianCalendar(); diff --git a/Essentials/src/com/earth2me/essentials/perm/IPermissionsHandler.java b/Essentials/src/com/earth2me/essentials/perm/IPermissionsHandler.java index bae9eeb58..e0d50a10d 100644 --- a/Essentials/src/com/earth2me/essentials/perm/IPermissionsHandler.java +++ b/Essentials/src/com/earth2me/essentials/perm/IPermissionsHandler.java @@ -16,6 +16,9 @@ public interface IPermissionsHandler { boolean hasPermission(Player base, String node); + // Does not check for * permissions + boolean isPermissionSet(Player base, String node); + String getPrefix(Player base); String getSuffix(Player base); diff --git a/Essentials/src/com/earth2me/essentials/perm/PermissionsHandler.java b/Essentials/src/com/earth2me/essentials/perm/PermissionsHandler.java index 3d3d6e50d..58c3acf07 100644 --- a/Essentials/src/com/earth2me/essentials/perm/PermissionsHandler.java +++ b/Essentials/src/com/earth2me/essentials/perm/PermissionsHandler.java @@ -62,6 +62,11 @@ public class PermissionsHandler implements IPermissionsHandler { return handler.hasPermission(base, node); } + @Override + public boolean isPermissionSet(final Player base, final String node) { + return handler.isPermissionSet(base, node); + } + @Override public String getPrefix(final Player base) { final long start = System.nanoTime(); diff --git a/Essentials/src/com/earth2me/essentials/perm/impl/SuperpermsHandler.java b/Essentials/src/com/earth2me/essentials/perm/impl/SuperpermsHandler.java index a989d3af4..4d31c0525 100644 --- a/Essentials/src/com/earth2me/essentials/perm/impl/SuperpermsHandler.java +++ b/Essentials/src/com/earth2me/essentials/perm/impl/SuperpermsHandler.java @@ -49,6 +49,11 @@ public class SuperpermsHandler implements IPermissionsHandler { } } + @Override + public boolean isPermissionSet(final Player base, final String node) { + return base.isPermissionSet(node); + } + @Override public String getPrefix(final Player base) { return null; diff --git a/Essentials/src/com/earth2me/essentials/utils/FormatUtil.java b/Essentials/src/com/earth2me/essentials/utils/FormatUtil.java index 7de0a4726..f77843384 100644 --- a/Essentials/src/com/earth2me/essentials/utils/FormatUtil.java +++ b/Essentials/src/com/earth2me/essentials/utils/FormatUtil.java @@ -3,23 +3,25 @@ package com.earth2me.essentials.utils; import net.ess3.api.IUser; import org.bukkit.ChatColor; +import java.util.EnumSet; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Matcher; import java.util.regex.Pattern; public class FormatUtil { + private static final Set COLORS = EnumSet.of(ChatColor.BLACK, ChatColor.DARK_BLUE, ChatColor.DARK_GREEN, ChatColor.DARK_AQUA, ChatColor.DARK_RED, ChatColor.DARK_PURPLE, ChatColor.GOLD, ChatColor.GRAY, ChatColor.DARK_GRAY, ChatColor.BLUE, ChatColor.GREEN, ChatColor.AQUA, ChatColor.RED, ChatColor.LIGHT_PURPLE, ChatColor.YELLOW, ChatColor.WHITE); + private static final Set FORMATS = EnumSet.of(ChatColor.BOLD, ChatColor.STRIKETHROUGH, ChatColor.UNDERLINE, ChatColor.ITALIC, ChatColor.RESET); + private static final Set MAGIC = EnumSet.of(ChatColor.MAGIC); + //Vanilla patterns used to strip existing formats - static final transient Pattern VANILLA_COLOR_PATTERN = Pattern.compile("\u00a7+[0-9A-Fa-f]"); - static final transient Pattern VANILLA_MAGIC_PATTERN = Pattern.compile("\u00a7+[Kk]"); - static final transient Pattern VANILLA_FORMAT_PATTERN = Pattern.compile("\u00a7+[L-ORl-or]"); + private static final Pattern STRIP_ALL_PATTERN = Pattern.compile("\u00a7+([0-9a-fk-orA-FK-OR])"); //Essentials '&' convention colour codes - static final transient Pattern REPLACE_ALL_PATTERN = Pattern.compile("(? supported) { + StringBuffer builder = new StringBuffer(); + Matcher matcher = REPLACE_ALL_PATTERN.matcher(input); + searchLoop: while (matcher.find()) { + boolean isEscaped = (matcher.group(1) != null); + if (!isEscaped) { + char code = matcher.group(2).toLowerCase(Locale.ROOT).charAt(0); + for (ChatColor color : supported) { + if (color.getChar() == code) { + matcher.appendReplacement(builder, "\u00a7$2"); + continue searchLoop; + } + } + } + // Don't change & to section sign (or replace two &'s with one) + matcher.appendReplacement(builder, "&$2"); + } + matcher.appendTail(builder); + return builder.toString(); + } + + static String stripColor(final String input, final Set strip) { + StringBuffer builder = new StringBuffer(); + Matcher matcher = STRIP_ALL_PATTERN.matcher(input); + searchLoop: while (matcher.find()) { + char code = matcher.group(1).toLowerCase(Locale.ROOT).charAt(0); + for (ChatColor color : strip) { + if (color.getChar() == code) { + matcher.appendReplacement(builder, ""); + continue searchLoop; + } + } + // Don't replace + matcher.appendReplacement(builder, "$0"); + } + matcher.appendTail(builder); + return builder.toString(); } //This is the general permission sensitive message format function, does not touch urls. - public static String formatString(final IUser user, final String permBase, final String input) { - if (input == null) { + public static String formatString(final IUser user, final String permBase, String message) { + if (message == null) { return null; } - String message; + EnumSet supported = EnumSet.noneOf(ChatColor.class); if (user.isAuthorized(permBase + ".color")) { - message = replaceColor(input, REPLACE_COLOR_PATTERN); - } else { - message = stripColor(input, VANILLA_COLOR_PATTERN); - } - if (user.isAuthorized(permBase + ".magic")) { - message = replaceColor(message, REPLACE_MAGIC_PATTERN); - } else { - message = stripColor(message, VANILLA_MAGIC_PATTERN); + supported.addAll(COLORS); } if (user.isAuthorized(permBase + ".format")) { - message = replaceColor(message, REPLACE_FORMAT_PATTERN); - } else { - message = stripColor(message, VANILLA_FORMAT_PATTERN); + supported.addAll(FORMATS); + } + if (user.isAuthorized(permBase + ".magic")) { + supported.addAll(MAGIC); + } + for (ChatColor chatColor : ChatColor.values()) { + String colorName = chatColor.name(); + if (chatColor == ChatColor.MAGIC) { + // Bukkit's name doesn't match with vanilla's + colorName = "obfuscated"; + } + + final String node = permBase + "." + colorName.toLowerCase(Locale.ROOT); + // Only handle individual colors that are explicitly added or removed. + if (!user.isPermissionSet(node)) { + continue; + } + if (user.isAuthorized(node)) { + supported.add(chatColor); + } else { + supported.remove(chatColor); + } + } + EnumSet strip = EnumSet.complementOf(supported); + + if (!supported.isEmpty()) { + message = replaceColor(message, supported); + } + if (!strip.isEmpty()) { + message = stripColor(message, strip); } return message; } diff --git a/Essentials/test/com/earth2me/essentials/utils/FormatUtilTest.java b/Essentials/test/com/earth2me/essentials/utils/FormatUtilTest.java new file mode 100644 index 000000000..0f922742e --- /dev/null +++ b/Essentials/test/com/earth2me/essentials/utils/FormatUtilTest.java @@ -0,0 +1,108 @@ +package com.earth2me.essentials.utils; + +import net.ess3.api.IUser; + +import org.junit.Test; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +public class FormatUtilTest { + + @Test + public void testFormatCase() { + checkFormatPerms("&aT&Aest", "&aT&Aest"); + checkFormatPerms("§aT§Aest", "Test"); + + checkFormatPerms("&aT&Aest", "§aT§Aest", "color"); + checkFormatPerms("§aT§Aest", "§aT§Aest", "color"); + } + + @Test + public void testFormatCategoryPerms() { + checkFormatPerms("Test", "Test"); + checkFormatPerms("Test", "Test", "color", "format"); + + checkFormatPerms("&1C&2o&3l&4o&5r&6m&7a&8t&9i&ac", "&1C&2o&3l&4o&5r&6m&7a&8t&9i&ac"); // Unchanged + checkFormatPerms("§1C§2o§3l§4o§5r§6m§7a§8t§9i§ac", "Colormatic"); // Removed + checkFormatPerms("&1C&2o&3l&4o&5r&6m&7a&8t&9i&ac", "§1C§2o§3l§4o§5r§6m§7a§8t§9i§ac", "color"); // Converted + checkFormatPerms("§1C§2o§3l§4o§5r§6m§7a§8t§9i§ac", "§1C§2o§3l§4o§5r§6m§7a§8t§9i§ac", "color"); // Unchanged + + checkFormatPerms("&kFUNKY LOL", "§kFUNKY LOL", "magic"); // Converted + checkFormatPerms("§kFUNKY LOL", "§kFUNKY LOL", "magic"); // Unchanged + + // Magic isn't included in the format group + checkFormatPerms("&kFUNKY LOL", "&kFUNKY LOL"); // Unchanged + checkFormatPerms("§kFUNKY LOL", "FUNKY LOL"); // Removed + checkFormatPerms("&kFUNKY LOL", "&kFUNKY LOL", "format"); // Unchanged + checkFormatPerms("§kFUNKY LOL", "FUNKY LOL", "format"); // Removed + + checkFormatPerms("&f<est", "&f<est"); + checkFormatPerms("§f§ltest", "test"); + checkFormatPerms("&f<est", "§f<est", "color"); + checkFormatPerms("§f§ltest", "§ftest", "color"); + checkFormatPerms("&f<est", "&f§ltest", "format"); + checkFormatPerms("§f§ltest", "§ltest", "format"); + checkFormatPerms("&f<est", "§f§ltest", "color", "format"); + checkFormatPerms("§f§ltest", "§f§ltest", "color", "format"); + } + + @Test + public void testFormatCodePerms() { + checkFormatPerms("&1Te&2st", "&1Te&2st"); + checkFormatPerms("§1Te§2st", "Test"); + + checkFormatPerms("&1Te&2st", "§1Te&2st", "dark_blue"); + checkFormatPerms("§1Te§2st", "§1Test", "dark_blue"); + + checkFormatPerms("&1Te&2st", "&1Te§2st", "dark_green"); + checkFormatPerms("§1Te§2st", "Te§2st", "dark_green"); + + checkFormatPerms("&1Te&2st", "§1Te§2st", "dark_blue", "dark_green"); + checkFormatPerms("§1Te§2st", "§1Te§2st", "dark_blue", "dark_green"); + + // Obfuscated behaves the same as magic + checkFormatPerms("&kFUNKY LOL", "§kFUNKY LOL", "obfuscated"); + checkFormatPerms("§kFUNKY LOL", "§kFUNKY LOL", "obfuscated"); + } + + @Test + public void testFormatAddRemovePerms() { + checkFormatPerms("&1Te&2st&ling", "&1Te§2st&ling", "color", "-dark_blue"); + checkFormatPerms("§1Te§2st§ling", "Te§2sting", "color", "-dark_blue"); + + // Nothing happens when negated without being previously present + checkFormatPerms("&1Te&2st&ling", "&1Te§2st&ling", "color", "-dark_blue", "-bold"); + checkFormatPerms("§1Te§2st§ling", "Te§2sting", "color", "-dark_blue", "-bold"); + } + + @Test + public void testFormatEscaping() { + // Don't do anything to non-format codes + checkFormatPerms("True & false", "True & false"); + checkFormatPerms("True && false", "True && false"); + + // Formats are only unescaped if you have the right perms + checkFormatPerms("This is &&a message", "This is &&a message"); + checkFormatPerms("This is &&a message", "This is &a message", "color"); + + // Can't put an & before a non-escaped format + checkFormatPerms("This is &&&a message", "This is &&&a message"); + checkFormatPerms("This is &&&a message", "This is &&a message", "color"); + } + + private void checkFormatPerms(String input, String expectedOutput, String... perms) { + IUser user = mock(IUser.class); + for (String perm : perms) { + if (perm.startsWith("-")) { + // Negated perms + perm = perm.substring(1); + when(user.isAuthorized("essentials.chat." + perm)).thenReturn(false); + } else { + when(user.isAuthorized("essentials.chat." + perm)).thenReturn(true); + } + + when(user.isPermissionSet("essentials.chat." + perm)).thenReturn(true); + } + assertEquals(expectedOutput, FormatUtil.formatString(user, "essentials.chat", input)); + } +} diff --git a/pom.xml b/pom.xml index 8513d2108..182d44944 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,12 @@ 4.12 test + + org.mockito + mockito-core + 2.8.47 + test +