diff --git a/src/main/java/world/bentobox/bentobox/api/commands/admin/AdminWhyCommand.java b/src/main/java/world/bentobox/bentobox/api/commands/admin/AdminWhyCommand.java new file mode 100644 index 000000000..32c3a24a7 --- /dev/null +++ b/src/main/java/world/bentobox/bentobox/api/commands/admin/AdminWhyCommand.java @@ -0,0 +1,72 @@ +package world.bentobox.bentobox.api.commands.admin; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.bukkit.metadata.FixedMetadataValue; + +import world.bentobox.bentobox.api.commands.CompositeCommand; +import world.bentobox.bentobox.api.commands.ConfirmableCommand; +import world.bentobox.bentobox.api.localization.TextVariables; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.util.Util; + +public class AdminWhyCommand extends ConfirmableCommand { + + public AdminWhyCommand(CompositeCommand parent) { + super(parent, "why"); + } + + @Override + public void setup() { + setPermission("admin.why"); + setParametersHelp("commands.admin.why.parameters"); + setDescription("commands.admin.why.description"); + } + + @Override + public boolean execute(User user, String label, List args) { + // If args are not right, show help + if (args.size() != 1) { + showHelp(this, user); + return false; + } + // Get target + UUID targetUUID = getPlayers().getUUID(args.get(0)); + if (targetUUID == null) { + user.sendMessage("general.errors.unknown-player", TextVariables.NAME, args.get(0)); + return false; + } + // Set meta data on player + User target = User.getInstance(targetUUID); + if (!target.isOnline()) { + user.sendMessage("general.errors.offline-player"); + return false; + } + // Determine the debug mode and toggle if required + boolean newValue = !target.getPlayer().getMetadata(getWorld().getName() + "_why_debug").stream() + .filter(p -> p.getOwningPlugin().equals(getPlugin())).findFirst().map(p -> p.asBoolean()).orElse(false); + if (newValue) { + user.sendMessage("commands.admin.why.turning-on", TextVariables.NAME, target.getName()); + } else { + user.sendMessage("commands.admin.why.turning-off", TextVariables.NAME, target.getName()); + } + // Set the debug meta + target.getPlayer().setMetadata(getWorld().getName() + "_why_debug", new FixedMetadataValue(getPlugin(), newValue)); + return true; + } + + @Override + public Optional> tabComplete(User user, String alias, List args) { + String lastArg = !args.isEmpty() ? args.get(args.size()-1) : ""; + if (args.isEmpty()) { + // Don't show every player on the server. Require at least the first letter + return Optional.empty(); + } + List options = new ArrayList<>(Util.getOnlinePlayerList(user)); + return Optional.of(Util.tabLimit(options, lastArg)); + } + +} \ No newline at end of file diff --git a/src/main/java/world/bentobox/bentobox/api/flags/AbstractFlagListener.java b/src/main/java/world/bentobox/bentobox/api/flags/AbstractFlagListener.java index 1a7cf7c4b..8cb5757ab 100644 --- a/src/main/java/world/bentobox/bentobox/api/flags/AbstractFlagListener.java +++ b/src/main/java/world/bentobox/bentobox/api/flags/AbstractFlagListener.java @@ -15,6 +15,7 @@ import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.managers.IslandWorldManager; import world.bentobox.bentobox.managers.IslandsManager; +import world.bentobox.bentobox.util.Util; /** * Abstract class for flag listeners. Provides common code. @@ -23,8 +24,33 @@ import world.bentobox.bentobox.managers.IslandsManager; */ public abstract class AbstractFlagListener implements Listener { + /** + * Reason for why flag was allowed or disallowed + * Used by admins for debugging player actions + * + */ + enum Why { + + UNPROTECTED_WORLD, + OP, + BYPASS_EVERYWHERE, + BYPASS_ISLAND, + RANK_ALLOWED, + ALLOWED_IN_WORLD, + ALLOWED_ON_ISLAND, + NOT_ALLOWED_ON_ISLAND, + NOT_ALLOWED_IN_WORLD, + ERROR_NO_ASSOCIATED_USER, + NOT_SET, + SETTING_ALLOWED_ON_ISLAND, + SETTING_NOT_ALLOWED_ON_ISLAND, + SETTING_ALLOWED_IN_WORLD, + SETTING_NOT_ALLOWED_IN_WORLD + } + private BentoBox plugin = BentoBox.getInstance(); private User user = null; + private Why why; /** * @return the plugin @@ -127,6 +153,7 @@ public abstract class AbstractFlagListener implements Listener { || (plugin.getIWM().isNether(loc.getWorld()) && !plugin.getIWM().isNetherIslands(loc.getWorld())) || (plugin.getIWM().isEnd(loc.getWorld()) && !plugin.getIWM().isEndIslands(loc.getWorld())) ) { + report(user, e, loc, flag, Why.UNPROTECTED_WORLD); return true; } // Get the island and if present @@ -134,6 +161,11 @@ public abstract class AbstractFlagListener implements Listener { // Handle Settings Flag if (flag.getType().equals(Flag.Type.SETTING)) { // If the island exists, return the setting, otherwise return the default setting for this flag + if (island.isPresent()) { + report(user, e, loc, flag, island.map(x -> x.isAllowed(flag)).orElse(false) ? Why.SETTING_ALLOWED_ON_ISLAND : Why.SETTING_NOT_ALLOWED_ON_ISLAND); + } else { + report(user, e, loc, flag, flag.isSetForWorld(loc.getWorld()) ? Why.SETTING_ALLOWED_IN_WORLD : Why.SETTING_NOT_ALLOWED_IN_WORLD); + } return island.map(x -> x.isAllowed(flag)).orElse(flag.isSetForWorld(loc.getWorld())); } @@ -144,11 +176,17 @@ public abstract class AbstractFlagListener implements Listener { // TODO: is this the correct handling here? if (user == null && !createEventUser(e)) { plugin.logError("Check island had no associated user! " + e.getEventName()); + report(user, e, loc, flag, Why.ERROR_NO_ASSOCIATED_USER); return false; } // Ops or "bypass everywhere" moderators can do anything - if (user.isOp() || user.hasPermission(getIWM().getPermissionPrefix(loc.getWorld()) + ".mod.bypass." + flag.getID() + ".everywhere")) { + if (user.isOp() || user.hasPermission(getIWM().getPermissionPrefix(loc.getWorld()) + ".mod.bypass." + flag.getID() + ".everywhere")) { + if (user.isOp()) { + report(user, e, loc, flag, Why.OP); + } else { + report(user, e, loc, flag, Why.BYPASS_EVERYWHERE); + } user = null; return true; } @@ -158,27 +196,43 @@ public abstract class AbstractFlagListener implements Listener { if (island.isPresent()) { // If it is not allowed on the island, "bypass island" moderators can do anything - if (!island.get().isAllowed(user, flag) && !user.hasPermission(getIWM().getPermissionPrefix(loc.getWorld()) + ".mod.bypass." + flag.getID() + ".island")) { - noGo(e, flag, silent); - // Clear the user for the next time - user = null; - return false; - } else { + if (island.get().isAllowed(user, flag)) { + report(user, e, loc, flag, Why.RANK_ALLOWED); user = null; return true; - } + } else if (user.hasPermission(getIWM().getPermissionPrefix(loc.getWorld()) + ".mod.bypass." + flag.getID() + ".island")) { + report(user, e, loc, flag, Why.BYPASS_ISLAND); + user = null; + return true; + } + report(user, e, loc, flag, Why.NOT_ALLOWED_ON_ISLAND); + noGo(e, flag, silent); + // Clear the user for the next time + user = null; + return false; } // The player is in the world, but not on an island, so general world settings apply - if (!flag.isSetForWorld(loc.getWorld())) { + if (flag.isSetForWorld(loc.getWorld())) { + report(user, e, loc, flag, Why.ALLOWED_IN_WORLD); + user = null; + return true; + } else { + report(user, e, loc, flag, Why.NOT_ALLOWED_IN_WORLD); noGo(e, flag, silent); user = null; return false; - } else { - user = null; - return true; } } + private void report(User user, Event e, Location loc, Flag flag, Why why) { + if (user != null && user.getPlayer().getMetadata(loc.getWorld().getName() + "_why_debug").stream() + .filter(p -> p.getOwningPlugin().equals(getPlugin())).findFirst().map(p -> p.asBoolean()).orElse(false)) { + plugin.log("Why: " + e.getEventName() + " in world " + loc.getWorld().getName() + " at " + Util.xyz(loc.toVector())); + plugin.log("Why: " + (user == null ? "Unknown" : user.getName()) + " " + flag.getID() + " - " + why.name()); + } + + } + /** * Get the flag for this ID * @param id - the flag ID @@ -203,4 +257,12 @@ public abstract class AbstractFlagListener implements Listener { protected IslandWorldManager getIWM() { return plugin.getIWM(); } + + /** + * Get why the flag was allowed or not allowed + * @return the why - reason + */ + public Why getWhy() { + return why; + } } diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index ff7e9f623..256b5d88b 100644 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -192,6 +192,11 @@ commands: description: "deletes a player's island" cannot-delete-team-leader: "&cAll island members have to be kicked from the island before deleting it." deleted-island: "&aIsland at &e[xyz] &ahas been successfully deleted." + why: + parameters: "" + description: "Toggle console protection debug reporting" + turning-on: "Turning on console debug for [name]" + turning-off: "Turning off console debug for [name]" bentobox: description: "BentoBox admin command" about: