Add permissions for individual colors (#1441)

* 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.
This commit is contained in:
Pokechu22 2019-06-09 07:56:58 -07:00 committed by md678685
parent 5a0b7285d4
commit 7a73301a37
8 changed files with 236 additions and 27 deletions

View File

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

View File

@ -97,6 +97,11 @@ public class User extends UserData implements Comparable<User>, 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<User>, 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();

View File

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

View File

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

View File

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

View File

@ -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<ChatColor> 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<ChatColor> FORMATS = EnumSet.of(ChatColor.BOLD, ChatColor.STRIKETHROUGH, ChatColor.UNDERLINE, ChatColor.ITALIC, ChatColor.RESET);
private static final Set<ChatColor> 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("(?<!&)&([0-9a-fk-orA-FK-OR])");
static final transient Pattern REPLACE_COLOR_PATTERN = Pattern.compile("(?<!&)&([0-9a-fA-F])");
static final transient Pattern REPLACE_MAGIC_PATTERN = Pattern.compile("(?<!&)&([Kk])");
static final transient Pattern REPLACE_FORMAT_PATTERN = Pattern.compile("(?<!&)&([l-orL-OR])");
static final transient Pattern REPLACE_PATTERN = Pattern.compile("&&(?=[0-9a-fk-orA-FK-OR])");
private static final Pattern REPLACE_ALL_PATTERN = Pattern.compile("(&)?&([0-9a-fk-orA-FK-OR])");
//Used to prepare xmpp output
static final transient Pattern LOGCOLOR_PATTERN = Pattern.compile("\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]");
static final transient Pattern URL_PATTERN = Pattern.compile("((?:(?:https?)://)?[\\w-_\\.]{2,})\\.([a-zA-Z]{2,3}(?:/\\S+)?)");
private static final Pattern LOGCOLOR_PATTERN = Pattern.compile("\\x1B\\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]");
private static final Pattern URL_PATTERN = Pattern.compile("((?:(?:https?)://)?[\\w-_\\.]{2,})\\.([a-zA-Z]{2,3}(?:/\\S+)?)");
public static final Pattern IPPATTERN = Pattern.compile("^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
//This method is used to simply strip the native minecraft colour codes
@ -55,33 +57,88 @@ public class FormatUtil {
if (input == null) {
return null;
}
return replaceColor(input, REPLACE_ALL_PATTERN);
return replaceColor(input, EnumSet.allOf(ChatColor.class));
}
static String replaceColor(final String input, final Pattern pattern) {
return REPLACE_PATTERN.matcher(pattern.matcher(input).replaceAll("\u00a7$1")).replaceAll("&");
static String replaceColor(final String input, final Set<ChatColor> 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<ChatColor> 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<ChatColor> 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<ChatColor> strip = EnumSet.complementOf(supported);
if (!supported.isEmpty()) {
message = replaceColor(message, supported);
}
if (!strip.isEmpty()) {
message = stripColor(message, strip);
}
return message;
}

View File

@ -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&ltest", "&f&ltest");
checkFormatPerms("§f§ltest", "test");
checkFormatPerms("&f&ltest", "§f&ltest", "color");
checkFormatPerms("§f§ltest", "§ftest", "color");
checkFormatPerms("&f&ltest", "&f§ltest", "format");
checkFormatPerms("§f§ltest", "§ltest", "format");
checkFormatPerms("&f&ltest", "§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));
}
}

View File

@ -67,6 +67,12 @@
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.8.47</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>