diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java index c910cb929..ec4cfd830 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java @@ -20,7 +20,7 @@ public class DebugCommand implements ExecutableCommand { private Factory debugSectionFactory; private Set> sectionClasses = - ImmutableSet.of(PermissionGroups.class, TestEmailSender.class); + ImmutableSet.of(PermissionGroups.class, TestEmailSender.class, PlayerAuthViewer.class, LimboPlayerViewer.class); private Map sections; diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java new file mode 100644 index 000000000..e2861ca4f --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java @@ -0,0 +1,55 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import org.bukkit.Location; + +import java.math.RoundingMode; +import java.text.DecimalFormat; + +/** + * Utilities used within the DebugSection implementations. + */ +final class DebugSectionUtils { + + private DebugSectionUtils() { + } + + /** + * Formats the given location in a human readable way. Null-safe. + * + * @param location the location to format + * @return the formatted location + */ + static String formatLocation(Location location) { + if (location == null) { + return "null"; + } + + String worldName = location.getWorld() == null ? "null" : location.getWorld().getName(); + return formatLocation(location.getX(), location.getY(), location.getZ(), worldName); + } + + /** + * Formats the given location in a human readable way. + * + * @param x the x coordinate + * @param y the y coordinate + * @param z the z coordinate + * @param world the world name + * @return the formatted location + */ + static String formatLocation(double x, double y, double z, String world) { + return "(" + round(x) + ", " + round(y) + ", " + round(z) + ") in '" + world + "'"; + } + + /** + * Rounds the given number to two decimals. + * + * @param number the number to round + * @return the rounded number + */ + private static String round(double number) { + DecimalFormat df = new DecimalFormat("#.##"); + df.setRoundingMode(RoundingMode.HALF_UP); + return df.format(number); + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/LimboPlayerViewer.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/LimboPlayerViewer.java new file mode 100644 index 000000000..5ac00fb18 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/LimboPlayerViewer.java @@ -0,0 +1,147 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.data.limbo.LimboPlayer; +import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.service.BukkitService; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import javax.inject.Inject; +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; + +import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation; + +/** + * Shows the data stored in LimboPlayers and the equivalent properties on online players. + */ +class LimboPlayerViewer implements DebugSection { + + @Inject + private LimboService limboService; + + @Inject + private BukkitService bukkitService; + + private Field limboServiceEntries; + + @Override + public String getName() { + return "limbo"; + } + + @Override + public String getDescription() { + return "View LimboPlayers and player's \"limbo stats\""; + } + + @Override + public void execute(CommandSender sender, List arguments) { + if (arguments.isEmpty()) { + sender.sendMessage("/authme debug limbo : show a player's limbo info"); + sender.sendMessage("Available limbo records: " + getLimboKeys()); + return; + } + + LimboPlayer limbo = limboService.getLimboPlayer(arguments.get(0)); + Player player = bukkitService.getPlayerExact(arguments.get(0)); + if (limbo == null && player == null) { + sender.sendMessage("No limbo info and no player online with name '" + arguments.get(0) + "'"); + return; + } + + sender.sendMessage(ChatColor.GOLD + "Showing limbo / player info for '" + arguments.get(0) + "'"); + new InfoDisplayer(sender, limbo, player) + .sendEntry("Is op", LimboPlayer::isOperator, Player::isOp) + .sendEntry("Walk speed", LimboPlayer::getWalkSpeed, Player::getWalkSpeed) + .sendEntry("Can fly", LimboPlayer::isCanFly, Player::getAllowFlight) + .sendEntry("Fly speed", LimboPlayer::getFlySpeed, Player::getFlySpeed) + .sendEntry("Location", l -> formatLocation(l.getLocation()), p -> formatLocation(p.getLocation())) + .sendEntry("Group", LimboPlayer::getGroup, p -> ""); + sender.sendMessage("Note: group is only shown for LimboPlayer"); + } + + /** + * Gets the names of the LimboPlayers in the LimboService. As we don't want to expose this + * information in non-debug settings, this is done over reflections. Since this is not a + * crucial feature, we generously catch all Exceptions + * + * @return player names for which there is a LimboPlayer (or error message upon failure) + */ + @SuppressWarnings("unchecked") + private Set getLimboKeys() { + // Lazy initialization + if (limboServiceEntries == null) { + try { + Field limboServiceEntries = LimboService.class.getDeclaredField("entries"); + limboServiceEntries.setAccessible(true); + this.limboServiceEntries = limboServiceEntries; + } catch (Exception e) { + ConsoleLogger.logException("Could not retrieve LimboService entries field:", e); + return Collections.singleton("Error retrieving LimboPlayer collection"); + } + } + + try { + return (Set) ((Map) limboServiceEntries.get(limboService)).keySet(); + } catch (Exception e) { + ConsoleLogger.logException("Could not retrieve LimboService values:", e); + return Collections.singleton("Error retrieving LimboPlayer values"); + } + } + + /** + * Displays the info for the given LimboPlayer and Player to the provided CommandSender. + */ + private static final class InfoDisplayer { + private final CommandSender sender; + private final Optional limbo; + private final Optional player; + + /** + * Constructor. + * + * @param sender command sender to send the information to + * @param limbo the limbo player to get data from + * @param player the player to get data from + */ + InfoDisplayer(CommandSender sender, LimboPlayer limbo, Player player) { + this.sender = sender; + this.limbo = Optional.ofNullable(limbo); + this.player = Optional.ofNullable(player); + + if (limbo == null) { + sender.sendMessage("Note: no Limbo information available"); + } else if (player == null) { + sender.sendMessage("Note: player is not online"); + } + } + + /** + * Displays a piece of information to the command sender. + * + * @param title the designation of the piece of information + * @param limboGetter getter for data retrieval on the LimboPlayer + * @param playerGetter getter for data retrieval on Player + * @param the data type + * @return this instance (for chaining) + */ + InfoDisplayer sendEntry(String title, + Function limboGetter, + Function playerGetter) { + sender.sendMessage( + title + ": " + + limbo.map(limboGetter).map(String::valueOf).orElse("--") + + " / " + + player.map(playerGetter).map(String::valueOf).orElse("--")); + return this; + } + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java new file mode 100644 index 000000000..a985c8271 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/PlayerAuthViewer.java @@ -0,0 +1,104 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.security.crypts.HashedPassword; +import fr.xephi.authme.util.StringUtils; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import javax.inject.Inject; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation; + +/** + * Allows to view the data of a PlayerAuth in the database. + */ +class PlayerAuthViewer implements DebugSection { + + @Inject + private DataSource dataSource; + + @Override + public String getName() { + return "db"; + } + + @Override + public String getDescription() { + return "View player's data in the database"; + } + + @Override + public void execute(CommandSender sender, List arguments) { + if (arguments.isEmpty()) { + sender.sendMessage("Enter player name to view his data in the database."); + sender.sendMessage("Example: /authme debug db Bobby"); + return; + } + + PlayerAuth auth = dataSource.getAuth(arguments.get(0)); + if (auth == null) { + sender.sendMessage("No record exists for '" + arguments.get(0) + "'"); + } else { + displayAuthToSender(auth, sender); + } + } + + /** + * Outputs the PlayerAuth information to the given sender. + * + * @param auth the PlayerAuth to display + * @param sender the sender to send the messages to + */ + private void displayAuthToSender(PlayerAuth auth, CommandSender sender) { + sender.sendMessage(ChatColor.GOLD + "[AuthMe] Player " + auth.getNickname() + " / " + auth.getRealName()); + sender.sendMessage("Email: " + auth.getEmail() + ". IP: " + auth.getIp() + ". Group: " + auth.getGroupId()); + sender.sendMessage("Quit location: " + + formatLocation(auth.getQuitLocX(), auth.getQuitLocY(), auth.getQuitLocZ(), auth.getWorld())); + sender.sendMessage("Last login: " + formatLastLogin(auth)); + + HashedPassword hashedPass = auth.getPassword(); + sender.sendMessage("Hash / salt (partial): '" + safeSubstring(hashedPass.getHash(), 6) + + "' / '" + safeSubstring(hashedPass.getSalt(), 4) + "'"); + } + + /** + * Fail-safe substring method. Guarantees not to show the entire String. + * + * @param str the string to transform + * @param length number of characters to show from the start of the String + * @return the first length characters of the string, or half of the string if it is shorter, + * or empty string if the string is null or empty + */ + private static String safeSubstring(String str, int length) { + if (StringUtils.isEmpty(str)) { + return ""; + } else if (str.length() < length) { + return str.substring(0, str.length() / 2) + "..."; + } else { + return str.substring(0, length) + "..."; + } + } + + /** + * Formats the last login date from the given PlayerAuth. + * + * @param auth the auth object + * @return the last login as human readable date + */ + private static String formatLastLogin(PlayerAuth auth) { + long lastLogin = auth.getLastLogin(); + if (lastLogin == 0) { + return "Never (0)"; + } else { + LocalDateTime date = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastLogin), ZoneId.systemDefault()); + return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(date); + } + } +} diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java index b0abcd557..1d5052548 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/TestEmailSender.java @@ -41,8 +41,8 @@ class TestEmailSender implements DebugSection { @Override public void execute(CommandSender sender, List arguments) { if (!sendMailSSL.hasAllInformation()) { - sender.sendMessage(ChatColor.RED + "You haven't set all required configurations in config.yml " + - "for sending emails. Please check your config.yml"); + sender.sendMessage(ChatColor.RED + "You haven't set all required configurations in config.yml " + + "for sending emails. Please check your config.yml"); return; } @@ -69,7 +69,8 @@ class TestEmailSender implements DebugSection { } String email = auth.getEmail(); if (email == null || "your@email.com".equals(email)) { - sender.sendMessage(ChatColor.RED + "No email set for your account! Please use /authme debug mail "); + sender.sendMessage(ChatColor.RED + "No email set for your account!" + + " Please use /authme debug mail "); return null; } return email; diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtilsTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtilsTest.java new file mode 100644 index 000000000..caf617883 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtilsTest.java @@ -0,0 +1,46 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.TestHelper; +import org.bukkit.Location; +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; + +/** + * Test for {@link DebugSectionUtils}. + */ +public class DebugSectionUtilsTest { + + @Test + public void shouldFormatLocation() { + // given / when + String result = DebugSectionUtils.formatLocation(0.0, 10.248592, -18934.2349023, "Main"); + + // then + assertThat(result, equalTo("(0, 10.25, -18934.23) in 'Main'")); + } + + @Test + public void shouldHandleNullWorld() { + // given + Location location = new Location(null, 3.7777, 2.14156, 1); + + // when + String result = DebugSectionUtils.formatLocation(location); + + // then + assertThat(result, equalTo("(3.78, 2.14, 1) in 'null'")); + } + + @Test + public void shouldHandleNullLocation() { + // given / when / then + assertThat(DebugSectionUtils.formatLocation(null), equalTo("null")); + } + + @Test + public void shouldHaveHiddenConstructor() { + TestHelper.validateHasOnlyPrivateEmptyConstructor(DebugSectionUtils.class); + } +}