diff --git a/src/main/java/fr/xephi/authme/AuthMe.java b/src/main/java/fr/xephi/authme/AuthMe.java index 69ad8db86..15e861bda 100644 --- a/src/main/java/fr/xephi/authme/AuthMe.java +++ b/src/main/java/fr/xephi/authme/AuthMe.java @@ -1,34 +1,9 @@ package fr.xephi.authme; -import java.io.IOException; -import java.net.URL; -import java.util.Calendar; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Logger; - -import org.apache.logging.log4j.LogManager; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Server; -import org.bukkit.World; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.plugin.PluginManager; -import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.scheduler.BukkitTask; -import org.mcstats.Metrics; -import org.mcstats.Metrics.Graph; - import com.earth2me.essentials.Essentials; import com.google.common.base.Charsets; import com.google.common.io.Resources; import com.onarandombox.MultiverseCore.MultiverseCore; - import fr.xephi.authme.api.API; import fr.xephi.authme.api.NewAPI; import fr.xephi.authme.cache.auth.PlayerAuth; @@ -73,11 +48,36 @@ import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.OtherAccounts; import fr.xephi.authme.settings.Settings; import fr.xephi.authme.settings.Spawn; +import fr.xephi.authme.settings.custom.NewSetting; import fr.xephi.authme.util.GeoLiteAPI; import fr.xephi.authme.util.StringUtils; import fr.xephi.authme.util.Utils; import fr.xephi.authme.util.Wrapper; import net.minelink.ctplus.CombatTagPlus; +import org.apache.logging.log4j.LogManager; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; +import org.mcstats.Metrics; +import org.mcstats.Metrics.Graph; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; /** * The AuthMe main class. @@ -99,6 +99,7 @@ public class AuthMe extends JavaPlugin { private CommandHandler commandHandler = null; private PermissionsManager permsMan = null; private Settings settings; + private NewSetting newSettings; private Messages messages; private JsonCache playerBackup; private ModuleManager moduleManager; @@ -215,6 +216,7 @@ public class AuthMe extends JavaPlugin { setEnabled(false); return; } + newSettings = createNewSetting(); // Set up messages & password security messages = Messages.getInstance(); @@ -235,7 +237,7 @@ public class AuthMe extends JavaPlugin { // Set up the permissions manager and command handler permsMan = initializePermissionsManager(); - commandHandler = initializeCommandHandler(permsMan, messages, passwordSecurity); + commandHandler = initializeCommandHandler(permsMan, messages, passwordSecurity, newSettings); // Set up the module manager setupModuleManager(); @@ -417,11 +419,12 @@ public class AuthMe extends JavaPlugin { } private CommandHandler initializeCommandHandler(PermissionsManager permissionsManager, Messages messages, - PasswordSecurity passwordSecurity) { + PasswordSecurity passwordSecurity, NewSetting settings) { HelpProvider helpProvider = new HelpProvider(permissionsManager); Set baseCommands = CommandInitializer.buildCommands(); - CommandMapper mapper = new CommandMapper(baseCommands, messages, permissionsManager, helpProvider); - CommandService commandService = new CommandService(this, mapper, helpProvider, messages, passwordSecurity); + CommandMapper mapper = new CommandMapper(baseCommands, permissionsManager); + CommandService commandService = new CommandService( + this, mapper, helpProvider, messages, passwordSecurity, permissionsManager, settings); return new CommandHandler(commandService); } @@ -443,19 +446,24 @@ public class AuthMe extends JavaPlugin { * @return True on success, false on failure. */ private boolean loadSettings() { - // TODO: new configuration style (more files) try { settings = new Settings(this); Settings.reload(); } catch (Exception e) { ConsoleLogger.writeStackTrace(e); - ConsoleLogger.showError("Can't load the configuration file... Something went wrong, to avoid security issues the server will shutdown!"); + ConsoleLogger.showError("Can't load the configuration file... Something went wrong. " + + "To avoid security issues the server will shut down!"); server.shutdown(); return true; } return false; } + private NewSetting createNewSetting() { + File configFile = new File(getDataFolder() + "config.yml"); + return new NewSetting(getConfig(), configFile); + } + /** * Set up the console filter. */ diff --git a/src/main/java/fr/xephi/authme/command/CommandHandler.java b/src/main/java/fr/xephi/authme/command/CommandHandler.java index e3ab671b4..37c1a2a16 100644 --- a/src/main/java/fr/xephi/authme/command/CommandHandler.java +++ b/src/main/java/fr/xephi/authme/command/CommandHandler.java @@ -3,6 +3,9 @@ package fr.xephi.authme.command; import java.util.ArrayList; import java.util.List; +import fr.xephi.authme.AuthMe; +import fr.xephi.authme.command.help.HelpProvider; +import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import fr.xephi.authme.util.StringUtils; @@ -13,6 +16,12 @@ import fr.xephi.authme.util.StringUtils; */ public class CommandHandler { + /** + * The threshold for suggesting a similar command. If the difference is below this value, we will + * ask the player whether he meant the similar command. + */ + private static final double SUGGEST_COMMAND_THRESHOLD = 0.75; + private final CommandService commandService; /** @@ -40,14 +49,32 @@ public class CommandHandler { parts.add(0, bukkitCommandLabel); FoundCommandResult result = commandService.mapPartsToCommand(sender, parts); - if (FoundResultStatus.SUCCESS.equals(result.getResultStatus())) { - executeCommand(sender, result); - } else { - commandService.outputMappingError(sender, result); - } + handleCommandResult(sender, result); return !FoundResultStatus.MISSING_BASE_COMMAND.equals(result.getResultStatus()); } + private void handleCommandResult(CommandSender sender, FoundCommandResult result) { + switch (result.getResultStatus()) { + case SUCCESS: + executeCommand(sender, result); + break; + case MISSING_BASE_COMMAND: + sender.sendMessage(ChatColor.DARK_RED + "Failed to parse " + AuthMe.getPluginName() + " command!"); + break; + case INCORRECT_ARGUMENTS: + sendImproperArgumentsMessage(sender, result); + break; + case UNKNOWN_LABEL: + sendUnknownCommandMessage(sender, result); + break; + case NO_PERMISSION: + sendPermissionDeniedError(sender); + break; + default: + throw new IllegalStateException("Unknown result status '" + result.getResultStatus() + "'"); + } + } + /** * Execute the command for the given command sender. * @@ -76,6 +103,46 @@ public class CommandHandler { return cleanArguments; } + /** + * Show an "unknown command" message to the user and suggest an existing command if its similarity is within + * the defined threshold. + * + * @param sender The command sender + * @param result The command that was found during the mapping process + */ + private static void sendUnknownCommandMessage(CommandSender sender, FoundCommandResult result) { + sender.sendMessage(ChatColor.DARK_RED + "Unknown command!"); + // Show a command suggestion if available and the difference isn't too big + if (result.getDifference() <= SUGGEST_COMMAND_THRESHOLD && result.getCommandDescription() != null) { + sender.sendMessage(ChatColor.YELLOW + "Did you mean " + ChatColor.GOLD + + CommandUtils.constructCommandPath(result.getCommandDescription()) + ChatColor.YELLOW + "?"); + } + + sender.sendMessage(ChatColor.YELLOW + "Use the command " + ChatColor.GOLD + "/" + result.getLabels().get(0) + + " help" + ChatColor.YELLOW + " to view help."); + } + + private void sendImproperArgumentsMessage(CommandSender sender, FoundCommandResult result) { + CommandDescription command = result.getCommandDescription(); + if (!commandService.getPermissionsManager().hasPermission(sender, command)) { + sendPermissionDeniedError(sender); + return; + } + + // Show the command argument help + sender.sendMessage(ChatColor.DARK_RED + "Incorrect command arguments!"); + commandService.outputHelp(sender, result, HelpProvider.SHOW_ARGUMENTS); + + List labels = result.getLabels(); + String childLabel = labels.size() >= 2 ? labels.get(1) : ""; + sender.sendMessage(ChatColor.GOLD + "Detailed help: " + ChatColor.WHITE + + "/" + labels.get(0) + " help " + childLabel); + } + + // TODO ljacqu 20151212: Remove me once I am a MessageKey + private static void sendPermissionDeniedError(CommandSender sender) { + sender.sendMessage(ChatColor.DARK_RED + "You don't have permission to use this command!"); + } } diff --git a/src/main/java/fr/xephi/authme/command/CommandMapper.java b/src/main/java/fr/xephi/authme/command/CommandMapper.java index 69d31fe5f..8bc5dccdd 100644 --- a/src/main/java/fr/xephi/authme/command/CommandMapper.java +++ b/src/main/java/fr/xephi/authme/command/CommandMapper.java @@ -1,13 +1,11 @@ package fr.xephi.authme.command; -import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.executable.HelpCommand; import fr.xephi.authme.command.help.HelpProvider; import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.util.CollectionUtils; import fr.xephi.authme.util.StringUtils; -import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import java.util.ArrayList; @@ -19,101 +17,24 @@ import static fr.xephi.authme.command.FoundResultStatus.MISSING_BASE_COMMAND; import static fr.xephi.authme.command.FoundResultStatus.UNKNOWN_LABEL; /** - * The AuthMe command handler, responsible for mapping incoming command parts to the correct {@link CommandDescription} - * or to display help messages for erroneous invocations (unknown command, no permission, etc.). + * The AuthMe command handler, responsible for mapping incoming + * command parts to the correct {@link CommandDescription}. */ public class CommandMapper { - /** - * The threshold for suggesting a similar command. If the difference is below this value, we will - * ask the player whether he meant the similar command. - */ - private static final double SUGGEST_COMMAND_THRESHOLD = 0.75; - /** * The class of the help command, to which the base label should also be passed in the arguments. */ private static final Class HELP_COMMAND_CLASS = HelpCommand.class; private final Set baseCommands; - private final Messages messages; private final PermissionsManager permissionsManager; - private final HelpProvider helpProvider; - public CommandMapper(Set baseCommands, Messages messages, - PermissionsManager permissionsManager, HelpProvider helpProvider) { + public CommandMapper(Set baseCommands, PermissionsManager permissionsManager) { this.baseCommands = baseCommands; - this.messages = messages; this.permissionsManager = permissionsManager; - this.helpProvider = helpProvider; } - public void outputStandardError(CommandSender sender, FoundCommandResult result) { - switch (result.getResultStatus()) { - case SUCCESS: - // Successful mapping, so no error to output - break; - case MISSING_BASE_COMMAND: - sender.sendMessage(ChatColor.DARK_RED + "Failed to parse " + AuthMe.getPluginName() + " command!"); - break; - case INCORRECT_ARGUMENTS: - sendImproperArgumentsMessage(sender, result); - break; - case UNKNOWN_LABEL: - sendUnknownCommandMessage(sender, result); - break; - case NO_PERMISSION: - sendPermissionDeniedError(sender); - break; - default: - throw new IllegalStateException("Unknown result status '" + result.getResultStatus() + "'"); - } - } - - /** - * Show an "unknown command" message to the user and suggest an existing command if its similarity is within - * the defined threshold. - * - * @param sender The command sender - * @param result The command that was found during the mapping process - */ - private static void sendUnknownCommandMessage(CommandSender sender, FoundCommandResult result) { - sender.sendMessage(ChatColor.DARK_RED + "Unknown command!"); - - // Show a command suggestion if available and the difference isn't too big - if (result.getDifference() <= SUGGEST_COMMAND_THRESHOLD && result.getCommandDescription() != null) { - sender.sendMessage(ChatColor.YELLOW + "Did you mean " + ChatColor.GOLD - + CommandUtils.constructCommandPath(result.getCommandDescription()) + ChatColor.YELLOW + "?"); - } - - sender.sendMessage(ChatColor.YELLOW + "Use the command " + ChatColor.GOLD + "/" + result.getLabels().get(0) - + " help" + ChatColor.YELLOW + " to view help."); - } - - private void sendImproperArgumentsMessage(CommandSender sender, FoundCommandResult result) { - CommandDescription command = result.getCommandDescription(); - if (!permissionsManager.hasPermission(sender, command)) { - sendPermissionDeniedError(sender); - return; - } - - // Show the command argument help - sender.sendMessage(ChatColor.DARK_RED + "Incorrect command arguments!"); - List lines = helpProvider.printHelp(sender, result, HelpProvider.SHOW_ARGUMENTS); - for (String line : lines) { - sender.sendMessage(line); - } - - List labels = result.getLabels(); - String childLabel = labels.size() >= 2 ? labels.get(1) : ""; - sender.sendMessage(ChatColor.GOLD + "Detailed help: " + ChatColor.WHITE - + "/" + labels.get(0) + " help " + childLabel); - } - - // TODO ljacqu 20151212: Remove me once I am a MessageKey - private static void sendPermissionDeniedError(CommandSender sender) { - sender.sendMessage(ChatColor.DARK_RED + "You don't have permission to use this command!"); - } /** * Map incoming command parts to a command. This processes all parts and distinguishes the labels from arguments. diff --git a/src/main/java/fr/xephi/authme/command/CommandService.java b/src/main/java/fr/xephi/authme/command/CommandService.java index 08a2790e8..8dd1da599 100644 --- a/src/main/java/fr/xephi/authme/command/CommandService.java +++ b/src/main/java/fr/xephi/authme/command/CommandService.java @@ -1,9 +1,5 @@ package fr.xephi.authme.command; -import java.util.List; - -import org.bukkit.command.CommandSender; - import fr.xephi.authme.AuthMe; import fr.xephi.authme.command.help.HelpProvider; import fr.xephi.authme.datasource.DataSource; @@ -12,6 +8,11 @@ import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.process.Management; import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.settings.custom.NewSetting; +import fr.xephi.authme.settings.domain.Property; +import org.bukkit.command.CommandSender; + +import java.util.List; /** * Service for implementations of {@link ExecutableCommand} to execute some common tasks. @@ -24,6 +25,8 @@ public class CommandService { private final HelpProvider helpProvider; private final CommandMapper commandMapper; private final PasswordSecurity passwordSecurity; + private final PermissionsManager permissionsManager; + private final NewSetting settings; /** * Constructor. @@ -33,14 +36,19 @@ public class CommandService { * @param helpProvider Help provider * @param messages Messages instance * @param passwordSecurity The Password Security instance + * @param permissionsManager The permissions manager + * @param settings The settings manager */ public CommandService(AuthMe authMe, CommandMapper commandMapper, HelpProvider helpProvider, Messages messages, - PasswordSecurity passwordSecurity) { + PasswordSecurity passwordSecurity, PermissionsManager permissionsManager, + NewSetting settings) { this.authMe = authMe; this.messages = messages; this.helpProvider = helpProvider; this.commandMapper = commandMapper; this.passwordSecurity = passwordSecurity; + this.permissionsManager = permissionsManager; + this.settings = settings; } /** @@ -75,17 +83,6 @@ public class CommandService { return commandMapper.mapPartsToCommand(sender, commandParts); } - /** - * Output the standard error message for the status in the provided {@link FoundCommandResult} object. - * Does not output anything for successful mappings. - * - * @param sender The sender to output the error to - * @param result The mapping result to process - */ - public void outputMappingError(CommandSender sender, FoundCommandResult result) { - commandMapper.outputStandardError(sender, result); - } - /** * Run the given task asynchronously with the Bukkit scheduler. * @@ -142,8 +139,7 @@ public class CommandService { * @return the permissions manager */ public PermissionsManager getPermissionsManager() { - // TODO ljacqu 20151226: Might be nicer to pass the perm manager via constructor - return authMe.getPermissionsManager(); + return permissionsManager; } /** @@ -156,4 +152,15 @@ public class CommandService { return messages.retrieve(key); } + /** + * Retrieve the given property's value. + * + * @param property The property to retrieve + * @param The type of the property + * @return The property's value + */ + public T getProperty(Property property) { + return settings.getProperty(property); + } + } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommand.java index ec2b7d98d..a1b27f16f 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/ChangePasswordAdminCommand.java @@ -8,7 +8,8 @@ import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.crypts.HashedPassword; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.custom.RestrictionSettings; +import fr.xephi.authme.settings.custom.SecuritySettings; import org.bukkit.command.CommandSender; import java.util.List; @@ -27,7 +28,7 @@ public class ChangePasswordAdminCommand implements ExecutableCommand { // Validate the password String playerPassLowerCase = playerPass.toLowerCase(); - if (!playerPassLowerCase.matches(Settings.getPassRegex)) { + if (!playerPassLowerCase.matches(commandService.getProperty(RestrictionSettings.ALLOWED_PASSWORD_REGEX))) { commandService.send(sender, MessageKey.PASSWORD_MATCH_ERROR); return; } @@ -35,12 +36,12 @@ public class ChangePasswordAdminCommand implements ExecutableCommand { commandService.send(sender, MessageKey.PASSWORD_IS_USERNAME_ERROR); return; } - if (playerPassLowerCase.length() < Settings.getPasswordMinLen - || playerPassLowerCase.length() > Settings.passwordMaxLength) { + if (playerPassLowerCase.length() < commandService.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH) + || playerPassLowerCase.length() > commandService.getProperty(SecuritySettings.MAX_PASSWORD_LENGTH)) { commandService.send(sender, MessageKey.INVALID_PASSWORD_LENGTH); return; } - if (!Settings.unsafePasswords.isEmpty() && Settings.unsafePasswords.contains(playerPassLowerCase)) { + if (commandService.getProperty(SecuritySettings.UNSAFE_PASSWORDS).contains(playerPassLowerCase)) { commandService.send(sender, MessageKey.PASSWORD_UNSAFE_ERROR); return; } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java index 46916b7b8..107f8c7ae 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/RegisterAdminCommand.java @@ -7,6 +7,7 @@ import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.crypts.HashedPassword; import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.custom.SecuritySettings; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; @@ -35,8 +36,8 @@ public class RegisterAdminCommand implements ExecutableCommand { commandService.send(sender, MessageKey.PASSWORD_IS_USERNAME_ERROR); return; } - if (playerPassLowerCase.length() < Settings.getPasswordMinLen - || playerPassLowerCase.length() > Settings.passwordMaxLength) { + if (playerPassLowerCase.length() < commandService.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH) + || playerPassLowerCase.length() > commandService.getProperty(SecuritySettings.MAX_PASSWORD_LENGTH)) { commandService.send(sender, MessageKey.INVALID_PASSWORD_LENGTH); return; } diff --git a/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java b/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java index d21bac778..79f963f0b 100644 --- a/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/captcha/CaptchaCommand.java @@ -6,7 +6,7 @@ import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.security.RandomString; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.custom.SecuritySettings; import fr.xephi.authme.util.Wrapper; import org.bukkit.entity.Player; @@ -28,20 +28,20 @@ public class CaptchaCommand extends PlayerCommand { return; } - if (!Settings.useCaptcha) { + if (!commandService.getProperty(SecuritySettings.USE_CAPTCHA)) { commandService.send(player, MessageKey.USAGE_LOGIN); return; } - if (!plugin.cap.containsKey(playerNameLowerCase)) { commandService.send(player, MessageKey.USAGE_LOGIN); return; } - if (Settings.useCaptcha && !captcha.equals(plugin.cap.get(playerNameLowerCase))) { + if (!captcha.equals(plugin.cap.get(playerNameLowerCase))) { plugin.cap.remove(playerNameLowerCase); - String randStr = RandomString.generate(Settings.captchaLength); + int captchaLength = commandService.getProperty(SecuritySettings.CAPTCHA_LENGTH); + String randStr = RandomString.generate(captchaLength); plugin.cap.put(playerNameLowerCase, randStr); commandService.send(player, MessageKey.CAPTCHA_WRONG_ERROR, plugin.cap.get(playerNameLowerCase)); return; diff --git a/src/main/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommand.java b/src/main/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommand.java index 0d1cdc481..a3b7f4451 100644 --- a/src/main/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommand.java @@ -5,7 +5,8 @@ import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.PlayerCommand; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.custom.RestrictionSettings; +import fr.xephi.authme.settings.custom.SecuritySettings; import fr.xephi.authme.task.ChangePasswordTask; import fr.xephi.authme.util.Wrapper; import org.bukkit.entity.Player; @@ -32,7 +33,7 @@ public class ChangePasswordCommand extends PlayerCommand { // Make sure the password is allowed String playerPassLowerCase = newPassword.toLowerCase(); - if (!playerPassLowerCase.matches(Settings.getPassRegex)) { + if (!playerPassLowerCase.matches(commandService.getProperty(RestrictionSettings.ALLOWED_PASSWORD_REGEX))) { commandService.send(player, MessageKey.PASSWORD_MATCH_ERROR); return; } @@ -40,17 +41,18 @@ public class ChangePasswordCommand extends PlayerCommand { commandService.send(player, MessageKey.PASSWORD_IS_USERNAME_ERROR); return; } - if (playerPassLowerCase.length() < Settings.getPasswordMinLen - || playerPassLowerCase.length() > Settings.passwordMaxLength) { + if (playerPassLowerCase.length() < commandService.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH) + || playerPassLowerCase.length() > commandService.getProperty(SecuritySettings.MAX_PASSWORD_LENGTH)) { commandService.send(player, MessageKey.INVALID_PASSWORD_LENGTH); return; } - if (!Settings.unsafePasswords.isEmpty() && Settings.unsafePasswords.contains(playerPassLowerCase)) { + if (commandService.getProperty(SecuritySettings.UNSAFE_PASSWORDS).contains(playerPassLowerCase)) { commandService.send(player, MessageKey.PASSWORD_UNSAFE_ERROR); return; } AuthMe plugin = AuthMe.getInstance(); + // TODO ljacqu 20160117: Call async task via Management commandService.runTaskAsynchronously(new ChangePasswordTask(plugin, player, oldPassword, newPassword)); } } diff --git a/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java b/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java index 51bf6c106..8ba9f2cb7 100644 --- a/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/register/RegisterCommand.java @@ -43,6 +43,6 @@ public class RegisterCommand extends PlayerCommand { @Override public String getAlternativeCommand() { - return "authme register "; + return "/authme register "; } } diff --git a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java index 1946df6ab..298d56bed 100644 --- a/src/main/java/fr/xephi/authme/security/PasswordSecurity.java +++ b/src/main/java/fr/xephi/authme/security/PasswordSecurity.java @@ -55,7 +55,7 @@ public class PasswordSecurity { * @param hashedPassword The encrypted password to test the clear-text password against * @param playerName The name of the player * - * @return True if the + * @return True if there was a password match with another encryption method, false otherwise */ private boolean compareWithAllEncryptionMethods(String password, HashedPassword hashedPassword, String playerName) { diff --git a/src/main/java/fr/xephi/authme/settings/Settings.java b/src/main/java/fr/xephi/authme/settings/Settings.java index 4adfe15bd..ebad00fa4 100644 --- a/src/main/java/fr/xephi/authme/settings/Settings.java +++ b/src/main/java/fr/xephi/authme/settings/Settings.java @@ -7,6 +7,7 @@ import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.datasource.DataSource.DataSourceType; import fr.xephi.authme.security.HashAlgorithm; +import fr.xephi.authme.util.StringUtils; import fr.xephi.authme.util.Wrapper; import org.bukkit.configuration.file.YamlConfiguration; @@ -311,7 +312,7 @@ public final class Settings { try { return Files.toString(EMAIL_FILE, Charsets.UTF_8); } catch (IOException e) { - ConsoleLogger.showError(e.getMessage()); + ConsoleLogger.showError("Error loading email text: " + StringUtils.formatException(e)); ConsoleLogger.writeStackTrace(e); return ""; } diff --git a/src/main/java/fr/xephi/authme/settings/custom/BackupSettings.java b/src/main/java/fr/xephi/authme/settings/custom/BackupSettings.java new file mode 100644 index 000000000..f2e78931b --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/BackupSettings.java @@ -0,0 +1,29 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; + +import static fr.xephi.authme.settings.domain.Property.newProperty; + +public class BackupSettings implements SettingsClass { + + @Comment("Enable or disable automatic backup") + public static final Property ENABLED = + newProperty("BackupSystem.ActivateBackup", false); + + @Comment("Set backup at every start of server") + public static final Property ON_SERVER_START = + newProperty("BackupSystem.OnServerStart", false); + + @Comment("Set backup at every stop of server") + public static final Property ON_SERVER_STOP = + newProperty("BackupSystem.OnServerStop", true); + + @Comment(" Windows only mysql installation Path") + public static final Property MYSQL_WINDOWS_PATH = + newProperty("BackupSystem.MysqlWindowsPath", "C:\\Program Files\\MySQL\\MySQL Server 5.1\\"); + + private BackupSettings() { + } +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java b/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java new file mode 100644 index 000000000..f32cf7483 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/ConverterSettings.java @@ -0,0 +1,32 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; + +import static fr.xephi.authme.settings.domain.Property.newProperty; +import static fr.xephi.authme.settings.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.domain.PropertyType.STRING; + +public class ConverterSettings implements SettingsClass { + + @Comment("Rakamak file name") + public static final Property RAKAMAK_FILE_NAME = + newProperty(STRING, "Converter.Rakamak.fileName", "users.rak"); + + @Comment("Rakamak use IP?") + public static final Property RAKAMAK_USE_IP = + newProperty(BOOLEAN, "Converter.Rakamak.useIP", false); + + @Comment("Rakamak IP file name") + public static final Property RAKAMAK_IP_FILE_NAME = + newProperty(STRING, "Converter.Rakamak.ipFileName", "UsersIp.rak"); + + @Comment("CrazyLogin database file name") + public static final Property CRAZYLOGIN_FILE_NAME = + newProperty(STRING, "Converter.CrazyLogin.fileName", "accounts.db"); + + private ConverterSettings() { + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java b/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java new file mode 100644 index 000000000..84f0b7085 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/DatabaseSettings.java @@ -0,0 +1,108 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; + +import static fr.xephi.authme.settings.domain.Property.newProperty; + +public class DatabaseSettings implements SettingsClass { + + @Comment({"What type of database do you want to use?", + "Valid values: sqlite, mysql"}) + public static final Property BACKEND = + newProperty(DataSource.DataSourceType.class, "DataSource.backend", DataSource.DataSourceType.SQLITE); + + @Comment("Enable database caching, should improve database performance") + public static final Property USE_CACHING = + newProperty("DataSource.caching", true); + + @Comment("Database host address") + public static final Property MYSQL_HOST = + newProperty("DataSource.mySQLHost", "127.0.0.1"); + + @Comment("Database port") + public static final Property MYSQL_PORT = + newProperty("DataSource.mySQLPort", "3306"); + + @Comment("Username about Database Connection Infos") + public static final Property MYSQL_USERNAME = + newProperty("DataSource.mySQLUsername", "authme"); + + @Comment("Password about Database Connection Infos") + public static final Property MYSQL_PASSWORD = + newProperty("DataSource.mySQLPassword", "123456"); + + @Comment("Database Name, use with converters or as SQLITE database name") + public static final Property MYSQL_DATABASE = + newProperty("DataSource.mySQLDatabase", "authme"); + + @Comment("Table of the database") + public static final Property MYSQL_TABLE = + newProperty("DataSource.mySQLTablename", "authme"); + + @Comment("Column of IDs to sort data") + public static final Property MYSQL_COL_ID = + newProperty("DataSource.mySQLColumnId", "id"); + + @Comment("Column for storing or checking players nickname") + public static final Property MYSQL_COL_NAME = + newProperty("DataSource.mySQLColumnName", "username"); + + @Comment("Column for storing or checking players RealName ") + public static final Property MYSQL_COL_REALNAME = + newProperty("DataSource.mySQLRealName", "realname"); + + @Comment("Column for storing players passwords") + public static final Property MYSQL_COL_PASSWORD = + newProperty("DataSource.mySQLColumnPassword", "password"); + + @Comment("Column for storing players passwords salts") + public static final Property MYSQL_COL_SALT = + newProperty("ExternalBoardOptions.mySQLColumnSalt", ""); + + @Comment("Column for storing players emails") + public static final Property MYSQL_COL_EMAIL = + newProperty("DataSource.mySQLColumnEmail", "email"); + + @Comment("Column for storing if a player is logged in or not") + public static final Property MYSQL_COL_ISLOGGED = + newProperty("DataSource.mySQLColumnLogged", "isLogged"); + + @Comment("Column for storing players ips") + public static final Property MYSQL_COL_IP = + newProperty("DataSource.mySQLColumnIp", "ip"); + + @Comment("Column for storing players lastlogins") + public static final Property MYSQL_COL_LASTLOGIN = + newProperty("DataSource.mySQLColumnLastLogin", "lastlogin"); + + @Comment("Column for storing player LastLocation - X") + public static final Property MYSQL_COL_LASTLOC_X = + newProperty("DataSource.mySQLlastlocX", "x"); + + @Comment("Column for storing player LastLocation - Y") + public static final Property MYSQL_COL_LASTLOC_Y = + newProperty("DataSource.mySQLlastlocY", "y"); + + @Comment("Column for storing player LastLocation - Z") + public static final Property MYSQL_COL_LASTLOC_Z = + newProperty("DataSource.mySQLlastlocZ", "z"); + + @Comment("Column for storing player LastLocation - World Name") + public static final Property MYSQL_COL_LASTLOC_WORLD = + newProperty("DataSource.mySQLlastlocWorld", "world"); + + @Comment("Column for storing players groups") + public static final Property MYSQL_COL_GROUP = + newProperty("ExternalBoardOptions.mySQLColumnGroup", ""); + + @Comment("Enable this when you allow registration through a website") + public static final Property MYSQL_WEBSITE = + newProperty("DataSource.mySQLWebsite", false); + + private DatabaseSettings() { + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java b/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java new file mode 100644 index 000000000..683143652 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/EmailSettings.java @@ -0,0 +1,76 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; + +import java.util.List; + +import static fr.xephi.authme.settings.domain.Property.newProperty; +import static fr.xephi.authme.settings.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.domain.PropertyType.INTEGER; +import static fr.xephi.authme.settings.domain.PropertyType.STRING; +import static fr.xephi.authme.settings.domain.PropertyType.STRING_LIST; + +public class EmailSettings implements SettingsClass { + + @Comment("Email SMTP server host") + public static final Property SMTP_HOST = + newProperty(STRING, "Email.mailSMTP", "smtp.gmail.com"); + + @Comment("Email SMTP server port") + public static final Property SMTP_PORT = + newProperty(INTEGER, "Email.mailPort", 465); + + @Comment("Email account which sends the mails") + public static final Property MAIL_ACCOUNT = + newProperty(STRING, "Email.mailAccount", ""); + + @Comment("Email account password") + public static final Property MAIL_PASSWORD = + newProperty(STRING, "Email.mailPassword", ""); + + @Comment("Custom sender name, replacing the mailAccount name in the email") + public static final Property MAIL_SENDER_NAME = + newProperty("Email.mailSenderName", ""); + + @Comment("Recovery password length") + public static final Property RECOVERY_PASSWORD_LENGTH = + newProperty(INTEGER, "Email.RecoveryPasswordLength", 8); + + @Comment("Mail Subject") + public static final Property RECOVERY_MAIL_SUBJECT = + newProperty(STRING, "Email.mailSubject", "Your new AuthMe password"); + + @Comment("Like maxRegPerIP but with email") + public static final Property MAX_REG_PER_EMAIL = + newProperty(INTEGER, "Email.maxRegPerEmail", 1); + + @Comment("Recall players to add an email?") + public static final Property RECALL_PLAYERS = + newProperty(BOOLEAN, "Email.recallPlayers", false); + + @Comment("Delay in minute for the recall scheduler") + public static final Property DELAY_RECALL = + newProperty(INTEGER, "Email.delayRecall", 5); + + @Comment("Blacklist these domains for emails") + public static final Property> DOMAIN_BLACKLIST = + newProperty(STRING_LIST, "Email.emailBlacklisted", "10minutemail.com"); + + @Comment("Whitelist ONLY these domains for emails") + public static final Property> DOMAIN_WHITELIST = + newProperty(STRING_LIST, "Email.emailWhitelisted"); + + @Comment("Send the new password drawn in an image?") + public static final Property PASSWORD_AS_IMAGE = + newProperty(BOOLEAN, "Email.generateImage", false); + + @Comment("The OAuth2 token") + public static final Property OAUTH2_TOKEN = + newProperty(STRING, "Email.emailOauth2Token", ""); + + private EmailSettings() { + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java new file mode 100644 index 000000000..af0458ac4 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/HooksSettings.java @@ -0,0 +1,72 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.PropertyType; +import fr.xephi.authme.settings.domain.SettingsClass; + +import java.util.List; + +import static fr.xephi.authme.settings.domain.Property.newProperty; + +public class HooksSettings implements SettingsClass { + + @Comment("Do we need to hook with multiverse for spawn checking?") + public static final Property MULTIVERSE = + newProperty("Hooks.multiverse", true); + + @Comment("Do we need to hook with BungeeCord?") + public static final Property BUNGEECORD = + newProperty("Hooks.bungeecord", false); + + @Comment("Send player to this BungeeCord server after register/login") + public static final Property BUNGEECORD_SERVER = + newProperty("Hooks.sendPlayerTo", ""); + + @Comment("Do we need to disable Essentials SocialSpy on join?") + public static final Property DISABLE_SOCIAL_SPY = + newProperty("Hooks.disableSocialSpy", false); + + @Comment("Do we need to force /motd Essentials command on join?") + public static final Property USE_ESSENTIALS_MOTD = + newProperty("Hooks.useEssentialsMotd", false); + + @Comment("Do we need to cache custom Attributes?") + public static final Property CACHE_CUSTOM_ATTRIBUTES = + newProperty("Hooks.customAttributes", false); + + @Comment("These features are only available on VeryGames Server Provider") + public static final Property ENABLE_VERYGAMES_IP_CHECK = + newProperty("VeryGames.enableIpCheck", false); + + @Comment({ + "-1 means disabled. If you want that only activated players", + "can log into your server, you can set here the group number", + "of unactivated users, needed for some forum/CMS support"}) + public static final Property NON_ACTIVATED_USERS_GROUP = + newProperty("ExternalBoardOptions.nonActivedUserGroup", -1); + + @Comment("Other MySQL columns where we need to put the username (case-sensitive)") + public static final Property> MYSQL_OTHER_USERNAME_COLS = + newProperty(PropertyType.STRING_LIST, "ExternalBoardOptions.mySQLOtherUsernameColumns"); + + @Comment("How much log2 rounds needed in BCrypt (do not change if you do not know what it does)") + public static final Property BCRYPT_LOG2_ROUND = + newProperty("ExternalBoardOptions.bCryptLog2Round", 10); + + @Comment("phpBB table prefix defined during the phpBB installation process") + public static final Property PHPBB_TABLE_PREFIX = + newProperty("ExternalBoardOptions.phpbbTablePrefix", "phpbb_"); + + @Comment("phpBB activated group ID; 2 is the default registered group defined by phpBB") + public static final Property PHPBB_ACTIVATED_GROUP_ID = + newProperty("ExternalBoardOptions.phpbbActivatedGroupId", 2); + + @Comment("Wordpress prefix defined during WordPress installation") + public static final Property WORDPRESS_TABLE_PREFIX = + newProperty("ExternalBoardOptions.wordpressTablePrefix", "wp_"); + + private HooksSettings() { + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java b/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java new file mode 100644 index 000000000..6f9abdc44 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/NewSetting.java @@ -0,0 +1,156 @@ +package fr.xephi.authme.settings.custom; + +import com.google.common.annotations.VisibleForTesting; +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.propertymap.PropertyMap; +import fr.xephi.authme.util.CollectionUtils; +import fr.xephi.authme.util.StringUtils; +import org.bukkit.configuration.file.FileConfiguration; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * The new settings manager. + */ +public class NewSetting { + + private File file; + private FileConfiguration configuration; + + /** + * Constructor. + * Loads the file as YAML and checks its integrity. + * + * @param configuration The configuration to interact with + * @param file The configuration file + */ + public NewSetting(FileConfiguration configuration, File file) { + this.configuration = configuration; + this.file = file; + + // TODO ljacqu 20160109: Ensure that save() works as desired (i.e. that it always produces valid YAML) + // and then uncomment the lines below. Once this is uncommented, the checks in the old Settings.java should + // be removed as we should check to rewrite the config.yml file only at one place + // -------- + // PropertyMap propertyMap = SettingsFieldRetriever.getAllPropertyFields(); + // if (!containsAllSettings(propertyMap)) { + // save(propertyMap); + // } + } + + /** + * Constructor for testing purposes, allowing more options. + * + * @param configuration The FileConfiguration object to use + * @param file The file to write to + * @param propertyMap The property map whose properties should be verified for presence, or null to skip this + */ + @VisibleForTesting + NewSetting(FileConfiguration configuration, File file, PropertyMap propertyMap) { + this.configuration = configuration; + this.file = file; + + if (propertyMap != null && !containsAllSettings(propertyMap)) { + save(propertyMap); + } + } + + /** + * Get the given property from the configuration. + * + * @param property The property to retrieve + * @param The property's type + * @return The property's value + */ + public T getProperty(Property property) { + return property.getFromFile(configuration); + } + + public void save() { + save(SettingsFieldRetriever.getAllPropertyFields()); + } + + public void save(PropertyMap propertyMap) { + try (FileWriter writer = new FileWriter(file)) { + writer.write(""); + + // Contains all but the last node of the setting, e.g. [DataSource, mysql] for "DataSource.mysql.username" + List currentPath = new ArrayList<>(); + for (Map.Entry, String[]> entry : propertyMap.entrySet()) { + Property property = entry.getKey(); + + // Handle properties + List propertyPath = Arrays.asList(property.getPath().split("\\.")); + List commonPathParts = CollectionUtils.filterCommonStart( + currentPath, propertyPath.subList(0, propertyPath.size() - 1)); + List newPathParts = CollectionUtils.getRange(propertyPath, commonPathParts.size()); + + if (commonPathParts.isEmpty()) { + writer.append("\n"); + } + + int indentationLevel = commonPathParts.size(); + if (newPathParts.size() > 1) { + for (String path : newPathParts.subList(0, newPathParts.size() - 1)) { + writer.append("\n") + .append(indent(indentationLevel)) + .append(path) + .append(": "); + ++indentationLevel; + } + } + for (String comment : entry.getValue()) { + writer.append("\n") + .append(indent(indentationLevel)) + .append("# ") + .append(comment); + } + writer.append("\n") + .append(indent(indentationLevel)) + .append(CollectionUtils.getRange(newPathParts, newPathParts.size() - 1).get(0)) + .append(": "); + + List yamlLines = property.formatValueAsYaml(configuration); + String delim = ""; + for (String yamlLine : yamlLines) { + writer.append(delim).append(yamlLine); + delim = "\n" + indent(indentationLevel); + } + + currentPath = propertyPath.subList(0, propertyPath.size() - 1); + } + writer.flush(); + writer.close(); + } catch (IOException e) { + ConsoleLogger.showError("Could not save config file - " + StringUtils.formatException(e)); + ConsoleLogger.writeStackTrace(e); + } + } + + @VisibleForTesting + boolean containsAllSettings(PropertyMap propertyMap) { + for (Property property : propertyMap.keySet()) { + if (!property.isPresent(configuration)) { + return false; + } + } + return true; + } + + private static String indent(int level) { + // YAML uses indentation of 4 spaces + StringBuilder sb = new StringBuilder(level * 4); + for (int i = 0; i < level; ++i) { + sb.append(" "); + } + return sb.toString(); + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/PluginSettings.java b/src/main/java/fr/xephi/authme/settings/custom/PluginSettings.java new file mode 100644 index 000000000..e35076db2 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/PluginSettings.java @@ -0,0 +1,60 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; + +import static fr.xephi.authme.settings.domain.Property.newProperty; + +public class PluginSettings implements SettingsClass { + + @Comment("The name shown in the help messages") + public static final Property HELP_HEADER = + newProperty("settings.helpHeader", "AuthMeReloaded"); + + @Comment({ + "Do you want to enable the session feature?", + "If enabled, when a player authenticates successfully,", + "his IP and his nickname is saved.", + "The next time the player joins the server, if his IP", + "is the same as last time and the timeout hasn't", + "expired, he will not need to authenticate." + }) + public static final Property SESSIONS_ENABLED = + newProperty("settings.sessions.enabled", false); + + @Comment({ + "After how many minutes should a session expire?", + "0 for unlimited time (Very dangerous, use it at your own risk!)", + "Remember that sessions will end only after the timeout, and", + "if the player's IP has changed but the timeout hasn't expired,", + "the player will be kicked from the server due to invalid session" + }) + public static final Property SESSIONS_TIMEOUT = + newProperty("settings.sessions.timeout", 10); + + @Comment({ + "Should the session expire if the player tries to log in with", + "another IP address?" + }) + public static final Property SESSIONS_EXPIRE_ON_IP_CHANGE = + newProperty("settings.sessions.sessionExpireOnIpChange", true); + + @Comment("Message language, available: en, de, br, cz, pl, fr, ru, hu, sk, es, zhtw, fi, zhcn, lt, it, ko, pt") + public static final Property MESSAGES_LANGUAGE = + newProperty("settings.messagesLanguage", "en"); + + @Comment({ + "Take care with this option; if you don't want", + "to use Vault and group switching of AuthMe", + "for unloggedIn players, set this setting to true.", + "Default is false." + }) + public static final Property ENABLE_PERMISSION_CHECK = + newProperty("permission.EnablePermissionCheck", false); + + + private PluginSettings() { + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java b/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java new file mode 100644 index 000000000..2582c277f --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/ProtectionSettings.java @@ -0,0 +1,46 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; + +import java.util.List; + +import static fr.xephi.authme.settings.domain.Property.newProperty; +import static fr.xephi.authme.settings.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.domain.PropertyType.INTEGER; +import static fr.xephi.authme.settings.domain.PropertyType.STRING_LIST; + + +public class ProtectionSettings implements SettingsClass { + + @Comment("Enable some servers protection (country based login, antibot)") + public static final Property ENABLE_PROTECTION = + newProperty(BOOLEAN, "Protection.enableProtection", false); + + @Comment({"Countries allowed to join the server and register, see http://dev.bukkit.org/bukkit-plugins/authme-reloaded/pages/countries-codes/ for countries' codes", + "PLEASE USE QUOTES!"}) + public static final Property> COUNTRIES_WHITELIST = + newProperty(STRING_LIST, "Protection.countries", "US", "GB", "A1"); + + @Comment({"Countries not allowed to join the server and register", + "PLEASE USE QUOTES!"}) + public static final Property> COUNTRIES_BLACKLIST = + newProperty(STRING_LIST, "Protection.countriesBlacklist"); + + @Comment("Do we need to enable automatic antibot system?") + public static final Property ENABLE_ANTIBOT = + newProperty(BOOLEAN, "Protection.enableAntiBot", false); + + @Comment("Max number of player allowed to login in 5 secs before enable AntiBot system automatically") + public static final Property ANTIBOT_SENSIBILITY = + newProperty(INTEGER, "Protection.antiBotSensibility", 5); + + @Comment("Duration in minutes of the antibot automatic system") + public static final Property ANTIBOT_DURATION = + newProperty(INTEGER, "Protection.antiBotDuration", 10); + + private ProtectionSettings() { + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java b/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java new file mode 100644 index 000000000..5fcc139d7 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/PurgeSettings.java @@ -0,0 +1,49 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; + +import static fr.xephi.authme.settings.domain.Property.newProperty; +import static fr.xephi.authme.settings.domain.PropertyType.BOOLEAN; +import static fr.xephi.authme.settings.domain.PropertyType.INTEGER; +import static fr.xephi.authme.settings.domain.PropertyType.STRING; + +public class PurgeSettings implements SettingsClass { + + @Comment("If enabled, AuthMe automatically purges old, unused accounts") + public static final Property USE_AUTO_PURGE = + newProperty(BOOLEAN, "Purge.useAutoPurge", false); + + @Comment("Number of Days an account become Unused") + public static final Property DAYS_BEFORE_REMOVE_PLAYER = + newProperty(INTEGER, "Purge.daysBeforeRemovePlayer", 60); + + @Comment("Do we need to remove the player.dat file during purge process?") + public static final Property REMOVE_PLAYER_DAT = + newProperty(BOOLEAN, "Purge.removePlayerDat", false); + + @Comment("Do we need to remove the Essentials/users/player.yml file during purge process?") + public static final Property REMOVE_ESSENTIALS_FILES = + newProperty(BOOLEAN, "Purge.removeEssentialsFile", false); + + @Comment("World where are players.dat stores") + public static final Property DEFAULT_WORLD = + newProperty(STRING, "Purge.defaultWorld", "world"); + + @Comment("Do we need to remove LimitedCreative/inventories/player.yml, player_creative.yml files during purge process ?") + public static final Property REMOVE_LIMITED_CREATIVE_INVENTORIES = + newProperty(BOOLEAN, "Purge.removeLimitedCreativesInventories", false); + + @Comment("Do we need to remove the AntiXRayData/PlayerData/player file during purge process?") + public static final Property REMOVE_ANTI_XRAY_FILE = + newProperty(BOOLEAN, "Purge.removeAntiXRayFile", false); + + @Comment("Do we need to remove permissions?") + public static final Property REMOVE_PERMISSIONS = + newProperty(BOOLEAN, "Purge.removePermissions", false); + + private PurgeSettings() { + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/RegistrationSettings.java b/src/main/java/fr/xephi/authme/settings/custom/RegistrationSettings.java new file mode 100644 index 000000000..7e8301456 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/RegistrationSettings.java @@ -0,0 +1,101 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.PropertyType; +import fr.xephi.authme.settings.domain.SettingsClass; + +import java.util.List; + +import static fr.xephi.authme.settings.domain.Property.newProperty; + +public class RegistrationSettings implements SettingsClass { + + @Comment("Enable registration on the server?") + public static final Property IS_ENABLED = + newProperty("settings.registration.enabled", true); + + @Comment({ + "Send every X seconds a message to a player to", + "remind him that he has to login/register"}) + public static final Property MESSAGE_INTERVAL = + newProperty("settings.registration.messageInterval", 5); + + @Comment({ + "Only registered and logged in players can play.", + "See restrictions for exceptions"}) + public static final Property FORCE = + newProperty("settings.registration.force", true); + + @Comment("Do we replace password registration by an email registration method?") + public static final Property USE_EMAIL_REGISTRATION = + newProperty("settings.registration.enableEmailRegistrationSystem", false); + + @Comment({ + "Enable double check of email when you register", + "when it's true, registration requires that kind of command:", + "/register "}) + public static final Property ENABLE_CONFIRM_EMAIL = + newProperty("settings.registration.doubleEmailCheck", false); + + @Comment({ + "Do we force kicking player after a successful registration?", + "Do not use with login feature below"}) + public static final Property FORCE_KICK_AFTER_REGISTER = + newProperty("settings.registration.forceKickAfterRegister", false); + + @Comment("Does AuthMe need to enforce a /login after a successful registration?") + public static final Property FORCE_LOGIN_AFTER_REGISTER = + newProperty("settings.registration.forceLoginAfterRegister", false); + + @Comment("Force these commands after /login, without any '/', use %p to replace with player name") + public static final Property> FORCE_COMMANDS = + newProperty(PropertyType.STRING_LIST, "settings.forceCommands"); + + @Comment("Force these commands after /login as service console, without any '/'. " + + "Use %p to replace with player name") + public static final Property> FORCE_COMMANDS_AS_CONSOLE = + newProperty(PropertyType.STRING_LIST, "settings.forceCommandsAsConsole"); + + @Comment("Force these commands after /register, without any '/', use %p to replace with player name") + public static final Property> FORCE_REGISTER_COMMANDS = + newProperty(PropertyType.STRING_LIST, "settings.forceRegisterCommands"); + + @Comment("Force these commands after /register as a server console, without any '/'. " + + "Use %p to replace with player name") + public static final Property> FORCE_REGISTER_COMMANDS_AS_CONSOLE = + newProperty(PropertyType.STRING_LIST, "settings.forceRegisterCommandsAsConsole"); + + @Comment({ + "Enable to display the welcome message (welcome.txt) after a registration or a login", + "You can use colors in this welcome.txt + some replaced strings:", + "{PLAYER}: player name, {ONLINE}: display number of online players, {MAXPLAYERS}: display server slots,", + "{IP}: player ip, {LOGINS}: number of players logged, {WORLD}: player current world, {SERVER}: server name", + "{VERSION}: get current bukkit version, {COUNTRY}: player country"}) + public static final Property USE_WELCOME_MESSAGE = + newProperty("settings.useWelcomeMessage", true); + + @Comment("Do we need to broadcast the welcome message to all server or only to the player? set true for " + + "server or false for player") + public static final Property BROADCAST_WELCOME_MESSAGE = + newProperty("settings.broadcastWelcomeMessage", false); + + @Comment("Do we need to delay the join/leave message to be displayed only when the player is authenticated?") + public static final Property DELAY_JOIN_LEAVE_MESSAGES = + newProperty("settings.delayJoinLeaveMessages", true); + + @Comment("Do we need to add potion effect Blinding before login/reigster?") + public static final Property APPLY_BLIND_EFFECT = + newProperty("settings.applyBlindEffect", false); + + @Comment({ + "Do we need to prevent people to login with another case?", + "If Xephi is registered, then Xephi can login, but not XEPHI/xephi/XePhI"}) + public static final Property PREVENT_OTHER_CASE = + newProperty("settings.preventOtherCase", false); + + + private RegistrationSettings() { + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java b/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java new file mode 100644 index 000000000..32b68586d --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/RestrictionSettings.java @@ -0,0 +1,184 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.PropertyType; +import fr.xephi.authme.settings.domain.SettingsClass; + +import java.util.List; + +import static fr.xephi.authme.settings.domain.Property.newProperty; + +public class RestrictionSettings implements SettingsClass { + + @Comment({ + "Can not authenticated players chat and see the chat log?", + "Keep in mind that this feature also blocks all commands not", + "listed in the list below."}) + public static final Property ALLOW_CHAT = + newProperty("settings.restrictions.allowChat", false); + + @Comment("Allowed commands for unauthenticated players") + public static final Property> ALLOW_COMMANDS = + newProperty(PropertyType.STRING_LIST, "settings.restrictions.allowCommands", + "login", "register", "l", "reg", "email", "captcha"); + + @Comment("Max number of allowed registrations per IP") + // TODO ljacqu 20160109: If 0 == unlimited, add this fact ot the comment + public static final Property MAX_REGISTRATION_PER_IP = + newProperty("settings.restrictions.maxRegPerIp", 1); + + @Comment("Minimum allowed username length") + public static final Property MIN_NICKNAME_LENGTH = + newProperty("settings.restrictions.minNicknameLength", 4); + + @Comment("Maximum allowed username length") + public static final Property MAX_NICKNAME_LENGTH = + newProperty("settings.restrictions.maxNicknameLength", 16); + + @Comment({ + "When this setting is enabled, online players can't be kicked out", + "due to \"Logged in from another Location\"", + "This setting will prevent potential security exploits."}) + public static final Property FORCE_SINGLE_SESSION = + newProperty("settings.restrictions.ForceSingleSession", true); + + @Comment({ + "If enabled, every player will be teleported to the world spawnpoint", + "after successful authentication.", + "The quit location of the player will be overwritten.", + "This is different from \"teleportUnAuthedToSpawn\" that teleport player", + "back to his quit location after the authentication."}) + public static final Property FORCE_SPAWN_LOCATION_AFTER_LOGIN = + newProperty("settings.restrictions.ForceSpawnLocOnJoinEnabled", false); + + @Comment("This option will save the quit location of the players.") + public static final Property SAVE_QUIT_LOCATION = + newProperty("settings.restrictions.SaveQuitLocation", false); + + @Comment({ + "To activate the restricted user feature you need", + "to enable this option and configure the AllowedRestrctedUser field."}) + public static final Property ENABLE_RESTRICTED_USERS = + newProperty("settings.restrictions.AllowRestrictedUser", false); + + @Comment({ + "The restricted user feature will kick players listed below", + "if they don't match the defined IP address.", + "Example:", + " AllowedRestrictedUser:", + " - playername;127.0.0.1"}) + public static final Property> ALLOWED_RESTRICTED_USERS = + newProperty(PropertyType.STRING_LIST, "settings.restrictions.AllowedRestrictedUser"); + + @Comment("Should unregistered players be kicked immediately?") + public static final Property KICK_NON_REGISTERED = + newProperty("settings.restrictions.kickNonRegistered", false); + + @Comment("Should players be kicked on wrong password?") + public static final Property KICK_ON_WRONG_PASSWORD = + newProperty("settings.restrictions.kickOnWrongPassword", false); + + @Comment({ + "Should not logged in players be teleported to the spawn?", + "After the authentication they will be teleported back to", + "their normal position."}) + public static final Property TELEPORT_UNAUTHED_TO_SPAWN = + newProperty("settings.restrictions.teleportUnAuthedToSpawn", false); + + @Comment("Can unregistered players walk around?") + public static final Property ALLOW_UNAUTHED_MOVEMENT = + newProperty("settings.restrictions.allowMovement", false); + + @Comment({ + "Should not authenticated players have speed = 0?", + "This will reset the fly/walk speed to default value after the login."}) + public static final Property REMOVE_SPEED = + newProperty("settings.restrictions.removeSpeed", true); + + @Comment({ + "After how many seconds should players who fail to login or register", + "be kicked? Set to 0 to disable."}) + public static final Property TIMEOUT = + newProperty("settings.restrictions.timeout", 30); + + @Comment("Regex syntax of allowed characters in the player name.") + public static final Property ALLOWED_NICKNAME_CHARACTERS = + newProperty("settings.restrictions.allowedNicknameCharacters", "[a-zA-Z0-9_]*"); + + @Comment({ + "How far can unregistered players walk?", + "Set to 0 for unlimited radius" + }) + public static final Property ALLOWED_MOVEMENT_RADIUS = + newProperty("settings.restrictions.allowedMovementRadius", 100); + + @Comment({ + "Enable double check of password when you register", + "when it's true, registration requires that kind of command:", + "/register "}) + public static final Property ENABLE_PASSWORD_CONFIRMATION = + newProperty("settings.restrictions.enablePasswordConfirmation", true); + + @Comment("Should we protect the player inventory before logging in?") + public static final Property PROTECT_INVENTORY_BEFORE_LOGIN = + newProperty("settings.restrictions.ProtectInventoryBeforeLogIn", true); + + @Comment({ + "Should we display all other accounts from a player when he joins?", + "permission: /authme.admin.accounts"}) + public static final Property DISPLAY_OTHER_ACCOUNTS = + newProperty("settings.restrictions.displayOtherAccounts", true); + + @Comment({ + "WorldNames where we need to force the spawn location for ForceSpawnLocOnJoinEnabled", + "Case-sensitive!"}) + public static final Property> FORCE_SPAWN_ON_WORLDS = + newProperty(PropertyType.STRING_LIST, "settings.restrictions.ForceSpawnOnTheseWorlds", + "world", "world_nether", "world_the_end"); + + @Comment("Ban ip when the ip is not the ip registered in database") + public static final Property BAN_UNKNOWN_IP = + newProperty("settings.restrictions.banUnsafedIP", false); + + @Comment("Spawn priority; values: authme, essentials, multiverse, default") + public static final Property SPAWN_PRIORITY = + newProperty("settings.restrictions.spawnPriority", "authme,essentials,multiverse,default"); + + @Comment("Maximum Login authorized by IP") + public static final Property MAX_LOGIN_PER_IP = + newProperty("settings.restrictions.maxLoginPerIp", 0); + + @Comment("Maximum Join authorized by IP") + public static final Property MAX_JOIN_PER_IP = + newProperty("settings.restrictions.maxJoinPerIp", 0); + + @Comment("AuthMe will NEVER teleport players if set to true!") + public static final Property NO_TELEPORT = + newProperty("settings.restrictions.noTeleport", false); + + @Comment("Regex syntax for allowed chars in passwords") + public static final Property ALLOWED_PASSWORD_REGEX = + newProperty("settings.restrictions.allowedPasswordCharacters", "[\\x21-\\x7E]*"); + + @Comment("Force survival gamemode when player joins?") + public static final Property FORCE_SURVIVAL_MODE = + newProperty("settings.GameMode.ForceSurvivalMode", false); + + @Comment({ + "Below you can list all account names that", + "AuthMe will ignore for registration or login, configure it", + "at your own risk!! Remember that if you are going to add", + "nickname with [], you have to delimit name with ' '.", + "this option add compatibility with BuildCraft and some", + "other mods.", + "It is case-sensitive!" + }) + public static final Property> UNRESTRICTED_NAMES = + newProperty(PropertyType.STRING_LIST, "settings.unrestrictions.UnrestrictedName"); + + + private RestrictionSettings() { + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java b/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java new file mode 100644 index 000000000..94e33258c --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/SecuritySettings.java @@ -0,0 +1,103 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.security.HashAlgorithm; +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; + +import java.util.List; + +import static fr.xephi.authme.settings.domain.Property.newProperty; +import static fr.xephi.authme.settings.domain.PropertyType.STRING_LIST; + +public class SecuritySettings implements SettingsClass { + + @Comment({"Stop the server if we can't contact the sql database", + "Take care with this, if you set this to false,", + "AuthMe will automatically disable and the server won't be protected!"}) + public static final Property STOP_SERVER_ON_PROBLEM = + newProperty("Security.SQLProblem.stopServer", true); + + @Comment("/reload support") + public static final Property USE_RELOAD_COMMAND_SUPPORT = + newProperty("Security.ReloadCommand.useReloadCommandSupport", true); + + @Comment("Remove spam from console?") + public static final Property REMOVE_SPAM_FROM_CONSOLE = + newProperty("Security.console.noConsoleSpam", false); + + @Comment("Remove passwords from console?") + public static final Property REMOVE_PASSWORD_FROM_CONSOLE = + newProperty("Security.console.removePassword", true); + + @Comment("Player need to put a captcha when he fails too lot the password") + public static final Property USE_CAPTCHA = + newProperty("Security.captcha.useCaptcha", false); + + @Comment("Max allowed tries before request a captcha") + public static final Property MAX_LOGIN_TRIES_BEFORE_CAPTCHA = + newProperty("Security.captcha.maxLoginTry", 5); + + @Comment("Captcha length") + public static final Property CAPTCHA_LENGTH = + newProperty("Security.captcha.captchaLength", 5); + + @Comment({"Kick players before stopping the server, that allow us to save position of players", + "and all needed information correctly without any corruption."}) + public static final Property KICK_PLAYERS_BEFORE_STOPPING = + newProperty("Security.stop.kickPlayersBeforeStopping", true); + + @Comment("Minimum length of password") + public static final Property MIN_PASSWORD_LENGTH = + newProperty("settings.security.minPasswordLength", 5); + + @Comment("Maximum length of password") + public static final Property MAX_PASSWORD_LENGTH = + newProperty("settings.security.passwordMaxLength", 30); + + @Comment({ + "This is a very important option: every time a player joins the server,", + "if they are registered, AuthMe will switch him to unLoggedInGroup.", + "This should prevent all major exploits.", + "You can set up your permission plugin with this special group to have no permissions,", + "or only permission to chat (or permission to send private messages etc.).", + "The better way is to set up this group with few permissions, so if a player", + "tries to exploit an account they can do only what you've defined for the group.", + "After, a logged in player will be moved to his correct permissions group!", + "Please note that the group name is case-sensitive, so 'admin' is different from 'Admin'", + "Otherwise your group will be wiped and the player will join in the default group []!", + "Example unLoggedinGroup: NotLogged" + }) + public static final Property UNLOGGEDIN_GROUP = + newProperty("settings.security.unLoggedinGroup", "unLoggedinGroup"); + + @Comment({ + "Possible values: MD5, SHA1, SHA256, WHIRLPOOL, XAUTH, MD5VB, PHPBB,", + "MYBB, IPB3, PHPFUSION, SMF, XENFORO, SALTED2MD5, JOOMLA, BCRYPT, WBB3, SHA512,", + "DOUBLEMD5, PBKDF2, PBKDF2DJANGO, WORDPRESS, ROYALAUTH, CUSTOM (for developers only)" + }) + public static final Property PASSWORD_HASH = + newProperty(HashAlgorithm.class, "settings.security.passwordHash", HashAlgorithm.SHA256); + + @Comment("Salt length for the SALTED2MD5 MD5(MD5(password)+salt)") + public static final Property DOUBLE_MD5_SALT_LENGTH = + newProperty("settings.security.doubleMD5SaltLength", 8); + + @Comment({"If password checking return false, do we need to check with all", + "other password algorithm to check an old password?", + "AuthMe will update the password to the new password hash"}) + public static final Property SUPPORT_OLD_PASSWORD_HASH = + newProperty("settings.security.supportOldPasswordHash", false); + + @Comment({"Prevent unsafe passwords from being used; put them in lowercase!", + "unsafePasswords:", + "- '123456'", + "- 'password'"}) + public static final Property> UNSAFE_PASSWORDS = + newProperty(STRING_LIST, "settings.security.unsafePasswords", + "123456", "password", "qwerty", "12345", "54321"); + + private SecuritySettings() { + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/custom/SettingsFieldRetriever.java b/src/main/java/fr/xephi/authme/settings/custom/SettingsFieldRetriever.java new file mode 100644 index 000000000..74b722e8f --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/custom/SettingsFieldRetriever.java @@ -0,0 +1,69 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Comment; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; +import fr.xephi.authme.settings.propertymap.PropertyMap; +import fr.xephi.authme.util.StringUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; + +/** + * Utility class responsible for the retrieval of all {@link Property} fields via reflections. + */ +final class SettingsFieldRetriever { + + /** The classes to scan for properties. */ + private static final List> CONFIGURATION_CLASSES = Arrays.asList( + ConverterSettings.class, PluginSettings.class, RestrictionSettings.class, + DatabaseSettings.class, EmailSettings.class, HooksSettings.class, + ProtectionSettings.class, PurgeSettings.class, SecuritySettings.class, + RegistrationSettings.class, BackupSettings.class); + + private SettingsFieldRetriever() { + } + + /** + * Scan all given classes for their properties and return the generated {@link PropertyMap}. + * + * @return PropertyMap containing all found properties and their associated comments + * @see #CONFIGURATION_CLASSES + */ + public static PropertyMap getAllPropertyFields() { + PropertyMap properties = new PropertyMap(); + for (Class clazz : CONFIGURATION_CLASSES) { + Field[] declaredFields = clazz.getDeclaredFields(); + for (Field field : declaredFields) { + Property property = getFieldIfRelevant(field); + if (property != null) { + properties.put(property, getCommentsForField(field)); + } + } + } + return properties; + } + + private static String[] getCommentsForField(Field field) { + if (field.isAnnotationPresent(Comment.class)) { + return field.getAnnotation(Comment.class).value(); + } + return new String[0]; + } + + private static Property getFieldIfRelevant(Field field) { + field.setAccessible(true); + if (field.isAccessible() && Property.class.equals(field.getType()) && Modifier.isStatic(field.getModifiers())) { + try { + return (Property) field.get(null); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Could not fetch field '" + field.getName() + "' from class '" + + field.getDeclaringClass().getSimpleName() + "': " + StringUtils.formatException(e)); + } + } + return null; + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/domain/Comment.java b/src/main/java/fr/xephi/authme/settings/domain/Comment.java new file mode 100644 index 000000000..07f20e257 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/domain/Comment.java @@ -0,0 +1,17 @@ +package fr.xephi.authme.settings.domain; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Comment for properties which are also included in the YAML file upon saving. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Comment { + + String[] value(); + +} diff --git a/src/main/java/fr/xephi/authme/settings/domain/EnumPropertyType.java b/src/main/java/fr/xephi/authme/settings/domain/EnumPropertyType.java new file mode 100644 index 000000000..ed184bb7d --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/domain/EnumPropertyType.java @@ -0,0 +1,50 @@ +package fr.xephi.authme.settings.domain; + +import org.bukkit.configuration.file.FileConfiguration; + +import java.util.List; + +import static java.util.Arrays.asList; + +/** + * Enum property type. + * @param The enum class + */ +class EnumPropertyType> extends PropertyType { + + private Class clazz; + + public EnumPropertyType(Class clazz) { + this.clazz = clazz; + } + + @Override + public E getFromFile(Property property, FileConfiguration configuration) { + String textValue = configuration.getString(property.getPath()); + if (textValue == null) { + return property.getDefaultValue(); + } + E mappedValue = mapToEnum(textValue); + return mappedValue != null ? mappedValue : property.getDefaultValue(); + } + + @Override + protected List asYaml(E value) { + return asList("'" + value + "'"); + } + + @Override + public boolean contains(Property property, FileConfiguration configuration) { + return super.contains(property, configuration) + && mapToEnum(configuration.getString(property.getPath())) != null; + } + + private E mapToEnum(String value) { + for (E entry : clazz.getEnumConstants()) { + if (entry.name().equalsIgnoreCase(value)) { + return entry; + } + } + return null; + } +} diff --git a/src/main/java/fr/xephi/authme/settings/domain/Property.java b/src/main/java/fr/xephi/authme/settings/domain/Property.java new file mode 100644 index 000000000..6b15b5d3b --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/domain/Property.java @@ -0,0 +1,114 @@ +package fr.xephi.authme.settings.domain; + +import org.bukkit.configuration.file.FileConfiguration; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Properties (i.e. a setting that is read from the config.yml file). + */ +public class Property { + + private final PropertyType type; + private final String path; + private final T defaultValue; + + private Property(PropertyType type, String path, T defaultValue) { + Objects.requireNonNull(defaultValue); + this.type = type; + this.path = path; + this.defaultValue = defaultValue; + } + + public static Property newProperty(PropertyType type, String path, T defaultValue) { + return new Property<>(type, path, defaultValue); + } + + @SafeVarargs + public static Property> newProperty(PropertyType> type, String path, U... defaultValues) { + return new Property<>(type, path, Arrays.asList(defaultValues)); + } + + public static > Property newProperty(Class clazz, String path, E defaultValue) { + return new Property<>(new EnumPropertyType<>(clazz), path, defaultValue); + } + + // ----- + // Overloaded convenience methods for specific types + // ----- + public static Property newProperty(String path, boolean defaultValue) { + return new Property<>(PropertyType.BOOLEAN, path, defaultValue); + } + + public static Property newProperty(String path, int defaultValue) { + return new Property<>(PropertyType.INTEGER, path, defaultValue); + } + + public static Property newProperty(String path, String defaultValue) { + return new Property<>(PropertyType.STRING, path, defaultValue); + } + + // ----- + // Hooks to the PropertyType methods + // ----- + + /** + * Get the property value from the given configuration. + * + * @param configuration The configuration to read the value from + * @return The value, or default if not present + */ + public T getFromFile(FileConfiguration configuration) { + return type.getFromFile(this, configuration); + } + + /** + * Format the property value as YAML. + * + * @param configuration The configuration to read the value from + * @return The property value as YAML + */ + public List formatValueAsYaml(FileConfiguration configuration) { + return type.asYaml(this, configuration); + } + + /** + * Return whether or not the given configuration file contains the property. + * + * @param configuration The configuration file to verify + * @return True if the property is present, false otherwise + */ + public boolean isPresent(FileConfiguration configuration) { + return type.contains(this, configuration); + } + + // ----- + // Trivial getters + // ----- + + /** + * Return the default value of the property. + * + * @return The default value + */ + public T getDefaultValue() { + return defaultValue; + } + + /** + * Return the property path (i.e. the node at which this property is located in the YAML file). + * + * @return The path + */ + public String getPath() { + return path; + } + + @Override + public String toString() { + return "Property '" + path + "'"; + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java b/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java new file mode 100644 index 000000000..dc1975bba --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/domain/PropertyType.java @@ -0,0 +1,162 @@ +package fr.xephi.authme.settings.domain; + +import org.bukkit.configuration.file.FileConfiguration; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Arrays.asList; + +/** + * Handles a certain property type and provides type-specific functionality. + * + * @param The value of the property + * @see Property + */ +public abstract class PropertyType { + + public static final PropertyType BOOLEAN = new BooleanProperty(); + public static final PropertyType DOUBLE = new DoubleProperty(); + public static final PropertyType INTEGER = new IntegerProperty(); + public static final PropertyType STRING = new StringProperty(); + public static final PropertyType> STRING_LIST = new StringListProperty(); + + /** + * Get the property's value from the given YAML configuration. + * + * @param property The property to retrieve + * @param configuration The YAML configuration to read from + * @return The read value, or the default value if absent + */ + public abstract T getFromFile(Property property, FileConfiguration configuration); + + /** + * Return the property's value (or its default) as YAML. + * + * @param property The property to transform + * @param configuration The YAML configuration to read from + * @return The read value or its default in YAML format + */ + public List asYaml(Property property, FileConfiguration configuration) { + return asYaml(getFromFile(property, configuration)); + } + + /** + * Return whether the property is present in the given configuration. + * + * @param property The property to search for + * @param configuration The configuration to verify + * @return True if the property is present, false otherwise + */ + public boolean contains(Property property, FileConfiguration configuration) { + return configuration.contains(property.getPath()); + } + + /** + * Transform the given value to YAML. + * + * @param value The value to transform + * @return The value as YAML + */ + protected abstract List asYaml(T value); + + + /** + * Boolean property. + */ + private static final class BooleanProperty extends PropertyType { + @Override + public Boolean getFromFile(Property property, FileConfiguration configuration) { + return configuration.getBoolean(property.getPath(), property.getDefaultValue()); + } + + @Override + protected List asYaml(Boolean value) { + return asList(value ? "true" : "false"); + } + } + + /** + * Double property. + */ + private static final class DoubleProperty extends PropertyType { + @Override + public Double getFromFile(Property property, FileConfiguration configuration) { + return configuration.getDouble(property.getPath(), property.getDefaultValue()); + } + + @Override + protected List asYaml(Double value) { + return asList(String.valueOf(value)); + } + } + + /** + * Integer property. + */ + private static final class IntegerProperty extends PropertyType { + @Override + public Integer getFromFile(Property property, FileConfiguration configuration) { + return configuration.getInt(property.getPath(), property.getDefaultValue()); + } + + @Override + protected List asYaml(Integer value) { + return asList(String.valueOf(value)); + } + } + + /** + * String property. + */ + private static final class StringProperty extends PropertyType { + @Override + public String getFromFile(Property property, FileConfiguration configuration) { + return configuration.getString(property.getPath(), property.getDefaultValue()); + } + + @Override + protected List asYaml(String value) { + return asList(toYamlLiteral(value)); + } + + public static String toYamlLiteral(String str) { + // TODO: Need to handle new lines properly + return "'" + str.replace("'", "''") + "'"; + } + } + + /** + * String list property. + */ + private static final class StringListProperty extends PropertyType> { + @Override + public List getFromFile(Property> property, FileConfiguration configuration) { + if (!configuration.isList(property.getPath())) { + return property.getDefaultValue(); + } + return configuration.getStringList(property.getPath()); + } + + @Override + protected List asYaml(List value) { + if (value.isEmpty()) { + return asList("[]"); + } + + List resultLines = new ArrayList<>(); + resultLines.add(""); // add + for (String entry : value) { + // TODO: StringProperty#toYamlLiteral will return List... + resultLines.add(" - " + StringProperty.toYamlLiteral(entry)); + } + return resultLines; + } + + @Override + public boolean contains(Property> property, FileConfiguration configuration) { + return configuration.contains(property.getPath()) && configuration.isList(property.getPath()); + } + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/domain/SettingsClass.java b/src/main/java/fr/xephi/authme/settings/domain/SettingsClass.java new file mode 100644 index 000000000..796505add --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/domain/SettingsClass.java @@ -0,0 +1,7 @@ +package fr.xephi.authme.settings.domain; + +/** + * Marker for classes that define {@link Property} fields. + */ +public interface SettingsClass { +} diff --git a/src/main/java/fr/xephi/authme/settings/propertymap/Node.java b/src/main/java/fr/xephi/authme/settings/propertymap/Node.java new file mode 100644 index 000000000..70d8ce239 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/propertymap/Node.java @@ -0,0 +1,121 @@ +package fr.xephi.authme.settings.propertymap; + +import java.util.ArrayList; +import java.util.List; + +/** + * Node class for building a tree from supplied String paths, ordered by insertion. + *

+ * For instance, consider a tree to which the following paths are inserted (in the given order): + * "animal.bird.duck", "color.yellow", "animal.rodent.rat", "animal.rodent.rabbit", "color.red". + * For such a tree:

    + *
  • "animal" (or any of its children) is sorted before "color" (or any of its children)
  • + *
  • "animal.bird" or any child thereof is sorted before "animal.rodent"
  • + *
  • "animal.rodent.rat" comes before "animal.rodent.rabbit"
  • + *
+ * + * @see PropertyMapComparator + */ +final class Node { + + private final String name; + private final List children; + + private Node(String name) { + this.name = name; + this.children = new ArrayList<>(); + } + + /** + * Create a root node, i.e. the starting node for a new tree. Call this method to create + * a new tree and always pass this root to other methods. + * + * @return The generated root node. + */ + public static Node createRoot() { + return new Node(null); + } + + /** + * Add a node to the root, creating any intermediary children that don't exist. + * + * @param root The root to add the path to + * @param fullPath The entire path of the node to add, separate by periods + */ + public static void addNode(Node root, String fullPath) { + String[] pathParts = fullPath.split("\\."); + Node parent = root; + for (String part : pathParts) { + Node child = parent.getChild(part); + if (child == null) { + child = new Node(part); + parent.children.add(child); + } + parent = child; + } + } + + /** + * Compare two nodes by this class' sorting behavior (insertion order). + * Note that this method assumes that both supplied paths exist in the tree. + * + * @param root The root of the tree + * @param fullPath1 The full path to the first node + * @param fullPath2 The full path to the second node + * @return The comparison result, in the same format as {@link Comparable#compareTo} + */ + public static int compare(Node root, String fullPath1, String fullPath2) { + String[] path1 = fullPath1.split("\\."); + String[] path2 = fullPath2.split("\\."); + + int commonCount = 0; + Node commonNode = root; + while (commonCount < path1.length && commonCount < path2.length + && path1[commonCount].equals(path2[commonCount]) && commonNode != null) { + commonNode = commonNode.getChild(path1[commonCount]); + ++commonCount; + } + + if (commonNode == null) { + System.err.println("Could not find common node for '" + fullPath1 + "' at index " + commonCount); + return fullPath1.compareTo(fullPath2); // fallback + } else if (commonCount >= path1.length || commonCount >= path2.length) { + return Integer.compare(path1.length, path2.length); + } + int child1Index = commonNode.getChildIndex(path1[commonCount]); + int child2Index = commonNode.getChildIndex(path2[commonCount]); + return Integer.compare(child1Index, child2Index); + } + + private Node getChild(String name) { + for (Node child : children) { + if (child.name.equals(name)) { + return child; + } + } + return null; + } + + /** + * Return the child's index, i.e. the position at which it was inserted to its parent. + * + * @param name The name of the node + * @return The insertion index + */ + private int getChildIndex(String name) { + int i = 0; + for (Node child : children) { + if (child.name.equals(name)) { + return i; + } + ++i; + } + return -1; + } + + @Override + public String toString() { + return "Node '" + name + "'"; + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMap.java b/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMap.java new file mode 100644 index 000000000..9cac52d30 --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMap.java @@ -0,0 +1,66 @@ +package fr.xephi.authme.settings.propertymap; + +import fr.xephi.authme.settings.domain.Property; + +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Class wrapping a {@code Map} for storing properties and their associated + * comments with custom ordering. + * + * @see PropertyMapComparator for details about the map's order + */ +public class PropertyMap { + + private Map, String[]> map; + private PropertyMapComparator comparator; + + /** + * Create a new property map. + */ + public PropertyMap() { + comparator = new PropertyMapComparator(); + map = new TreeMap<>(comparator); + } + + /** + * Add a new property to the map. + * + * @param property The property to add + * @param comments The comments associated to the property + */ + public void put(Property property, String[] comments) { + comparator.add(property); + map.put(property, comments); + } + + /** + * Return the entry set of the map. + * + * @return The entry set + */ + public Set, String[]>> entrySet() { + return map.entrySet(); + } + + /** + * Return the key set of the map, i.e. all property objects it holds. + * + * @return The key set + */ + public Set> keySet() { + return map.keySet(); + } + + /** + * Return the size of the map. + * + * @return The size + */ + public int size() { + return map.size(); + } + +} diff --git a/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMapComparator.java b/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMapComparator.java new file mode 100644 index 000000000..e646fb61d --- /dev/null +++ b/src/main/java/fr/xephi/authme/settings/propertymap/PropertyMapComparator.java @@ -0,0 +1,39 @@ +package fr.xephi.authme.settings.propertymap; + +import fr.xephi.authme.settings.domain.Property; + +import java.util.Comparator; + +/** + * Custom comparator for {@link PropertyMap}. It guarantees that the map's entries: + *
    + *
  • are grouped by path, e.g. all "DataSource.mysql" properties are together, and "DataSource.mysql" properties + * are within the broader "DataSource" group.
  • + *
  • are ordered by insertion, e.g. if the first "DataSource" property is inserted before the first "security" + * property, then "DataSource" properties will come before the "security" ones.
  • + *
+ */ +final class PropertyMapComparator implements Comparator { + + private Node parent = Node.createRoot(); + + /** + * Method to call when adding a new property to the map (as to retain its insertion time). + * + * @param property The property that is being added + */ + public void add(Property property) { + Node.addNode(parent, property.getPath()); + } + + @Override + public int compare(Property p1, Property p2) { + return Node.compare(parent, p1.getPath(), p2.getPath()); + } + + @Override + public boolean equals(Object obj) { + return this == obj; + } + +} diff --git a/src/main/java/fr/xephi/authme/util/CollectionUtils.java b/src/main/java/fr/xephi/authme/util/CollectionUtils.java index 5c3eaa7f7..13077547c 100644 --- a/src/main/java/fr/xephi/authme/util/CollectionUtils.java +++ b/src/main/java/fr/xephi/authme/util/CollectionUtils.java @@ -3,6 +3,7 @@ package fr.xephi.authme.util; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Objects; /** * Utils class for collections. @@ -58,4 +59,15 @@ public final class CollectionUtils { public static boolean isEmpty(Collection coll) { return coll == null || coll.isEmpty(); } + + public static List filterCommonStart(List list1, List list2) { + List commonStart = new ArrayList<>(); + int minSize = Math.min(list1.size(), list2.size()); + int i = 0; + while (i < minSize && Objects.equals(list1.get(i), list2.get(i))) { + commonStart.add(list1.get(i)); + ++i; + } + return commonStart; + } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 7f151c495..d2e0a9e74 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -156,8 +156,10 @@ settings: # ForceSurvivalMode to player when join ? ForceSurvivalMode: false security: - # minimum Length of password + # Minimum length of password minPasswordLength: 5 + # Maximum length of password + passwordMaxLength: 30 # this is very important options, # every time player join the server, # if they are registered, AuthMe will switch him @@ -209,7 +211,7 @@ settings: # Only registered and logged in players can play. # See restrictions for exceptions force: true - # Does we replace password registration by an Email registration method? + # Do we replace password registration by an email registration method? enableEmailRegistrationSystem: false # Enable double check of email when you register # when it's true, registration require that kind of command: @@ -340,8 +342,6 @@ Email: RecoveryPasswordLength: 8 # Email subject of password get mailSubject: 'Your new AuthMe Password' - # Email text here - mailText: 'Dear ,

This is your new AuthMe password for the server

:



Do not forget to change password after login!
/changepassword newPassword' # Like maxRegPerIp but with email maxRegPerEmail: 1 # Recall players to add an email? @@ -355,6 +355,8 @@ Email: emailWhitelisted: [] # Do we need to send new password draw in an image? generateImage: false + # The email OAuth 2 token (leave empty if not used) + emailOauth2Token: '' Hooks: # Do we need to hook with multiverse for spawn checking? multiverse: true diff --git a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java index 018721c0d..345082bc9 100644 --- a/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandHandlerTest.java @@ -1,5 +1,6 @@ package fr.xephi.authme.command; +import fr.xephi.authme.permission.PermissionsManager; import org.bukkit.command.CommandSender; import org.junit.Before; import org.junit.Test; @@ -7,20 +8,27 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.MockitoAnnotations; +import java.util.Collections; import java.util.List; +import static fr.xephi.authme.command.FoundResultStatus.INCORRECT_ARGUMENTS; +import static fr.xephi.authme.command.FoundResultStatus.MISSING_BASE_COMMAND; import static fr.xephi.authme.command.FoundResultStatus.NO_PERMISSION; import static fr.xephi.authme.command.FoundResultStatus.SUCCESS; +import static fr.xephi.authme.command.FoundResultStatus.UNKNOWN_LABEL; import static java.util.Arrays.asList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; import static org.mockito.BDDMockito.given; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyListOf; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; /** @@ -89,7 +97,138 @@ public class CommandHandlerTest { assertThat(captor.getValue(), contains("unreg", "testPlayer")); verify(command, never()).getExecutableCommand(); - verify(serviceMock).outputMappingError(eq(sender), any(FoundCommandResult.class)); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(sender).sendMessage(captor.capture()); + assertThat(captor.getValue(), containsString("don't have permission")); + } + + @Test + public void shouldNotCallExecutableForWrongArguments() { + // given + String bukkitLabel = "unreg"; + String[] bukkitArgs = {"testPlayer"}; + CommandSender sender = mock(CommandSender.class); + CommandDescription command = mock(CommandDescription.class); + given(serviceMock.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + new FoundCommandResult(command, asList("unreg"), asList("testPlayer"), 0.0, INCORRECT_ARGUMENTS)); + PermissionsManager permissionsManager = mock(PermissionsManager.class); + given(permissionsManager.hasPermission(sender, command)).willReturn(true); + given(serviceMock.getPermissionsManager()).willReturn(permissionsManager); + + // when + handler.processCommand(sender, bukkitLabel, bukkitArgs); + + // then + verify(serviceMock).mapPartsToCommand(eq(sender), captor.capture()); + assertThat(captor.getValue(), contains("unreg", "testPlayer")); + + verify(command, never()).getExecutableCommand(); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(sender, atLeastOnce()).sendMessage(captor.capture()); + assertThat(captor.getAllValues().get(0), containsString("Incorrect command arguments")); + } + + @Test + public void shouldNotCallExecutableForWrongArgumentsAndPermissionDenied() { + // given + String bukkitLabel = "unreg"; + String[] bukkitArgs = {"testPlayer"}; + CommandSender sender = mock(CommandSender.class); + CommandDescription command = mock(CommandDescription.class); + given(serviceMock.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + new FoundCommandResult(command, asList("unreg"), asList("testPlayer"), 0.0, INCORRECT_ARGUMENTS)); + PermissionsManager permissionsManager = mock(PermissionsManager.class); + given(permissionsManager.hasPermission(sender, command)).willReturn(false); + given(serviceMock.getPermissionsManager()).willReturn(permissionsManager); + + // when + handler.processCommand(sender, bukkitLabel, bukkitArgs); + + // then + verify(serviceMock).mapPartsToCommand(eq(sender), captor.capture()); + assertThat(captor.getValue(), contains("unreg", "testPlayer")); + + verify(command, never()).getExecutableCommand(); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(sender).sendMessage(captor.capture()); + assertThat(captor.getValue(), containsString("You don't have permission")); + } + + @Test + public void shouldNotCallExecutableForFailedParsing() { + // given + String bukkitLabel = "unreg"; + String[] bukkitArgs = {"testPlayer"}; + CommandSender sender = mock(CommandSender.class); + CommandDescription command = mock(CommandDescription.class); + given(serviceMock.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + new FoundCommandResult(command, asList("unreg"), asList("testPlayer"), 0.0, MISSING_BASE_COMMAND)); + + // when + handler.processCommand(sender, bukkitLabel, bukkitArgs); + + // then + verify(serviceMock).mapPartsToCommand(eq(sender), captor.capture()); + assertThat(captor.getValue(), contains("unreg", "testPlayer")); + + verify(command, never()).getExecutableCommand(); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(sender).sendMessage(captor.capture()); + assertThat(captor.getValue(), containsString("Failed to parse")); + } + + @Test + public void shouldNotCallExecutableForUnknownLabelAndHaveSuggestion() { + // given + String bukkitLabel = "unreg"; + String[] bukkitArgs = {"testPlayer"}; + CommandSender sender = mock(CommandSender.class); + CommandDescription command = mock(CommandDescription.class); + given(command.getLabels()).willReturn(Collections.singletonList("test_cmd")); + given(serviceMock.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + new FoundCommandResult(command, asList("unreg"), asList("testPlayer"), 0.01, UNKNOWN_LABEL)); + + // when + handler.processCommand(sender, bukkitLabel, bukkitArgs); + + // then + verify(serviceMock).mapPartsToCommand(eq(sender), captor.capture()); + assertThat(captor.getValue(), contains("unreg", "testPlayer")); + + verify(command, never()).getExecutableCommand(); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(sender, times(3)).sendMessage(captor.capture()); + assertThat(captor.getAllValues().get(0), containsString("Unknown command")); + assertThat(captor.getAllValues().get(1), containsString("Did you mean")); + assertThat(captor.getAllValues().get(1), containsString("/test_cmd")); + assertThat(captor.getAllValues().get(2), containsString("Use the command")); + assertThat(captor.getAllValues().get(2), containsString("to view help")); + } + + @Test + public void shouldNotCallExecutableForUnknownLabelAndNotSuggestCommand() { + // given + String bukkitLabel = "unreg"; + String[] bukkitArgs = {"testPlayer"}; + CommandSender sender = mock(CommandSender.class); + CommandDescription command = mock(CommandDescription.class); + given(command.getLabels()).willReturn(Collections.singletonList("test_cmd")); + given(serviceMock.mapPartsToCommand(any(CommandSender.class), anyListOf(String.class))).willReturn( + new FoundCommandResult(command, asList("unreg"), asList("testPlayer"), 1.0, UNKNOWN_LABEL)); + + // when + handler.processCommand(sender, bukkitLabel, bukkitArgs); + + // then + verify(serviceMock).mapPartsToCommand(eq(sender), captor.capture()); + assertThat(captor.getValue(), contains("unreg", "testPlayer")); + + verify(command, never()).getExecutableCommand(); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(sender, times(2)).sendMessage(captor.capture()); + assertThat(captor.getAllValues().get(0), containsString("Unknown command")); + assertThat(captor.getAllValues().get(1), containsString("Use the command")); + assertThat(captor.getAllValues().get(1), containsString("to view help")); } @Test diff --git a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java index e65c08c68..8da420431 100644 --- a/src/test/java/fr/xephi/authme/command/CommandMapperTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandMapperTest.java @@ -1,7 +1,5 @@ package fr.xephi.authme.command; -import fr.xephi.authme.command.help.HelpProvider; -import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; import org.bukkit.command.CommandSender; import org.junit.Before; @@ -43,7 +41,7 @@ public class CommandMapperTest { @Before public void setUpMocks() { permissionsManagerMock = mock(PermissionsManager.class); - mapper = new CommandMapper(commands, mock(Messages.class), permissionsManagerMock, mock(HelpProvider.class)); + mapper = new CommandMapper(commands, permissionsManagerMock); } // ----------- @@ -256,4 +254,23 @@ public class CommandMapperTest { assertThat(result.getDifference(), equalTo(0.0)); } + @Test + public void shouldRecognizeMissingPermissionForCommand() { + // given + List parts = Arrays.asList("authme", "login", "test1"); + CommandSender sender = mock(CommandSender.class); + given(permissionsManagerMock.hasPermission(eq(sender), any(CommandDescription.class))).willReturn(false); + + // when + FoundCommandResult result = mapper.mapPartsToCommand(sender, parts); + + // then + assertThat(result.getCommandDescription(), equalTo(getCommandWithLabel(commands, "authme", "login"))); + assertThat(result.getResultStatus(), equalTo(FoundResultStatus.NO_PERMISSION)); + assertThat(result.getArguments(), contains("test1")); + assertThat(result.getDifference(), equalTo(0.0)); + assertThat(result.getLabels(), equalTo(parts.subList(0, 2))); + assertThat(result.getArguments(), contains(parts.get(2))); + } + } diff --git a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java index 3b457291e..e3e67e3ed 100644 --- a/src/test/java/fr/xephi/authme/command/CommandServiceTest.java +++ b/src/test/java/fr/xephi/authme/command/CommandServiceTest.java @@ -8,6 +8,9 @@ import fr.xephi.authme.output.Messages; import fr.xephi.authme.permission.PermissionsManager; import fr.xephi.authme.process.Management; import fr.xephi.authme.security.PasswordSecurity; +import fr.xephi.authme.settings.custom.NewSetting; +import fr.xephi.authme.settings.custom.SecuritySettings; +import fr.xephi.authme.settings.domain.Property; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.Before; @@ -36,6 +39,8 @@ public class CommandServiceTest { private Messages messages; private PasswordSecurity passwordSecurity; private CommandService commandService; + private PermissionsManager permissionsManager; + private NewSetting settings; @Before public void setUpService() { @@ -44,7 +49,10 @@ public class CommandServiceTest { helpProvider = mock(HelpProvider.class); messages = mock(Messages.class); passwordSecurity = mock(PasswordSecurity.class); - commandService = new CommandService(authMe, commandMapper, helpProvider, messages, passwordSecurity); + permissionsManager = mock(PermissionsManager.class); + settings = mock(NewSetting.class); + commandService = new CommandService( + authMe, commandMapper, helpProvider, messages, passwordSecurity, permissionsManager, settings); } @Test @@ -87,19 +95,6 @@ public class CommandServiceTest { verify(commandMapper).mapPartsToCommand(sender, commandParts); } - @Test - public void shouldOutputMappingError() { - // given - CommandSender sender = mock(CommandSender.class); - FoundCommandResult result = mock(FoundCommandResult.class); - - // when - commandService.outputMappingError(sender, result); - - // then - verify(commandMapper).outputStandardError(sender, result); - } - @Test @Ignore public void shouldRunTaskInAsync() { @@ -169,16 +164,11 @@ public class CommandServiceTest { @Test public void shouldReturnPermissionsManager() { - // given - PermissionsManager manager = mock(PermissionsManager.class); - given(authMe.getPermissionsManager()).willReturn(manager); - - // when + // given / when PermissionsManager result = commandService.getPermissionsManager(); // then - assertThat(result, equalTo(manager)); - verify(authMe).getPermissionsManager(); + assertThat(result, equalTo(permissionsManager)); } @Test @@ -195,4 +185,18 @@ public class CommandServiceTest { assertThat(result, equalTo(givenMessages)); verify(messages).retrieve(key); } + + @Test + public void shouldRetrieveProperty() { + // given + Property property = SecuritySettings.CAPTCHA_LENGTH; + given(settings.getProperty(property)).willReturn(7); + + // when + int result = settings.getProperty(property); + + // then + assertThat(result, equalTo(7)); + verify(settings).getProperty(property); + } } diff --git a/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java index 60c72bd31..c33b13ef3 100644 --- a/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/captcha/CaptchaCommandTest.java @@ -5,13 +5,12 @@ import fr.xephi.authme.command.CommandService; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.output.MessageKey; import fr.xephi.authme.output.Messages; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.custom.SecuritySettings; import fr.xephi.authme.util.WrapperMock; import org.bukkit.command.BlockCommandSender; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.mockito.Mockito; @@ -20,6 +19,7 @@ import java.util.Collections; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -35,8 +35,8 @@ public class CaptchaCommandTest { @Before public void setUpWrapperMock() { wrapperMock = WrapperMock.createInstance(); - Settings.useCaptcha = true; commandService = mock(CommandService.class); + given(commandService.getProperty(SecuritySettings.USE_CAPTCHA)).willReturn(true); } @Test @@ -46,7 +46,7 @@ public class CaptchaCommandTest { ExecutableCommand command = new CaptchaCommand(); // when - command.executeCommand(sender, new ArrayList(), mock(CommandService.class)); + command.executeCommand(sender, new ArrayList(), commandService); // then assertThat(wrapperMock.wasMockCalled(AuthMe.class), equalTo(false)); @@ -54,14 +54,14 @@ public class CaptchaCommandTest { } @Test - @Ignore public void shouldRejectIfCaptchaIsNotUsed() { // given Player player = mockPlayerWithName("testplayer"); ExecutableCommand command = new CaptchaCommand(); + given(commandService.getProperty(SecuritySettings.USE_CAPTCHA)).willReturn(false); // when - command.executeCommand(player, Collections.singletonList("1234"), mock(CommandService.class)); + command.executeCommand(player, Collections.singletonList("1234"), commandService); // then verify(commandService).send(player, MessageKey.USAGE_LOGIN); diff --git a/src/test/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommandTest.java index 380480ae6..d00a4e37d 100644 --- a/src/test/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/changepassword/ChangePasswordCommandTest.java @@ -4,7 +4,8 @@ import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.cache.auth.PlayerCache; import fr.xephi.authme.command.CommandService; import fr.xephi.authme.output.MessageKey; -import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.custom.RestrictionSettings; +import fr.xephi.authme.settings.custom.SecuritySettings; import fr.xephi.authme.task.ChangePasswordTask; import fr.xephi.authme.util.WrapperMock; import org.bukkit.Server; @@ -19,9 +20,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import static java.util.Arrays.asList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.any; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; @@ -44,11 +45,11 @@ public class ChangePasswordCommandTest { cacheMock = wrapperMock.getPlayerCache(); commandService = mock(CommandService.class); + when(commandService.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH)).thenReturn(2); + when(commandService.getProperty(SecuritySettings.MAX_PASSWORD_LENGTH)).thenReturn(50); // Only allow passwords with alphanumerical characters for the test - Settings.getPassRegex = "[a-zA-Z0-9]+"; - Settings.getPasswordMinLen = 2; - Settings.passwordMaxLength = 50; - Settings.unsafePasswords = Collections.EMPTY_LIST; + when(commandService.getProperty(RestrictionSettings.ALLOWED_PASSWORD_REGEX)).thenReturn("[a-zA-Z0-9]+"); + when(commandService.getProperty(SecuritySettings.UNSAFE_PASSWORDS)).thenReturn(Collections.EMPTY_LIST); } @Test @@ -112,7 +113,7 @@ public class ChangePasswordCommandTest { // given CommandSender sender = initPlayerWithName("abc12", true); ChangePasswordCommand command = new ChangePasswordCommand(); - Settings.passwordMaxLength = 3; + given(commandService.getProperty(SecuritySettings.MAX_PASSWORD_LENGTH)).willReturn(3); // when command.executeCommand(sender, Arrays.asList("12", "test"), commandService); @@ -127,7 +128,7 @@ public class ChangePasswordCommandTest { // given CommandSender sender = initPlayerWithName("abc12", true); ChangePasswordCommand command = new ChangePasswordCommand(); - Settings.getPasswordMinLen = 7; + given(commandService.getProperty(SecuritySettings.MIN_PASSWORD_LENGTH)).willReturn(7); // when command.executeCommand(sender, Arrays.asList("oldverylongpassword", "tester"), commandService); @@ -142,7 +143,8 @@ public class ChangePasswordCommandTest { // given CommandSender sender = initPlayerWithName("player", true); ChangePasswordCommand command = new ChangePasswordCommand(); - Settings.unsafePasswords = asList("test", "abc123"); + given(commandService.getProperty(SecuritySettings.UNSAFE_PASSWORDS)) + .willReturn(Arrays.asList("test", "abc123")); // when command.executeCommand(sender, Arrays.asList("oldpw", "abc123"), commandService); diff --git a/src/test/java/fr/xephi/authme/settings/custom/ConfigFileConsistencyTest.java b/src/test/java/fr/xephi/authme/settings/custom/ConfigFileConsistencyTest.java new file mode 100644 index 000000000..d012a50a4 --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/custom/ConfigFileConsistencyTest.java @@ -0,0 +1,92 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.propertymap.PropertyMap; +import fr.xephi.authme.util.StringUtils; +import org.bukkit.configuration.MemorySection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.fail; + +/** + * Test for {@link NewSetting} and the project's config.yml, + * verifying that no settings are missing from the file. + */ +public class ConfigFileConsistencyTest { + + /** The file name of the project's sample config file. */ + private static final String CONFIG_FILE = "/config.yml"; + + @Test + public void shouldHaveAllConfigs() throws IOException { + // given + URL url = this.getClass().getResource(CONFIG_FILE); + File configFile = new File(url.getFile()); + NewSetting settings = new NewSetting(YamlConfiguration.loadConfiguration(configFile), new File("bogus"), null); + + // when + boolean result = settings.containsAllSettings(SettingsFieldRetriever.getAllPropertyFields()); + + // then + if (!result) { + FileConfiguration configuration = + (FileConfiguration) ReflectionTestUtils.getFieldValue(NewSetting.class, settings, "configuration"); + + Set knownProperties = getAllKnownPropertyPaths(); + List missingProperties = new ArrayList<>(); + for (String path : knownProperties) { + if (!configuration.contains(path)) { + missingProperties.add(path); + } + } + fail("Found missing properties!\n-" + StringUtils.join("\n-", missingProperties)); + } + } + + @Test + public void shouldNotHaveUnknownConfigs() { + // given + URL url = this.getClass().getResource(CONFIG_FILE); + File configFile = new File(url.getFile()); + YamlConfiguration configuration = YamlConfiguration.loadConfiguration(configFile); + Map allReadProperties = configuration.getValues(true); + Set knownKeys = getAllKnownPropertyPaths(); + + // when + List unknownPaths = new ArrayList<>(); + for (Map.Entry entry : allReadProperties.entrySet()) { + // The value being a MemorySection means it's a parent node + if (!(entry.getValue() instanceof MemorySection) && !knownKeys.contains(entry.getKey())) { + unknownPaths.add(entry.getKey()); + } + } + + // then + if (!unknownPaths.isEmpty()) { + fail("Found " + unknownPaths.size() + " unknown property paths in the project's config.yml: \n- " + + StringUtils.join("\n- ", unknownPaths)); + } + } + + private static Set getAllKnownPropertyPaths() { + PropertyMap propertyMap = SettingsFieldRetriever.getAllPropertyFields(); + Set paths = new HashSet<>(propertyMap.size()); + for (Property property : propertyMap.keySet()) { + paths.add(property.getPath()); + } + return paths; + } + +} diff --git a/src/test/java/fr/xephi/authme/settings/custom/NewSettingIntegrationTest.java b/src/test/java/fr/xephi/authme/settings/custom/NewSettingIntegrationTest.java new file mode 100644 index 000000000..73b80b020 --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/custom/NewSettingIntegrationTest.java @@ -0,0 +1,115 @@ +package fr.xephi.authme.settings.custom; + +import com.google.common.collect.ImmutableMap; +import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.propertymap.PropertyMap; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.lang.reflect.Field; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeThat; + +/** + * Integration test for {@link NewSetting}. + */ +public class NewSettingIntegrationTest { + + /** File name of the sample config including all {@link TestConfiguration} values. */ + private static final String COMPLETE_FILE = "config-sample-values.yml"; + /** File name of the sample config missing certain {@link TestConfiguration} values. */ + private static final String INCOMPLETE_FILE = "config-incomplete-sample.yml"; + + private static PropertyMap propertyMap; + + @BeforeClass + public static void generatePropertyMap() { + propertyMap = new PropertyMap(); + for (Field field : TestConfiguration.class.getDeclaredFields()) { + Object fieldValue = ReflectionTestUtils.getFieldValue(TestConfiguration.class, null, field.getName()); + if (fieldValue instanceof Property) { + Property property = (Property) fieldValue; + String[] comments = new String[]{"Comment for '" + property.getPath() + "'"}; + propertyMap.put(property, comments); + } + } + } + + @Test + public void shouldLoadAndReadAllProperties() { + // given + YamlConfiguration configuration = YamlConfiguration.loadConfiguration(getConfigFile(COMPLETE_FILE)); + File file = new File("unused"); + assumeThat(file.exists(), equalTo(false)); + + // when / then + NewSetting settings = new NewSetting(configuration, file, propertyMap); + Map, Object> expectedValues = ImmutableMap., Object>builder() + .put(TestConfiguration.DURATION_IN_SECONDS, 22) + .put(TestConfiguration.SYSTEM_NAME, "Custom sys name") + .put(TestConfiguration.RATIO_LIMIT, -4.1) + .put(TestConfiguration.RATIO_FIELDS, Arrays.asList("Australia", "Burundi", "Colombia")) + .put(TestConfiguration.VERSION_NUMBER, 2492) + .put(TestConfiguration.SKIP_BORING_FEATURES, false) + .put(TestConfiguration.BORING_COLORS, Arrays.asList("beige", "gray")) + .put(TestConfiguration.DUST_LEVEL, 0.81) + .put(TestConfiguration.USE_COOL_FEATURES, true) + .put(TestConfiguration.COOL_OPTIONS, Arrays.asList("Dinosaurs", "Explosions", "Big trucks")) + .build(); + for (Map.Entry, Object> entry : expectedValues.entrySet()) { + assertThat("Property '" + entry.getKey().getPath() + "' has expected value", + settings.getProperty(entry.getKey()), equalTo(entry.getValue())); + } + assertThat(file.exists(), equalTo(false)); + } + + @Test + public void shouldWriteMissingProperties() { + // given/when + File file = getConfigFile(INCOMPLETE_FILE); + YamlConfiguration configuration = YamlConfiguration.loadConfiguration(file); + assumeThat(configuration.contains(TestConfiguration.BORING_COLORS.getPath()), equalTo(false)); + // Expectation: File is rewritten to since it does not have all configurations + new NewSetting(configuration, file, propertyMap); + + // Load the settings again -> checks that what we wrote can be loaded again + configuration = YamlConfiguration.loadConfiguration(file); + + // then + NewSetting settings = new NewSetting(configuration, file, propertyMap); + Map, Object> expectedValues = ImmutableMap., Object>builder() + .put(TestConfiguration.DURATION_IN_SECONDS, 22) + .put(TestConfiguration.SYSTEM_NAME, "[TestDefaultValue]") + .put(TestConfiguration.RATIO_LIMIT, 3.0) + .put(TestConfiguration.RATIO_FIELDS, Arrays.asList("Australia", "Burundi", "Colombia")) + .put(TestConfiguration.VERSION_NUMBER, 32046) + .put(TestConfiguration.SKIP_BORING_FEATURES, false) + .put(TestConfiguration.BORING_COLORS, Collections.EMPTY_LIST) + .put(TestConfiguration.DUST_LEVEL, 0.2) + .put(TestConfiguration.USE_COOL_FEATURES, false) + .put(TestConfiguration.COOL_OPTIONS, Arrays.asList("Dinosaurs", "Explosions", "Big trucks")) + .build(); + for (Map.Entry, Object> entry : expectedValues.entrySet()) { + assertThat("Property '" + entry.getKey().getPath() + "' has expected value", + settings.getProperty(entry.getKey()), equalTo(entry.getValue())); + } + } + + private File getConfigFile(String file) { + URL url = getClass().getClassLoader().getResource(file); + if (url == null) { + throw new IllegalStateException("File '" + file + "' could not be loaded"); + } + return new File(url.getFile()); + } + +} diff --git a/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java b/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java new file mode 100644 index 000000000..64e213aed --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/custom/NewSettingTest.java @@ -0,0 +1,84 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Property; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.File; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyDouble; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Test for {@link NewSetting}. + */ +public class NewSettingTest { + + @Test + public void shouldLoadAllConfigs() { + // given + YamlConfiguration file = mock(YamlConfiguration.class); + given(file.getString(anyString(), anyString())).willAnswer(withDefaultArgument()); + given(file.getBoolean(anyString(), anyBoolean())).willAnswer(withDefaultArgument()); + given(file.getDouble(anyString(), anyDouble())).willAnswer(withDefaultArgument()); + given(file.getInt(anyString(), anyInt())).willAnswer(withDefaultArgument()); + + setReturnValue(file, TestConfiguration.VERSION_NUMBER, 20); + setReturnValue(file, TestConfiguration.SKIP_BORING_FEATURES, true); + setReturnValue(file, TestConfiguration.RATIO_LIMIT, 4.25); + setReturnValue(file, TestConfiguration.SYSTEM_NAME, "myTestSys"); + + // when / then + NewSetting settings = new NewSetting(file, new File("conf.txt"), null); + + assertThat(settings.getProperty(TestConfiguration.VERSION_NUMBER), equalTo(20)); + assertThat(settings.getProperty(TestConfiguration.SKIP_BORING_FEATURES), equalTo(true)); + assertThat(settings.getProperty(TestConfiguration.RATIO_LIMIT), equalTo(4.25)); + assertThat(settings.getProperty(TestConfiguration.SYSTEM_NAME), equalTo("myTestSys")); + + assertDefaultValue(TestConfiguration.DURATION_IN_SECONDS, settings); + assertDefaultValue(TestConfiguration.DUST_LEVEL, settings); + assertDefaultValue(TestConfiguration.COOL_OPTIONS, settings); + } + + private static void setReturnValue(YamlConfiguration config, Property property, T value) { + if (value instanceof String) { + when(config.getString(eq(property.getPath()), anyString())).thenReturn((String) value); + } else if (value instanceof Integer) { + when(config.getInt(eq(property.getPath()), anyInt())).thenReturn((Integer) value); + } else if (value instanceof Boolean) { + when(config.getBoolean(eq(property.getPath()), anyBoolean())).thenReturn((Boolean) value); + } else if (value instanceof Double) { + when(config.getDouble(eq(property.getPath()), anyDouble())).thenReturn((Double) value); + } else { + throw new UnsupportedOperationException("Value has unsupported type '" + + (value == null ? "null" : value.getClass().getSimpleName()) + "'"); + } + } + + private static void assertDefaultValue(Property property, NewSetting setting) { + assertThat(property.getPath() + " has default value", + setting.getProperty(property).equals(property.getDefaultValue()), equalTo(true)); + } + + private static Answer withDefaultArgument() { + return new Answer() { + @Override + public T answer(InvocationOnMock invocation) throws Throwable { + // Return the second parameter -> the default + return (T) invocation.getArguments()[1]; + } + }; + } + +} diff --git a/src/test/java/fr/xephi/authme/settings/custom/SettingsClassConsistencyTest.java b/src/test/java/fr/xephi/authme/settings/custom/SettingsClassConsistencyTest.java new file mode 100644 index 000000000..a6b67763a --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/custom/SettingsClassConsistencyTest.java @@ -0,0 +1,119 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.SettingsClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +/** + * Test for {@link SettingsClass} implementations. + */ +public class SettingsClassConsistencyTest { + + private static final String SETTINGS_FOLDER = "src/main/java/fr/xephi/authme/settings/custom"; + private static List> classes; + + @BeforeClass + public static void scanForSettingsClasses() { + File settingsFolder = new File(SETTINGS_FOLDER); + File[] filesInFolder = settingsFolder.listFiles(); + if (filesInFolder == null || filesInFolder.length == 0) { + throw new IllegalStateException("Could not read folder '" + SETTINGS_FOLDER + "'. Is it correct?"); + } + + classes = new ArrayList<>(); + for (File file : filesInFolder) { + Class clazz = getSettingsClassFromFile(file); + if (clazz != null) { + classes.add(clazz); + } + } + System.out.println("Found " + classes.size() + " SettingsClass implementations"); + } + + /** + * Make sure that all {@link Property} instances we define are in public, static, final fields. + */ + @Test + public void shouldHavePublicStaticFinalFields() { + for (Class clazz : classes) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + if (Property.class.isAssignableFrom(field.getType())) { + String fieldName = "Field " + clazz.getSimpleName() + "#" + field.getName(); + assertThat(fieldName + " should be public, static, and final", + isValidConstantField(field), equalTo(true)); + } + } + } + } + + /** + * Make sure that no properties use the same path. + */ + @Test + public void shouldHaveUniquePaths() { + Set paths = new HashSet<>(); + for (Class clazz : classes) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + if (Property.class.isAssignableFrom(field.getType())) { + Property property = + (Property) ReflectionTestUtils.getFieldValue(clazz, null, field.getName()); + if (paths.contains(property.getPath())) { + fail("Path '" + property.getPath() + "' should be used by only one constant"); + } + paths.add(property.getPath()); + } + } + } + } + + @Test + public void shouldHaveHiddenDefaultConstructorOnly() { + for (Class clazz : classes) { + Constructor[] constructors = clazz.getDeclaredConstructors(); + assertThat(clazz + " should only have one constructor", + constructors, arrayWithSize(1)); + assertThat("Constructor of " + clazz + " is private", + Modifier.isPrivate(constructors[0].getModifiers()), equalTo(true)); + } + } + + private static boolean isValidConstantField(Field field) { + int modifiers = field.getModifiers(); + return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers); + } + + private static Class getSettingsClassFromFile(File file) { + String fileName = file.getPath(); + String className = fileName + .substring("src/main/java/".length(), fileName.length() - ".java".length()) + .replace(File.separator, "."); + try { + Class clazz = SettingsClassConsistencyTest.class.getClassLoader().loadClass(className); + if (SettingsClass.class.isAssignableFrom(clazz)) { + return (Class) clazz; + } + return null; + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Could not load class '" + className + "'", e); + } + } + +} diff --git a/src/test/java/fr/xephi/authme/settings/custom/TestConfiguration.java b/src/test/java/fr/xephi/authme/settings/custom/TestConfiguration.java new file mode 100644 index 000000000..5f43262c7 --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/custom/TestConfiguration.java @@ -0,0 +1,46 @@ +package fr.xephi.authme.settings.custom; + +import fr.xephi.authme.settings.domain.Property; +import fr.xephi.authme.settings.domain.PropertyType; +import fr.xephi.authme.settings.domain.SettingsClass; + +import java.util.List; + +import static fr.xephi.authme.settings.domain.Property.newProperty; + +/** + * Sample properties for testing purposes. + */ +class TestConfiguration implements SettingsClass { + + public static final Property DURATION_IN_SECONDS = + newProperty("test.duration", 4); + + public static final Property SYSTEM_NAME = + newProperty("test.systemName", "[TestDefaultValue]"); + + public static final Property RATIO_LIMIT = + newProperty(PropertyType.DOUBLE, "sample.ratio.limit", 3.0); + + public static final Property> RATIO_FIELDS = + newProperty(PropertyType.STRING_LIST, "sample.ratio.fields", "a", "b", "c"); + + public static final Property VERSION_NUMBER = + newProperty("version", 32046); + + public static final Property SKIP_BORING_FEATURES = + newProperty("features.boring.skip", false); + + public static final Property> BORING_COLORS = + newProperty(PropertyType.STRING_LIST, "features.boring.colors"); + + public static final Property DUST_LEVEL = + newProperty(PropertyType.DOUBLE, "features.boring.dustLevel", 0.2); + + public static final Property USE_COOL_FEATURES = + newProperty("features.cool.enabled", false); + + public static final Property> COOL_OPTIONS = + newProperty(PropertyType.STRING_LIST, "features.cool.options", "Sparks", "Sprinkles"); + +} diff --git a/src/test/java/fr/xephi/authme/settings/domain/EnumPropertyTypeTest.java b/src/test/java/fr/xephi/authme/settings/domain/EnumPropertyTypeTest.java new file mode 100644 index 000000000..461314e8d --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/domain/EnumPropertyTypeTest.java @@ -0,0 +1,118 @@ +package fr.xephi.authme.settings.domain; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.Test; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Test for {@link EnumPropertyType}. + */ +public class EnumPropertyTypeTest { + + @Test + public void shouldReturnCorrectEnumValue() { + // given + PropertyType propertyType = new EnumPropertyType<>(TestEnum.class); + Property property = Property.newProperty(TestEnum.class, "enum.path", TestEnum.ENTRY_C); + YamlConfiguration configuration = mock(YamlConfiguration.class); + given(configuration.getString(property.getPath())).willReturn("Entry_B"); + + // when + TestEnum result = propertyType.getFromFile(property, configuration); + + // then + assertThat(result, equalTo(TestEnum.ENTRY_B)); + } + + @Test + public void shouldFallBackToDefaultForInvalidValue() { + // given + PropertyType propertyType = new EnumPropertyType<>(TestEnum.class); + Property property = Property.newProperty(TestEnum.class, "enum.path", TestEnum.ENTRY_C); + YamlConfiguration configuration = mock(YamlConfiguration.class); + given(configuration.getString(property.getPath())).willReturn("Bogus"); + + // when + TestEnum result = propertyType.getFromFile(property, configuration); + + // then + assertThat(result, equalTo(TestEnum.ENTRY_C)); + } + + @Test + public void shouldFallBackToDefaultForNonExistentValue() { + // given + PropertyType propertyType = new EnumPropertyType<>(TestEnum.class); + Property property = Property.newProperty(TestEnum.class, "enum.path", TestEnum.ENTRY_C); + YamlConfiguration configuration = mock(YamlConfiguration.class); + given(configuration.getString(property.getPath())).willReturn(null); + + // when + TestEnum result = propertyType.getFromFile(property, configuration); + + // then + assertThat(result, equalTo(TestEnum.ENTRY_C)); + } + + @Test + public void shouldReturnTrueForContainsCheck() { + // given + PropertyType propertyType = new EnumPropertyType<>(TestEnum.class); + Property property = Property.newProperty(TestEnum.class, "my.test.path", TestEnum.ENTRY_C); + YamlConfiguration configuration = mock(YamlConfiguration.class); + given(configuration.contains(property.getPath())).willReturn(true); + given(configuration.getString(property.getPath())).willReturn("ENTRY_B"); + + // when + boolean result = propertyType.contains(property, configuration); + + // then + assertThat(result, equalTo(true)); + } + + @Test + public void shouldReturnFalseForFileWithoutConfig() { + // given + PropertyType propertyType = new EnumPropertyType<>(TestEnum.class); + Property property = Property.newProperty(TestEnum.class, "my.test.path", TestEnum.ENTRY_C); + YamlConfiguration configuration = mock(YamlConfiguration.class); + given(configuration.contains(property.getPath())).willReturn(false); + + // when + boolean result = propertyType.contains(property, configuration); + + // then + assertThat(result, equalTo(false)); + } + + @Test + public void shouldReturnFalseForUnknownValue() { + // given + PropertyType propertyType = new EnumPropertyType<>(TestEnum.class); + Property property = Property.newProperty(TestEnum.class, "my.test.path", TestEnum.ENTRY_C); + YamlConfiguration configuration = mock(YamlConfiguration.class); + given(configuration.contains(property.getPath())).willReturn(true); + given(configuration.getString(property.getPath())).willReturn("wrong value"); + + // when + boolean result = propertyType.contains(property, configuration); + + // then + assertThat(result, equalTo(false)); + } + + + private enum TestEnum { + + ENTRY_A, + + ENTRY_B, + + ENTRY_C + + } +} diff --git a/src/test/java/fr/xephi/authme/settings/domain/PropertyTypeTest.java b/src/test/java/fr/xephi/authme/settings/domain/PropertyTypeTest.java new file mode 100644 index 000000000..6dce2ad3e --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/domain/PropertyTypeTest.java @@ -0,0 +1,182 @@ +package fr.xephi.authme.settings.domain; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyDouble; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Test for {@link PropertyType} and the contained subtypes. + */ +public class PropertyTypeTest { + + private static YamlConfiguration configuration; + + @BeforeClass + public static void setUpYamlConfigurationMock() { + configuration = mock(YamlConfiguration.class); + + when(configuration.getBoolean(eq("bool.path.test"), anyBoolean())).thenReturn(true); + when(configuration.getBoolean(eq("bool.path.wrong"), anyBoolean())).thenAnswer(secondParameter()); + when(configuration.getDouble(eq("double.path.test"), anyDouble())).thenReturn(-6.4); + when(configuration.getDouble(eq("double.path.wrong"), anyDouble())).thenAnswer(secondParameter()); + when(configuration.getInt(eq("int.path.test"), anyInt())).thenReturn(27); + when(configuration.getInt(eq("int.path.wrong"), anyInt())).thenAnswer(secondParameter()); + when(configuration.getString(eq("str.path.test"), anyString())).thenReturn("Test value"); + when(configuration.getString(eq("str.path.wrong"), anyString())).thenAnswer(secondParameter()); + when(configuration.isList("list.path.test")).thenReturn(true); + when(configuration.getStringList("list.path.test")).thenReturn(Arrays.asList("test1", "Test2", "3rd test")); + when(configuration.isList("list.path.wrong")).thenReturn(false); + } + + /* Boolean */ + @Test + public void shouldGetBoolValue() { + // given + Property property = Property.newProperty("bool.path.test", false); + + // when + boolean result = property.getFromFile(configuration); + + // then + assertThat(result, equalTo(true)); + } + + @Test + public void shouldGetBoolDefault() { + // given + Property property = Property.newProperty("bool.path.wrong", true); + + // when + boolean result = property.getFromFile(configuration); + + // then + assertThat(result, equalTo(true)); + } + + /* Double */ + @Test + public void shouldGetDoubleValue() { + // given + Property property = Property.newProperty(PropertyType.DOUBLE, "double.path.test", 3.8); + + // when + double result = property.getFromFile(configuration); + + // then + assertThat(result, equalTo(-6.4)); + } + + @Test + public void shouldGetDoubleDefault() { + // given + Property property = Property.newProperty(PropertyType.DOUBLE, "double.path.wrong", 12.0); + + // when + double result = property.getFromFile(configuration); + + // then + assertThat(result, equalTo(12.0)); + } + + /* Integer */ + @Test + public void shouldGetIntValue() { + // given + Property property = Property.newProperty("int.path.test", 3); + + // when + int result = property.getFromFile(configuration); + + // then + assertThat(result, equalTo(27)); + } + + @Test + public void shouldGetIntDefault() { + // given + Property property = Property.newProperty("int.path.wrong", -10); + + // when + int result = property.getFromFile(configuration); + + // then + assertThat(result, equalTo(-10)); + } + + /* String */ + @Test + public void shouldGetStringValue() { + // given + Property property = Property.newProperty("str.path.test", "unused default"); + + // when + String result = property.getFromFile(configuration); + + // then + assertThat(result, equalTo("Test value")); + } + + @Test + public void shouldGetStringDefault() { + // given + Property property = Property.newProperty("str.path.wrong", "given default value"); + + // when + String result = property.getFromFile(configuration); + + // then + assertThat(result, equalTo("given default value")); + } + + /* String list */ + @Test + public void shouldGetStringListValue() { + // given + Property> property = Property.newProperty(PropertyType.STRING_LIST, "list.path.test", "1", "b"); + + // when + List result = property.getFromFile(configuration); + + // then + assertThat(result, contains("test1", "Test2", "3rd test")); + } + + @Test + public void shouldGetStringListDefault() { + // given + Property> property = + Property.newProperty(PropertyType.STRING_LIST, "list.path.wrong", "default", "list", "elements"); + + // when + List result = property.getFromFile(configuration); + + // then + assertThat(result, contains("default", "list", "elements")); + } + + private static Answer secondParameter() { + return new Answer() { + @Override + public T answer(InvocationOnMock invocation) throws Throwable { + // Return the second parameter -> the default + return (T) invocation.getArguments()[1]; + } + }; + } +} diff --git a/src/test/java/fr/xephi/authme/settings/propertymap/PropertyMapTest.java b/src/test/java/fr/xephi/authme/settings/propertymap/PropertyMapTest.java new file mode 100644 index 000000000..40ea14b74 --- /dev/null +++ b/src/test/java/fr/xephi/authme/settings/propertymap/PropertyMapTest.java @@ -0,0 +1,53 @@ +package fr.xephi.authme.settings.propertymap; + +import fr.xephi.authme.settings.domain.Property; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +import static org.hamcrest.Matchers.contains; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Test for {@link PropertyMap}. + */ +public class PropertyMapTest { + + @Test + public void shouldKeepEntriesByInsertionAndGroup() { + // given + List paths = Arrays.asList("japan", "indonesia.jakarta", "japan.tokyo", "china.shanghai", "egypt.cairo", + "china.shenzhen", "china", "indonesia.jakarta.tugu", "egypt", "japan.nagoya", "japan.tokyo.taito"); + PropertyMap map = new PropertyMap(); + + // when + for (String path : paths) { + Property property = createPropertyWithPath(path); + map.put(property, new String[0]); + } + + // then + Set, String[]>> entrySet = map.entrySet(); + List resultPaths = new ArrayList<>(entrySet.size()); + for (Map.Entry, String[]> entry : entrySet) { + resultPaths.add(entry.getKey().getPath()); + } + + Assert.assertThat(resultPaths, contains("japan", "japan.tokyo", "japan.tokyo.taito", "japan.nagoya", + "indonesia.jakarta", "indonesia.jakarta.tugu", "china", "china.shanghai", "china.shenzhen", + "egypt", "egypt.cairo")); + } + + private static Property createPropertyWithPath(String path) { + Property property = mock(Property.class); + when(property.getPath()).thenReturn(path); + return property; + } +} diff --git a/src/test/resources/config-incomplete-sample.yml b/src/test/resources/config-incomplete-sample.yml new file mode 100644 index 000000000..a29879720 --- /dev/null +++ b/src/test/resources/config-incomplete-sample.yml @@ -0,0 +1,27 @@ +# Test config file with missing options from TestConfiguration +# Notice the commented out lines! + +test: + duration: 22 +# systemName: 'Custom sys name' +sample: + ratio: +# limit: 3.0 + fields: + - 'Australia' + - 'Burundi' + - 'Colombia' +#version: 2492 +features: +# boring: +# skip: false +# colors: +# - 'beige' +# - 'gray' +# dustLevel: 0.81 + cool: +# enabled: true + options: + - 'Dinosaurs' + - 'Explosions' + - 'Big trucks' diff --git a/src/test/resources/config-sample-values.yml b/src/test/resources/config-sample-values.yml new file mode 100644 index 000000000..1bd99d771 --- /dev/null +++ b/src/test/resources/config-sample-values.yml @@ -0,0 +1,27 @@ +# Test config file with all options +# defined in the TestConfiguration class + +test: + duration: 22 + systemName: 'Custom sys name' +sample: + ratio: + limit: -4.1 + fields: + - 'Australia' + - 'Burundi' + - 'Colombia' +version: 2492 +features: + boring: + skip: false + colors: + - 'beige' + - 'gray' + dustLevel: 0.81 + cool: + enabled: true + options: + - 'Dinosaurs' + - 'Explosions' + - 'Big trucks'