diff --git a/SpigotCore_Main/src/de/steamwar/command/CommandNoHelpException.java b/SpigotCore_Main/src/de/steamwar/command/CommandNoHelpException.java new file mode 100644 index 0000000..41b487b --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/command/CommandNoHelpException.java @@ -0,0 +1,26 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2020 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.command; + +class CommandNoHelpException extends RuntimeException { + + CommandNoHelpException() { + } +} diff --git a/SpigotCore_Main/src/de/steamwar/command/GuardCheckType.java b/SpigotCore_Main/src/de/steamwar/command/GuardCheckType.java new file mode 100644 index 0000000..0f023b8 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/command/GuardCheckType.java @@ -0,0 +1,26 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2020 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.command; + +public enum GuardCheckType { + COMMAND, + HELP_COMMAND, + TAB_COMPLETE +} diff --git a/SpigotCore_Main/src/de/steamwar/command/GuardChecker.java b/SpigotCore_Main/src/de/steamwar/command/GuardChecker.java new file mode 100644 index 0000000..e8f3a1f --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/command/GuardChecker.java @@ -0,0 +1,30 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2020 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.command; + +import org.bukkit.command.CommandSender; + +@FunctionalInterface +public interface GuardChecker { + /** + * While guarding the first parameter of the command the parameter s of this method is {@code null} + */ + GuardResult guard(CommandSender commandSender, GuardCheckType guardCheckType, String[] previousArguments, String s); +} diff --git a/SpigotCore_Main/src/de/steamwar/command/GuardResult.java b/SpigotCore_Main/src/de/steamwar/command/GuardResult.java new file mode 100644 index 0000000..9ffbf77 --- /dev/null +++ b/SpigotCore_Main/src/de/steamwar/command/GuardResult.java @@ -0,0 +1,26 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2020 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.command; + +public enum GuardResult { + ALLOWED, + DENIED_WITH_HELP, + DENIED +} diff --git a/SpigotCore_Main/src/de/steamwar/command/SWCommand.java b/SpigotCore_Main/src/de/steamwar/command/SWCommand.java index 1db2988..99864c2 100644 --- a/SpigotCore_Main/src/de/steamwar/command/SWCommand.java +++ b/SpigotCore_Main/src/de/steamwar/command/SWCommand.java @@ -39,6 +39,7 @@ public abstract class SWCommand { private final List commandList = new ArrayList<>(); private final List commandHelpList = new ArrayList<>(); private final Map> localTypeMapper = new HashMap<>(); + private final Map localGuardChecker = new HashMap<>(); protected SWCommand(String command) { this(command, new String[0]); @@ -51,8 +52,12 @@ public abstract class SWCommand { if (!initialized) { createMapping(); } - if (!commandList.stream().anyMatch(s -> s.invoke(sender, args))) { - commandHelpList.stream().anyMatch(s -> s.invoke(sender, args)); + try { + if (!commandList.stream().anyMatch(s -> s.invoke(sender, args))) { + commandHelpList.stream().anyMatch(s -> s.invoke(sender, args)); + } + } catch (CommandNoHelpException e) { + // Ignored } return false; } @@ -85,6 +90,13 @@ public abstract class SWCommand { addMapper(ClassMapper.class, method, i -> i == 0, false, TypeMapper.class, (anno, typeMapper) -> { (anno.local() ? localTypeMapper : SWCommandUtils.MAPPER_FUNCTIONS).putIfAbsent(anno.value().getTypeName(), typeMapper); }); + addGuard(Guard.class, method, i -> i == 0, false, GuardChecker.class, (anno, guardChecker) -> { + (anno.local() ? localGuardChecker : SWCommandUtils.GUARD_FUNCTIONS).putIfAbsent(anno.value(), guardChecker); + }); + addGuard(ClassGuard.class, method, i -> i == 0, false, GuardChecker.class, (anno, guardChecker) -> { + (anno.local() ? localGuardChecker : SWCommandUtils.GUARD_FUNCTIONS).putIfAbsent(anno.value().getTypeName(), guardChecker); + }); + add(Register.class, method, i -> i > 0, true, null, (anno, parameters) -> { if (!anno.help()) return; if (parameters.length != 2) { @@ -97,7 +109,7 @@ public abstract class SWCommand { Bukkit.getLogger().log(Level.WARNING, () -> "The method '" + method.toString() + "' is lacking the varArgs parameters of type '" + String.class.getTypeName() + "' as last Argument"); return; } - commandHelpList.add(new SubCommand(this, method, anno.value(), new HashMap<>())); + commandHelpList.add(new SubCommand(this, method, anno.value(), new HashMap<>(), localGuardChecker, true)); }); } for (Method method : methods) { @@ -119,7 +131,7 @@ public abstract class SWCommand { return; } } - commandList.add(new SubCommand(this, method, anno.value(), localTypeMapper)); + commandList.add(new SubCommand(this, method, anno.value(), localTypeMapper, localGuardChecker, false)); }); this.commandList.sort((o1, o2) -> { @@ -167,6 +179,17 @@ public abstract class SWCommand { }); } + private void addGuard(Class annotation, Method method, IntPredicate parameterTester, boolean firstParameter, Class returnType, BiConsumer consumer) { + add(annotation, method, parameterTester, firstParameter, returnType, (anno, parameters) -> { + try { + method.setAccessible(true); + consumer.accept(anno, (GuardChecker) method.invoke(this)); + } catch (Exception e) { + throw new SecurityException(e.getMessage(), e); + } + }); + } + public void unregister() { SWCommandUtils.knownCommandMap.remove(command.getName()); command.getAliases().forEach(SWCommandUtils.knownCommandMap::remove); @@ -207,4 +230,20 @@ public abstract class SWCommand { boolean local() default false; } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.PARAMETER, ElementType.METHOD}) + protected @interface Guard { + String value() default ""; + + boolean local() default false; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD}) + protected @interface ClassGuard { + Class value(); + + boolean local() default false; + } } diff --git a/SpigotCore_Main/src/de/steamwar/command/SWCommandUtils.java b/SpigotCore_Main/src/de/steamwar/command/SWCommandUtils.java index 64c735d..52e362e 100644 --- a/SpigotCore_Main/src/de/steamwar/command/SWCommandUtils.java +++ b/SpigotCore_Main/src/de/steamwar/command/SWCommandUtils.java @@ -44,6 +44,7 @@ public class SWCommandUtils { } static final Map> MAPPER_FUNCTIONS = new HashMap<>(); + static final Map GUARD_FUNCTIONS = new HashMap<>(); static final TypeMapper ERROR_FUNCTION = createMapper(s -> { throw new SecurityException(); @@ -100,7 +101,7 @@ public class SWCommandUtils { } } - static Object[] generateArgumentArray(CommandSender commandSender, TypeMapper[] parameters, String[] args, Class varArgType, String[] subCommand) throws CommandParseException { + static Object[] generateArgumentArray(CommandSender commandSender, TypeMapper[] parameters, GuardChecker[] guards, String[] args, Class varArgType, String[] subCommand) throws CommandParseException { Object[] arguments = new Object[parameters.length + 1]; int index = 0; while (index < subCommand.length) { @@ -142,6 +143,14 @@ public class SWCommandUtils { MAPPER_FUNCTIONS.putIfAbsent(name, mapper); } + public static void addGuard(Class clazz, GuardChecker guardChecker) { + addGuard(clazz.getTypeName(), guardChecker); + } + + public static void addGuard(String name, GuardChecker guardChecker) { + GUARD_FUNCTIONS.putIfAbsent(name, guardChecker); + } + public static TypeMapper createMapper(Function mapper, Function> tabCompleter) { return createMapper(mapper, (commandSender, s) -> tabCompleter.apply(s)); } diff --git a/SpigotCore_Main/src/de/steamwar/command/SubCommand.java b/SpigotCore_Main/src/de/steamwar/command/SubCommand.java index 15dad9a..3cd6778 100644 --- a/SpigotCore_Main/src/de/steamwar/command/SubCommand.java +++ b/SpigotCore_Main/src/de/steamwar/command/SubCommand.java @@ -19,6 +19,7 @@ package de.steamwar.command; +import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import java.lang.reflect.InvocationTargetException; @@ -27,6 +28,7 @@ import java.lang.reflect.Parameter; import java.util.*; import java.util.function.Function; import java.util.function.Predicate; +import java.util.logging.Level; import static de.steamwar.command.SWCommandUtils.*; @@ -36,20 +38,26 @@ class SubCommand { private Method method; String[] subCommand; TypeMapper[] arguments; + GuardChecker[] guards; private Predicate commandSenderPredicate; private Function commandSenderFunction; + private GuardChecker guardChecker; Class varArgType = null; + private boolean help; - SubCommand(SWCommand swCommand, Method method, String[] subCommand, Map> localTypeMapper) { + SubCommand(SWCommand swCommand, Method method, String[] subCommand, Map> localTypeMapper, Map localGuardChecker, boolean help) { this.swCommand = swCommand; this.method = method; + this.help = help; Parameter[] parameters = method.getParameters(); commandSenderPredicate = sender -> parameters[0].getType().isAssignableFrom(sender.getClass()); commandSenderFunction = sender -> parameters[0].getType().cast(sender); this.subCommand = subCommand; + guardChecker = getGuardChecker(parameters[0], localGuardChecker); arguments = new TypeMapper[parameters.length - 1]; + guards = new GuardChecker[parameters.length - 1]; for (int i = 1; i < parameters.length; i++) { Parameter parameter = parameters[i]; Class clazz = parameter.getType(); @@ -76,9 +84,29 @@ class SubCommand { arguments[i - 1] = localTypeMapper.containsKey(name) ? localTypeMapper.get(name) : MAPPER_FUNCTIONS.getOrDefault(name, ERROR_FUNCTION); + guards[i - 1] = getGuardChecker(parameter, localGuardChecker); } } + private GuardChecker getGuardChecker(Parameter parameter, Map localGuardChecker) { + SWCommand.Guard guard = parameter.getAnnotation(SWCommand.Guard.class); + if (guard != null) { + if (guard.value() == null || guard.value().isEmpty()) { + String s = parameter.getType().getTypeName(); + if (parameter.isVarArgs()) { + s = parameter.getType().getComponentType().getTypeName(); + } + return localGuardChecker.getOrDefault(s, GUARD_FUNCTIONS.getOrDefault(s, null)); + } + GuardChecker current = localGuardChecker.getOrDefault(guard.value(), GUARD_FUNCTIONS.getOrDefault(guard.value(), null)); + if (guardChecker == null) { + Bukkit.getLogger().log(Level.WARNING, () -> "The guard checker with name '" + guard.value() + "' is neither a local guard checker nor a global one"); + } + return current; + } + return null; + } + boolean invoke(CommandSender commandSender, String[] args) { if (args.length < arguments.length + subCommand.length - (varArgType != null ? 1 : 0)) { return false; @@ -90,10 +118,38 @@ class SubCommand { if (!commandSenderPredicate.test(commandSender)) { return false; } - Object[] objects = SWCommandUtils.generateArgumentArray(commandSender, arguments, args, varArgType, subCommand); + Object[] objects = SWCommandUtils.generateArgumentArray(commandSender, arguments, guards, args, varArgType, subCommand); objects[0] = commandSenderFunction.apply(commandSender); + for (int i = subCommand.length; i < args.length; i++) { + GuardChecker current; + if (i == subCommand.length) { + current = guardChecker; + } else { + if (i >= objects.length + subCommand.length) { + current = guards[guards.length - 1]; + } else { + current = guards[i - 1 - subCommand.length]; + } + } + if (current != null) { + GuardResult guardResult; + if (i == 0) { + guardResult = current.guard(commandSender, help ? GuardCheckType.HELP_COMMAND : GuardCheckType.COMMAND, new String[0], null); + } else { + guardResult = current.guard(commandSender, help ? GuardCheckType.HELP_COMMAND : GuardCheckType.COMMAND, Arrays.copyOf(args, i - 1), args[i - 1]); + } + if (guardResult != GuardResult.ALLOWED) { + if (guardResult == GuardResult.DENIED) { + throw new CommandNoHelpException(); + } + return false; + } + } + } method.setAccessible(true); method.invoke(swCommand, objects); + } catch (CommandNoHelpException e) { + throw e; } catch (IllegalAccessException | RuntimeException | InvocationTargetException e) { throw new SecurityException(e.getMessage(), e); } catch (CommandParseException e) { @@ -106,6 +162,9 @@ class SubCommand { if (varArgType == null && args.length > arguments.length + subCommand.length) { return null; } + if (guardChecker != null && guardChecker.guard(commandSender, GuardCheckType.TAB_COMPLETE, new String[0], null) != GuardResult.ALLOWED) { + return null; + } int index = 0; List argsList = new LinkedList<>(Arrays.asList(args)); for (String value : subCommand) { @@ -114,12 +173,19 @@ class SubCommand { if (!value.equalsIgnoreCase(s)) return null; index++; } + int guardIndex = 0; for (TypeMapper argument : arguments) { String s = argsList.remove(0); if (argsList.isEmpty()) { + if (guards[guardIndex] != null && guards[guardIndex].guard(commandSender, GuardCheckType.TAB_COMPLETE, Arrays.copyOf(args, args.length - 1), s) != GuardResult.ALLOWED) { + return null; + } return argument.tabCompletes(commandSender, Arrays.copyOf(args, args.length - 1), s); } try { + if (guards[guardIndex] != null && guards[guardIndex].guard(commandSender, GuardCheckType.TAB_COMPLETE, Arrays.copyOf(args, index), s) != GuardResult.ALLOWED) { + return null; + } if (argument.map(commandSender, Arrays.copyOf(args, index), s) == null) { return null; } @@ -127,14 +193,21 @@ class SubCommand { return null; } index++; + guardIndex++; } if (varArgType != null && !argsList.isEmpty()) { while (!argsList.isEmpty()) { String s = argsList.remove(0); if (argsList.isEmpty()) { + if (guards[guards.length - 1] != null && guards[guards.length - 1].guard(commandSender, GuardCheckType.TAB_COMPLETE, Arrays.copyOf(args, args.length - 1), s) != GuardResult.ALLOWED) { + return null; + } return arguments[arguments.length - 1].tabCompletes(commandSender, Arrays.copyOf(args, args.length - 1), s); } try { + if (guards[guards.length - 1] != null && guards[guards.length - 1].guard(commandSender, GuardCheckType.TAB_COMPLETE, Arrays.copyOf(args, index), s) != GuardResult.ALLOWED) { + return null; + } if (arguments[arguments.length - 1].map(commandSender, Arrays.copyOf(args, index), s) == null) { return null; }